The underlying idea of a dispatch semaphore isn't difficult to understand. Remember that a semaphore is nothing more than a variable that can be incremented and decremented in a thread safe manner. The most challenging aspect of a dispatch semaphore is correctly using it. Working in a multithreaded environment is complex. Semaphores help you manage that complexity.

Images

We revisit Images, a project we explored earlier in this series. The application shows a table view, listing a collection of images. Each table view cell has an image on the left and a title on the right.

Images

The current implementation takes advantage of the DispatchWorkItem class to efficiently fetch images from a remote server.

In this episode, we refactor the application in such a way that it doesn't stress the system's resources too much. Every time the user scrolls the table view, the application fetches images from a remote server. If you continue to scroll the table view, the number of concurrent requests increases substantially.

To lower the stress on the system, we refactor the current implementation. The ImageTableViewCell class will no longer be responsible for fetching a remote resource. The application will delegate that task to a service that is dedicated to fetching the remote resources. The service will limit the number of concurrent requests to lower the stress on the system.

This strategy has a number of advantages. By delegating this task to a single object, that object can make more calculated decisions. It is aware of every request the application makes and can take that information into account to decide when it's appropriate to initiate a request for a remote resource.

If the device has a slow cellular connection, for example, it can decide to reduce the maximum number of concurrent requests. The moment the device has access to a Wi-Fi connection, it can increase the maximum number of concurrent requests to increase application performance.

Let's get to work. We start by creating a group with name Services. We add a new Swift file to the group and name it ImageService.swift. Add an import statement for UIKit and Foundation, and define a class with name ImageService.

import UIKit
import Foundation

class ImageService {

}

The idea is simple. The ImageService class uses a concurrent dispatch queue to fetch a remote resource. The concurrent dispatch queue is private to the ImageService class. We define a straightforward API other objects can use to fetch a remote resource.

Define a property, imageQueue, for the concurrent dispatch queue. We initialize a DispatchQueue instance and store a reference in the imageQueue property. We initialize the dispatch queue with label Image Queue and set its quality of service class to utility. Remember that a dispatch queue is serial by default. To create a concurrent dispatch queue, we pass the concurrent attribute to the initializer.

import UIKit
import Foundation

class ImageService {

    // MARK: - Properties

    private let imageQueue = DispatchQueue(label: "Image Queue", qos: .utility, attributes: [.concurrent])

}

The next step is defining the API to fetch a remote resource. We define a method that accepts a URL instance and a completion handler. The completion handler is invoked when the ImageService instance has fetched the remote resource. The completion handler accepts one argument of type UIImage?.

import UIKit
import Foundation

class ImageService {

    // MARK: - Properties

    private let imageQueue = DispatchQueue(label: "Image Queue", qos: .utility, attributes: [.concurrent])

    // MARK: - Public API

    func image(with url: URL, completion: @escaping (UIImage?) -> Void) {

    }

}

The implementation is fairly straightforward. We submit a block of work to the concurrent dispatch queue for asynchronous execution. In the block of work, we define a variable, image, of type UIImage? and we fetch the data for the remote resource. The data is used to create a UIImage instance, which is assigned to the image variable.

To make the life of the consumer of the API easier, we execute the completion handler on the main thread. We submit a closure to the main dispatch queue and execute the completion handler in that closure.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Helpers
        var image: UIImage?

        // Load Data
        if let data = try? Data(contentsOf: url) {
            // Initialize Image
            image = UIImage(data: data)
        }

        // Invoke Handler on Main Thread
        DispatchQueue.main.async {
            completion(image)
        }
    }
}

Before we start using the ImageService class, we add a few print statements to better understand how the ImageService class manages the requests for remote resources. Define a private property, counter, of type Int with an initial value of 0.

import UIKit
import Foundation

class ImageService {

    // MARK: - Properties

    private let imageQueue = DispatchQueue(label: "Image Queue", qos: .utility, attributes: [.concurrent])

    // MARK: -

    private var counter: Int = 0

    ...

}

The value of counter is incremented in the block we submit to the concurrent dispatch queue. We cache the current value of counter in a local variable with name localCounter.

We print START and the value stored in localCounter before fetching the data for the remote resource. We print END and the value stored in localCounter after invoking the completion handler that is passed to the image(with:completion:) method. Because several requests can be executed concurrently, we need to cache the current value of counter in a local variable.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Increment Counter
        self.counter += 1
        let localCounter = self.counter

        // Helpers
        var image: UIImage?

        print("START \(localCounter)")

        // Load Data
        if let data = try? Data(contentsOf: url) {
            // Initialize Image
            image = UIImage(data: data)
        }

        // Invoke Handler on Main Thread
        DispatchQueue.main.async {
            completion(image)

            print("END \(localCounter)")
        }
    }
}

