Grand Central Dispatch (GCD) is very clever in that “work submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes.” The last sentence is subtle, but has critical implications: GCD is not for thread-unsafe, shared resources.
The Problem with GCD
Using serial queues is great for enforcing thread-safe arrays or collections. However, more complex resources, such as a database, is where GCD falls short.
Take Realm and Couchbase Lite for examples. To achieve data consistency, results retrieved from the database are “auto-updating” across all threads. When changes are made to the database, internal notifications are triggered to keep your variables updated in real-time. This is possible because the underlying database is shared across various instances.
The problem is when you pass managed resources across threads and changes are made from the other thread. The transaction version would be forked and dangerous to reconcile. So most libraries built around shared resources have left it out, others like Core Data have gone the extra step to enable thread safety for managed objects, but come with their own issues like “faulting“.
You may be thinking to use the same DispatchQueue instance to work with your Realm resource for example. However, this will lead to intermittent crashes because creating Realm resources in a dispatch queue is not guaranteed to be run on the same thread next time you access it even though its the same DispatchQueue instance.
Let’s look at a simple example:
let list = realm.objects(Automobile.self) ... URLSession.shared.dataTask(with: url) { // Update list }.resume()
Data was retrieved from a remote server and we would like to persist any updates from the server. The catch is that the “URLSession.shared.dataTask” completion handler runs on its own operation queue. We run into a crash because the Realm resources are being updated from another thread.
Queues and Threads Are Not Created Equal
The solution for resources like these is to use ‘ol fashion threads instead of GCD:
let list = realm.objects(Automobile.self) let thread = Thread.current ... URLSession.shared.dataTask(with: url) { thread.perform(#selector(update), on: self, with: list, waitUntilDone: false) }.resume() ... func update() { // Update list }
Notice I’m referencing the original thread where the resource was created, which cannot be assumed to be always the main queue. Then I’m performing a process under that thread.
The Thread API’s are super archaic though, so I’ve added some helpers to make it more Swifty:
extension Thread { private typealias Block = @convention(block) () -> Void /** Execute block, used internally for async/sync functions. - parameter block: Process to be executed. */ @objc private func run(block: Block) { block() } /** Perform block on current thread asynchronously. - parameter block: Process to be executed. */ public func async(execute: Block) { guard Thread.current != self else { return execute() } perform(#selector(run(block:)), on: self, with: execute, waitUntilDone: false) } /** Perform block on current thread synchronously. - parameter block: Process to be executed. */ public func sync(execute: Block) { guard Thread.current != self else { return execute() } perform(#selector(run(block:)), on: self, with: execute, waitUntilDone: true) } }
I’ve added new “async” and “sync” functions that accepts a closure. Then performs the process via selector by passing the closure to a private “run” function. Notice the closure has a type alias with “convention“. This is so Swift can pass a closure to the C-language underneath for the thread.
With all this abstracted out, now we can do this:
let list = realm.objects(Automobile.self) let thread = Thread.current ... URLSession.shared.dataTask(with: url) { thread.async { // Update list } }.resume()
This feels more like GCD in Swift 🙂
Bonus
For creating threads and adding queue-like API’s, this Thread library was a gem to work with.
Happy Coding!!
Quick question – why wouldn’t you just use Barriers to ensure safe concurrent access in GCD?
Hi Matthew, this works for simple objects like arrays and plain objects. However, the meaning of thread-safety and concurrent access start to diverge when dealing with complex objects that have a framework or architecture behind it, like Realm or Couchbase. Concurrency across threads isn’t the only problem there, but they have observable mechanisms that notify the system on updates, as well as caching mechanisms that were only built with one thread in mind. When trying to access the resources from other threads, things start to fall apart in their world beyond concurrent access.
So with barriers, you’d have the same problem as with Realm or Couchbase?
I forgot to mention that I’m asking in the context of CoreData