Your Friendly iOS Coder

Dabby [ダビー]

Ensuring Thread-safety Using GCD

When multiple operations that modify a shared resource are performed concurrently, there is always a risk of losing data integrity. In this post, we are going to create a means of ensuring thread-safety using Grand Central Dispatch (GCD).

DispatchQueue.async{...} defers the execution of the block to be done asynchronously. By default, DispatchQueues are sequential (only one operation can be executed at a given time in the queue). Using this fact, we can easily ensure thread safety by always wrapping the critical paths of the concurrent operation in a DispatchQueue.async {…} call. Consider the following example:

Let’s assume we have a function called execute that does something important.

1
2
3
func execute() {
	print("executing...")
}

We are performing 10 concurrent operations that all invoke execute, but we only want to invoke the execute function once.

  • Incorrect
1
2
3
4
5
6
7
var didExecute = false
DispatchQueue.concurrentPerform(iterations: 10) { _ in
	guard !didExecute else { return }

	execute()
	didExecute = true
}

In the above example, the execute function will get invoked multiple times. This is because at any given time, multiple iterations of the DispatchQueue.concurrentPerform would have interpreted !cancelInvoked to be true, therefore executing the if-block.

  • Correct
1
2
3
4
5
6
7
8
9
10
11
var didExecute = false
let queue = DispatchQueue(label: "queue")

DispatchQueue.concurrentPerform(iterations: 10) { _ in
	queue.async {
		guard !didExecute else { return }

		execute()
		didExecute = true
	}
}

This example gives us the expected behaviour because every iteration of the DispatchQueue.concurrentPerform gets dispatched into the queue to be performed asynchronously. Since the queue is sequential, only one operation will execute at a given time.

Read-Write Lock using GCD

We can use this logic to create a ReadWriteLock that will ensure thread-safety when reading/writing to a resource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ReadWriteLock {
	private let queue: DispatchQueue

	init(label: String) {
		queue = DispatchQueue(label: label, attributes: .concurrent) // (1)
	}

	func read<T>(closure: () -> T) -> T {
		return queue.sync { // (2)
			closure()
		}
	}

	func write(closure: @escaping () -> Void) {
		// using the barrier flag ensures that no
		// other operation will run during a write
		queue.async(flags: .barrier) { // (3)
			closure()
		}
	}
}
  1. We create a private queue, and set it’s attributes to be .concurrent. This means that the queue can perform multiple operations at the same time.
  2. read operations are dispatched synchronously. This ensures that we return from the function with a value. Note that synchronous operations can still be performed concurrently.
  3. write operations are dispatched asynchrounously using a barrier flag. The barrier flag ensures that no other operations are performed during a write. This will help prevent a read and write operation from executing at the same time in the queue. Note that multiple reads can still be executed at the same time, and this is intended.

Effects of barrier flag on concurrent queue

Protected Resources

Using the ReadWriteLock defined above, we can take the abstraction one step further by creating a Protected type which encapsulates all the logic of ensuring thread-safety using ReadWriteLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Protected<Resource: Any> {
    private let lock: ReadWriteLock
    private var resource: Resource

    init(resource: Resource) {
        self.lock = ReadWriteLock(label: "\(Resource.self)")
        self.resource = resource
    }

    func read() -> Resource {
        return lock.read {
            self.resource
        }
    }

    func mutate(closure: @escaping (Resource) -> Resource) {
        lock.write {
            self.resource = closure(self.resource)
        }
    }
}

Let’s take a look at how this can be used.

Imagine we wanted to fetch a set of images using their URLs. We can define a function that take an array of URLs and a completion that gets invoked when all images have been fetched. The completion is successful if and only if all images were fetched without any errors. See the implementation below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
enum Result<T> {
    case success(T)
	case failure(Error)
}

struct FetchImageErrors: Error {
    let errors: [Error]
}

func fetchImages(by urls: [URL], completion: @escaping (Result<[UIImage]>) -> Void) {
    var images = [UIImage?](repeating: nil, count: urls.count)
    let errors = Protected(resource: [Error]())
    let group = DispatchGroup()
	// Abstraction built on URLSession that fetches an image given a URL
    // usage: imageFetcher.fetch(url: URL, completion: @escaping (Result<UIImamge>) -> Void))
    let imageFetcher = ImageFetcher()

    urls.enumerated().forEach { index, url in
        group.enter()
        imageFetcher.fetchImage(url: url, completion: { (result) in
            switch result {
            case .success(let image):
                images[index] = image
            case .failure(let error):
                errors.mutate {
                    var mutableArray = $0
                    mutableArray.append(error)
                    return mutableArray
                }
            }
            group.leave()
        })
    }

    group.notify(queue: .main) {
		let encounteredErrors = errors.read()
        if !encounteredErrors.isEmpty {
            completion(.failure(FetchImageErrors(errors: encounteredErrors)))
        } else {
            completion(.success(images.compactMap { $0 }))
        }
    }
}

In the code above, we are concurrently fetching the images using their respective URLs. In order to ensure integrity of the array of errors, we create an instance Protected<[Error]> to wrap the array of errors resource. If an error occurs when fetching an image, we can safely add the error to the errors resource, else we set the index corresponding to the URL of the image in images array. 🚀

Conclusion

By using GCD, we are able to create a nice abstraction that ensures thread-safety when mutating shared resources concurrently. I tend to use this abstraction when applicable in my personal projects, and I hope this article has provided you with enough insight as to how you can leverage GCD to do the same.

Thanks for taking the time 🙏🏿

Find me on twitter or contact me if you have any questions.