Refactoring ImageTableViewCell

The ImageTableViewCell class is no longer responsible for fetching the remote resource it displays. Open ImageTableViewCell.swift and navigate to the configure(title:url:) method. We can remove the url parameter since the ImageTableViewCell class no longer needs to know the source of the remote resource it's displaying.

func configure(title: String) {
    ...
}

We can remove the if statement of the configure(title:url:) method. The ImageTableViewCell class is no longer responsible for fetching the remote resource.

func configure(title: String) {
    // Configure Title Label
    titleLabel.text = title

    // Animate Activity Indicator View
    activityIndicatorView.startAnimating()
}

We also remove the fetchDataWorkItem property and any references to it.

import UIKit

final class ImageTableViewCell: UITableViewCell {

    // MARK: - Static Properties

    static var reuseIdentifier: String {
        return String(describing: self)
    }

    // MARK: - Properties

    @IBOutlet private var titleLabel: UILabel!

    // MARK: -

    @IBOutlet private var thumbnailImageView: UIImageView!

    // MARK: -

    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

    // MARK: - Public API

    func configure(title: String) {
        // Configure Title Label
        titleLabel.text = title

        // Animate Activity Indicator View
        activityIndicatorView.startAnimating()
    }

    // MARK: - Overrides

    override func prepareForReuse() {
        super.prepareForReuse()

        // Reset Thumnail Image View
        thumbnailImageView.image = nil
    }

}

Refactoring ImagesViewController

The next class we need to update is the ImagesViewController class. The ImagesViewController instance is the owner of the image service. Define a property with name imageService. We initialize an instance of the ImageService class and store a reference in the imageService property.

import UIKit

final class ImagesViewController: UITableViewController {

    // MARK: - Types

    private struct Image {

        // MARK: - Properties

        let title: String

        // MARK: -

        let url: URL?

    }

    // MARK: - Properties

    private let imageService = ImageService()

    ...

}

The only other change we need to make is updating the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol. We update the configure(with:) method since we no longer need to pass it a URL instance.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.reuseIdentifier, for: indexPath) as? ImageTableViewCell else {
        fatalError("Unable to Dequeue Image Table View Cell")
    }

    // Fetch Image
    let image = dataSource[indexPath.row]

    // Configure Cell
    cell.configure(title: image.title)

    return cell
}

We safely unwrap the value of the url property of the Image instance and pass it to the image(with:completion:) method of the ImageService instance. In the block we pass to the image(with:completion:) method, we assign the UIImage instance to the image property of the thumbnail image view of the table view cell. Remember that the completion handler is invoked on the main thread, which means we don't need to worry about threading violations.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.reuseIdentifier, for: indexPath) as? ImageTableViewCell else {
        fatalError("Unable to Dequeue Image Table View Cell")
    }

    // Fetch Image
    let image = dataSource[indexPath.row]

    // Configure Cell
    cell.configure(title: image.title)

    if let url = image.url {
        // Fetch Image
        imageService.image(with: url) { (image) in
            cell.thumbnailImageView.image = image
        }
    }

    return cell
}

Build and run the application to see the result. Scroll the table view and inspect the output in the console. This is what the output should look like.

START 1
START 2
START 3
START 4
START 5
START 6
START 7
START 8
START 9
START 10
START 11
START 12
START 13
START 14
START 15
END 3
END 2
END 4
END 6
END 8
END 9
END 5
END 1
END 11
END 10
END 14
END 15
END 13
END 12
END 7

The ImageService instance fetches a remote resource as soon as the image(with:completion:) method is invoked, which means that the application initiates more than a dozen concurrent requests. The problem becomes more prominent the moment the table view is scrolled up and down a few times.

There's plenty of room for improvement and we discussed several options earlier in this series. In this episode, we use a dispatch semaphore to control the maximum number of concurrent requests the ImageService class executes.

Setting Limits With a Semaphore

We need to update the implementation of the ImageService class, but the changes we need to make are surprisingly small. We start by declaring a private, constant property with name semaphore. We initialize a DispatchSemaphore instance and store a reference in the semaphore property. We initialize the semaphore with an initial value of 3. We discussed what that means in the previous episode.

import UIKit
import Foundation

class ImageService {

    // MARK: - Properties

