Grand Central Dispatch Tutorial for Swift 5: Part 1/2

Learn all about multithreading, dispatch queues and concurrency in the first part of this Swift 5 tutorial on Grand Central Dispatch. By Fabrizio Brancati.

4.3 (6) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Managing Singletons

Singletons are as popular on iOS as cat photos on the web.

One frequent concern with singletons is that often they’re not thread-safe. This concern is justified, given their use: Singletons are often used from multiple controllers accessing the singleton instance simultaneously. Your PhotoManager class is a singleton, so you’ll need to consider this issue.

Thread-safe code can safely be called from multiple threads or concurrent tasks without causing any problems such as data corruption or app crashes. Code that isn’t thread-safe can only run in one context at a time.

There are two thread-safety cases to consider:

  • During initialization of the singleton instance.
  • During reads and writes to the instance.

Initialization turns out to be the easy case because of how Swift initializes static properties. It initializes static properties when they’re first accessed, and it guarantees initialization is atomic. That is, Swift treats the code performing initialization as a critical section and guarantees it completes before any other thread gets access to the static property.

A critical section is a piece of code that must not execute concurrently, that is, from two threads at once. This is usually because the code manipulates a shared resource such as a property that can become corrupt if it’s accessed by concurrent processes.

Open PhotoManager.swift to see how you initialize the singleton:

final class PhotoManager {
  private init() {}
  static let shared = PhotoManager()
}

The private initializer makes sure the only PhotoManager instance is the one assigned to shared. This way, you don’t have to worry about syncing changes to your photo store between different managers.

You still have to deal with thread safety when accessing code in the singleton that manipulates shared internal data. Handle this through methods such as synchronizing data access. You’ll see one approach in the next section.

Handling the Readers-Writers Problem

In Swift, any property declared with the let keyword is a constant and, therefore, read-only and thread-safe. Declare the property with the var keyword, however, and it becomes mutable and not thread-safe unless the data type is designed to be so. The Swift collection types like Array and Dictionary aren’t thread-safe when declared mutable.

Although many threads can read a mutable instance of Array simultaneously without issue, it’s not safe to let one thread modify the array while another is reading it. Your singleton doesn’t prevent this condition from happening in its current state.

To see the problem, look at addPhoto(_:) in PhotoManager.swift, which is reproduced below:

func addPhoto(_ photo: Photo) {
  unsafePhotos.append(photo)
  DispatchQueue.main.async { [weak self] in
    self?.postContentAddedNotification()
  }
}

This is a write method as it modifies a mutable array object.

Now, look at the photos property, reproduced below:

private var unsafePhotos: [Photo] = []
  
var photos: [Photo] {
  return unsafePhotos
}

The getter for this property is termed a read method, as it’s reading the mutable array. The caller gets a copy of the array and is protected against inappropriately mutating the original array. However, this doesn’t provide any protection against one thread calling the write method addPhoto(_:) while another thread simultaneously calls the getter for the photos property.

That’s why the backing property is named unsafePhotos — if it’s accessed on the wrong thread, you can get some wacky behavior!

Passing by value results in a copy of the object, and changes to the copy won’t affect the original. By default in Swift, class instances are passed by reference and structs are passed by value. Swift’s built-in data types like Array and Dictionary are implemented as structs.

It may look like there’s a lot of copying in your code when passing collections back and forth. Don’t worry about the memory usage implications of this. The Swift collection types are optimized to make copies only when necessary, for instance, when your app modifies an array passed by value for the first time.

Note: In the code above, why does the caller get a copy of the photos array? In Swift, parameters and return types of functions are either passed by reference or by value.

Reasoning About Dispatch Barriers

This is the classic software development Readers-Writers Problem. GCD provides an elegant solution of creating a read/write lock using dispatch barriers. Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues.

When you submit a DispatchWorkItem to a dispatch queue, you can set flags to indicate that it should be the only item executed on the specified queue for that particular time. This means all items submitted to the queue prior to the dispatch barrier must complete before DispatchWorkItem executes.