    private let semaphore = DispatchSemaphore(value: 3)

    // MARK: -

    private let imageQueue = DispatchQueue(label: "Image Queue", qos: .utility, attributes: [.concurrent])

    ...

}

We invoke wait() on the semaphore in the closure we submit to the concurrent dispatch queue. Remember that invoking the wait() method decrements the value of the semaphore. We set the initial value of the semaphore to 3, which means that it won't immediately block the execution of the thread the block of work is executed on. In other words, the initial value of the semaphore defines the maximum number of concurrent requests the ImageService class executes.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Wait
        self.semaphore.wait()

        // Increment Counter
        self.counter += 1
        let localCounter = self.counter

        // Helpers
        var image: UIImage?

        print("START \(localCounter)")

        // Load Data
        if let data = try? Data(contentsOf: url) {
            // Initialize Image
            image = UIImage(data: data)
        }

        // Invoke Handler on Main Thread
        DispatchQueue.main.async {
            completion(image)

            print("END \(localCounter)")
        }
    }
}

We signal the semaphore after invoking the completion handler of the image(with:completion:) method. Remember that it's essential that the signal() method is invoked on a different thread to avoid a deadlock. We discussed this in the previous episode.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Wait
        self.semaphore.wait()

        // Increment Counter
        self.counter += 1
        let localCounter = self.counter

        // Helpers
        var image: UIImage?

        print("START \(localCounter)")

        // Load Data
        if let data = try? Data(contentsOf: url) {
            // Initialize Image
            image = UIImage(data: data)
        }

        // Invoke Handler on Main Thread
        DispatchQueue.main.async {
            completion(image)

            // Signal
            self.semaphore.signal()

            print("END \(localCounter)")
        }
    }
}

Build and run the application to see the result. Scroll the table view and inspect the output in the console. The output looks very different.

START 2
START 3
START 1
END 3
START 4
END 2
START 5
END 1
START 6
END 4
START 7
END 6
START 8
END 7
START 9
END 8
START 10
END 9
START 11
END 11
START 12
END 12
START 13
END 10
START 14
END 13
START 15
END 15
END 14
END 5

The semaphore limits the maximum number of concurrent requests the ImageService class executes. The output shows that a pending request is executed as soon as a request has completed executing.

Waiting With a Timeout

Waiting indefinitely for a task to finish isn't always the best option. It is at times safer to wait for a predefined amount of time and continue if that amount of time has passed. The DispatchSemaphore class offers this option. Using a timeout is a recommended strategy for operations that rely on the network. The network is unpredictable and you need to guard against that unpredictability.

The DispatchSemaphore class defines three variants of the wait method. We already know the behavior of the wait() method. If we invoke the wait(timeout:) method or the wait(wallTimeout:) method, we can specify a timeout. The behavior is similar to that of the DispatchGroup and DispatchWorkItem classes, which we covered earlier in this series.

If the timeout that is passed to the wait method is exceeded, execution of the current thread continues. This is useful if the application is waiting for a resource, but it doesn't want to wait indefinitely. Keep in mind that the current thread continues execution as soon as the value of the semaphore is equal to or higher than 0.

We covered the difference between DispatchTime and DispatchWallTime earlier in this series. Remember that DispatchTime represents a relative point in time whereas DispatchWallTime represents an absolute point in time.

Replace the wait() method with the wait variant that accepts a DispatchWallTime instance as its argument. If the semaphore isn't signaled before the timeout is exceeded, it continues execution of the current thread.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Wait
        self.semaphore.wait(wallTimeout: .now() + 5.0)

        ...
    }
}

The wait variants that accept a timeout also return a result. The returned value indicates whether the wait method returned as a result of a timeout or because the semaphore was signaled before the timeout was exceeded. Let's store the result of the wait(wallTimeout:) method in a constant and print its value to the console.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        // Wait
        let result = self.semaphore.wait(wallTimeout: .now() + 5.0)

        // Increment Counter
        self.counter += 1
        let localCounter = self.counter

        // Helpers
        var image: UIImage?

        print("START \(localCounter) | TIMED OUT \(result == .timedOut)")

        ...
    }
}

Build and run the application and inspect the output in the console. The images the application fetches are quite large and the output in the console depends on the speed of the network connection of your device or your machine.

START 1 | TIMED OUT false
START 2 | TIMED OUT false
START 3 | TIMED OUT false
END 1
START 4 | TIMED OUT false
END 4
START 5 | TIMED OUT false
END 5
START 6 | TIMED OUT false
END 2
START 7 | TIMED OUT false
END 3
START 8 | TIMED OUT false
END 6
START 9 | TIMED OUT false
END 9
START 10 | TIMED OUT false
START 11 | TIMED OUT true
START 12 | TIMED OUT true
START 13 | TIMED OUT true
START 14 | TIMED OUT true
START 15 | TIMED OUT true
END 11
END 12
END 13
END 15
END 14
END 7
END 10
END 8

Sleeping and Waking

There's one last tidbit I'd like to share. The signal() method returns a value of type Int. If the value returned by the signal() method is not equal to 0, then signaling the semaphore woke up a thread that was put to sleep by the semaphore. I've never found a valid use case for the return value of the signal() method, but I'm sure it can be useful in certain scenarios.

Let's store the return value of the signal() method in a constant with name result and include the value in the last print statement.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    imageQueue.async {
        ...

        // Invoke Handler on Main Thread
        DispatchQueue.main.async {
            completion(image)

            // Signal
            let result = self.semaphore.signal()

            print("END \(localCounter) | THREAD WOKEN \(result > 0)")
        }
    }
}

Build and run the application and inspect the output in the console. If you understand how a counting semaphore works, then you should be able to predict the output in the console.

START 1 | TIMED OUT false
START 2 | TIMED OUT false
START 3 | TIMED OUT false
END 3 | THREAD WOKEN true
START 4 | TIMED OUT false
END 2 | THREAD WOKEN true
END 1 | THREAD WOKEN true
START 5 | TIMED OUT false
START 6 | TIMED OUT false
END 4 | THREAD WOKEN true
START 7 | TIMED OUT false
END 7 | THREAD WOKEN true
START 8 | TIMED OUT false
END 8 | THREAD WOKEN true
START 9 | TIMED OUT false
END 5 | THREAD WOKEN true
START 10 | TIMED OUT false
END 9 | THREAD WOKEN true
START 11 | TIMED OUT false
END 11 | THREAD WOKEN true
START 12 | TIMED OUT false
END 6 | THREAD WOKEN true
START 13 | TIMED OUT false
END 12 | THREAD WOKEN true
START 14 | TIMED OUT false
END 10 | THREAD WOKEN true
START 15 | TIMED OUT false
END 14 | THREAD WOKEN false
END 15 | THREAD WOKEN false
END 13 | THREAD WOKEN false

Remember the Pitfalls

You may have wondered why we invoke the wait(wallTimeout:) method in the closure that is submitted to the image queue. Let's find out what happens if we invoke the wait(wallTimeout:) method before submitting the closure to the concurrent dispatch queue.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    // Wait
    let result = self.semaphore.wait(wallTimeout: .now() + 5.0)

    imageQueue.async {
        ...
    }
}

Build and run the application. The application shows a blank screen and nothing seems to happen. The user isn't able to interact with the application. What is happening?

Images

The output in the console offers us a clue.

START 1 | TIMED OUT success
START 2 | TIMED OUT success
START 3 | TIMED OUT success
START 4 | TIMED OUT timedOut
START 5 | TIMED OUT timedOut
START 6 | TIMED OUT timedOut
START 7 | TIMED OUT timedOut

Remember from the previous episode that you should never wait on the main thread. The image(url:completion:) method of the ImageService class can be invoked on any thread, including the main thread. In this example, the image(url:completion:) method is invoked on the main thread. The semaphore puts the main thread to sleep, rendering the application unresponsive.

I also emphasized in the previous episode that the wait() and signal() methods should be invoked on different threads to avoid a deadlock. In this example, the application is only able to recover because we specify a timeout for the wait method. Remove the timeout from the wait method and run the application one more time.

func image(with url: URL, completion: @escaping (UIImage?) -> Void) {
    // Wait
    self.semaphore.wait()

    imageQueue.async {
        ...
    }
}

The output in the console shows us the problem.

START 1
START 2
START 3

The semaphore blocks the main thread when its value drops below 0. The main thread remains blocked until the semaphore is signaled. The problem is that we invoke the signal() method on the main thread, the thread that is being blocked by the semaphore. We have caused a deadlock the application is unable to recover from.

Control and Flexibility

It's important that you understand that a semaphore can be applied in a range of scenarios. Controlling access to a shared resource is one such scenario. Limiting the number of concurrent operations is another example.

The documentation of the DispatchSemaphore class is short not only because the documentation is sparse but also because the API is simple and concise. Semaphores help manage complexity and improve safety. You won't use them often, but they are indispensable in a multithreaded environment.