When DispatchWorkItem‘s turn arrives, the barrier executes it and ensures the queue doesn’t execute any other tasks during that time. Once finished, the queue returns to its default implementation.

The diagram below illustrates the effect of a barrier on various asynchronous tasks:

GooglyPuff app chart showing tasks and barrier task

Notice how in normal operation, the queue acts just like a normal concurrent queue. But when the barrier is executing, it essentially acts as a serial queue. That is, the barrier is the only thing executing. After the barrier finishes, the queue goes back to being a normal concurrent queue.

Use caution when using barriers in global background concurrent queues, as these queues are shared resources. Using barriers in a custom serial queue is redundant, as it already executes serially. Using barriers in the custom concurrent queue is a great choice for handling thread safety in atomic or critical areas of code.

You’ll use a custom concurrent queue to handle your barrier function and separate the read and write functions. The concurrent queue will allow multiple read operations simultaneously.

Open PhotoManager.swift and add a private property just above the unsafePhotos declaration:

private let concurrentPhotoQueue =
  DispatchQueue(
    label: "com.raywenderlich.GooglyPuff.photoQueue",
    attributes: .concurrent)

This initializes concurrentPhotoQueue as a concurrent queue. You set up label with a descriptive name that’s helpful during debugging. Typically, you use the reverse DNS style naming convention.

Next, replace addPhoto(_:) with the following code:

func addPhoto(_ photo: Photo) {
  // 1
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}

Here’s how your new write method works:

  1. You dispatch the write operation asynchronously with a barrier. When it executes, it’s the only item in your queue.
  2. You add the object to the array.
  3. Finally, you post a notification that you’ve added the photo. You must post this notification on the main thread because it will do UI work. So you dispatch another task asynchronously to the main queue to trigger the notification.

This takes care of the write, but you also need to implement the photos read method.

To ensure thread safety with your writes, you need to perform reads on concurrentPhotoQueue. You need return data from the function call, so an asynchronous dispatch won’t cut it. In this case, sync would be an excellent candidate.

Use sync to keep track of your work with dispatch barriers or when you need to wait for the operation to finish before you can use the data processed by the closure.

You need to be careful, though. Imagine if you call sync and target the current queue you’re already running on. This would result in a deadlock situation.

Two or more items — in most cases, threads — deadlock if they get stuck waiting for each other to complete or perform another action. The first can’t finish because it’s waiting for the second to finish, but the second can’t finish because it’s waiting for the first to finish.

In your case, the sync call will wait until the closure finishes, but the closure can’t finish — or even start! — until the currently executing closure finishes, which it can’t! This should force you to be conscious of which queue you’re calling from — as well as which queue you’re passing in.

Here’s a quick overview of when and where to use sync:

  • Main queue: Be very careful for the same reasons as above. This situation also has potential for a deadlock condition, which is especially bad on the main queue because the whole app will become unresponsive.
  • Global queue: This is a good candidate to sync work through dispatch barriers or when waiting for a task to complete so you can perform further processing.
  • Custom serial queue: Be very careful in this situation. If you’re running in a queue and call sync targeting the same queue, you’ll definitely create a deadlock.

Still in PhotoManager.swift, modify the photos property getter:

var photos: [Photo] {
  var photosCopy: [Photo] = []

  // 1
  concurrentPhotoQueue.sync {
    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

Here’s what’s going on, step by step:

  1. Dispatch synchronously onto the concurrentPhotoQueue to perform the read.
  2. Store a copy of the photo array in photosCopy and return it.

Build and run the app. Download photos through Le Internet. It should behave as before, but underneath the hood, you have some happy threads.

GooglyPuff app displaying three photos to choose from

Congratulations — your PhotoManager singleton is now thread-safe! No matter where or how you read or write photos, you can be confident that it will happen in a safe manner with no surprises.