Using Dispatch to Avoid Race Conditions and Deadlock

My Setup: I have an Xcode workspace holding a software stack of 3 custom Swift frameworks (each with targets for both iOS and macOS), and a Swift iOS application using those frameworks. Other applications (both iOS and macOS) will be added to this stack in the future. My project currently has about 25,000 lines of Swift, no Objective-C, and is growing fast.

I am currently re-evaluating my concurrency model after watching a few WWDC talks from the GCD team, but both my old and new (theoretical) approaches seem to have fatal flaws.

My question is: What is the recommended practice for solving the problems in the approaches below in Swift on Apple platforms.

Approach 1: The existing approach in my project is that each class requiring synchronization (because shared resources will be accessed) maintains its own serial queue (with a nil target and .workItem autoreleaseFrequency) per instance and calls sync and async on it as necessary. When done correctly, this approach properly avoids race conditions, but it clutters the code. Additionally, based on several years worth of GCD talks at WWDC, it seems this approach could be prone to thread explosion.

Approach 2: At this year's WWDC, the GCD team recommended creating a small finite number of serial queue hierarchies per subsystem, given recent changes to how GCD allots threads under the hood. I then decided to create a new branch and try an approach where a QueueManager object distributes queues to those who need it. For serial queues, when an object wants a serial queue, it would ask the singleton QueueManager for one, and it would receive a new serial DispatchQueue with its target set to the shared one for that subsystem. This approach not only maintains the cluttered code (because sync and async are still everywhere), but it also introduces another inherent problem: deadlock. Suppose objects A and B are two classes in the same subsystem, they both need to be thread-safe, and A happens to be built on top of B. In this model, both A and B will be using the same serial queue at the root of their queue hierarchy because they are part of the same subsystem. So, if A calls one of B's methods from within a sync block, and that B method also calls sync, deadlock will occur. Solving this would seem to require that A have intimate knowledge of B's implementation detail, which also seems wrong.

Yes, designing around the need for synchronization is the ideal approach, but I'm not sure how this is attainable in practice. For example, take an object that dispatches a bunch of blocks to run in parallel and then has a completion method called after each block finishes. Since this could be called simultaneously, you would need synchronization for shared resources. What is the recommended approach to avoid the issues above?

Accepted Reply

>> The best way to eliminate deadlocks is to NOT use synchronous dispatch.


I absolutely agree. If you can get away without ever using the "sync" method, your implementation will be much safer.


>> UIKit is perfectly thread safe with no need for synchronization as long as you do everything UIKit related on its thread/dispatch queue.


This is not quite a complete picture. UIKit doesn't have "its" thread. Rather, most APIs in UIKit must be invoked on the app's main thread. This implicitly serializes such operations, and you can often use the main thread to extend the serialization to other things. However, it gets a bit more complicated, because in some cases (for example) UIKit invokes completions and other closures on a different thread, and it's up to you to "jump" to the main thread if necessary. Plus, if you see a need for a thread safety strategy in your app, you are presumably expecting to need to do some things (not calling UIKit) on background threads. Plus, depending on what your app is doing, you may wish to move some things off the main thread for performance reasons. So, it's complicated.


>> a QueueManager object distributes queues to those who need it


Frankly, a generic or general solution is never going to work. If it was possible generally, there would be a standard synchronization library you could use to make your app thread safe. You also have to be very careful not to think that specific tools (such as atomicity, synchronization and locks) automatically give you thread safety. They typically don't. You can use those things as part of a thread safety strategy, but the strategy needs to be designed and verified for each specific case. Further, the concept of thread safety per class isn't really meaningful, in general. Thread safety is a characteristic of an algorithm or execution flow, which is usually smeared across classes.


>> concerning the example of the object that dispatches blocks and executes completion handlers


For slightly higher level actions, you should also look into NSOperationQueue, which provides dependencies between operations (where operations are basically equivalent to closures).


>> is the way to solve this simply by providing solid documentation and design planning of B so that A knows B's completion takes place asynchronously?


The problem here is that it's not clear whether A expects some result from B and when. The way you've written this, you seem to expect that there are 5 sequential steps, represented here by your "print" statements, but you've explicitly and privately subverted the sequence in both of your methods. This is not exactly about documentation. It's about what the methods are for. If B's "completion" method is for doing steps 3 and 4, then dispatching those steps asynchronously is just a bug.


The "code smell" here is that both your methods are called "completion", when neither actually completes anything.

Replies

I think there's a really important point to make when dealing with GCD:

The best way to eliminate deadlocks is to NOT use synchronous dispatch.

If you use synchronized dispatch between systems, you create the possibility for deadlocks. The hard lesson in many cases is rewriting your code and your approach to work without synchronized dispatching. Generally that means that the blocks end up aquiring more state information than they would if you were using synchronous dispatching.


UIKit is perfectly thread safe with no need for synchronization as long as you do everything UIKit related on its thread/dispatch queue. That's the "implicit" synchronization standard: The task currently running on the queue has full run of the system that belongs to that queue, for the duration that it runs. And only the task running on the queue has access to that system.


Concerning "For example, take an object that dispatches a bunch of blocks to run in parallel and then has a completion method called after each block finishes. Since this could be called simultaneously, you would need synchronization for shared resources." There's a straight forward solution:

The first (and only) thing the completion method does is an asynchronous dispatch of the actual work you want done to a known queue.

Thanks. That's actually quite helpful. I had not heavily considered the approach of implicit synchronization.


But concerning the example of the object that dispatches blocks and executes completion handlers, consider the following code:


public class A {
    // Will be called from some concurrent queue
    public func completion() {
        subsystemQueue.async {
            print("1")
            print("2")
            // Unaware of B's implementation detail
            B().completion()
            print("5")
        }
    }
}
class B {
    // Suppose that in standard use cases, B will be used without A.
    // So completion() expects to be called on some concurrent queue
    func completion() {
        subsystemQueue.async {
            print("3")
            print("4")
        }
    }
}
A().completion() // 1, 2, 5, 3, 4


I understand why this happens, but is the way to solve this simply by providing solid documentation and design planning of B so that A knows B's completion takes place asynchronously? This is not a situation that I believe is currently happening in my code, but it doesn't seem far off.

>> The best way to eliminate deadlocks is to NOT use synchronous dispatch.


I absolutely agree. If you can get away without ever using the "sync" method, your implementation will be much safer.


>> UIKit is perfectly thread safe with no need for synchronization as long as you do everything UIKit related on its thread/dispatch queue.


This is not quite a complete picture. UIKit doesn't have "its" thread. Rather, most APIs in UIKit must be invoked on the app's main thread. This implicitly serializes such operations, and you can often use the main thread to extend the serialization to other things. However, it gets a bit more complicated, because in some cases (for example) UIKit invokes completions and other closures on a different thread, and it's up to you to "jump" to the main thread if necessary. Plus, if you see a need for a thread safety strategy in your app, you are presumably expecting to need to do some things (not calling UIKit) on background threads. Plus, depending on what your app is doing, you may wish to move some things off the main thread for performance reasons. So, it's complicated.


>> a QueueManager object distributes queues to those who need it


Frankly, a generic or general solution is never going to work. If it was possible generally, there would be a standard synchronization library you could use to make your app thread safe. You also have to be very careful not to think that specific tools (such as atomicity, synchronization and locks) automatically give you thread safety. They typically don't. You can use those things as part of a thread safety strategy, but the strategy needs to be designed and verified for each specific case. Further, the concept of thread safety per class isn't really meaningful, in general. Thread safety is a characteristic of an algorithm or execution flow, which is usually smeared across classes.


>> concerning the example of the object that dispatches blocks and executes completion handlers


For slightly higher level actions, you should also look into NSOperationQueue, which provides dependencies between operations (where operations are basically equivalent to closures).


>> is the way to solve this simply by providing solid documentation and design planning of B so that A knows B's completion takes place asynchronously?


The problem here is that it's not clear whether A expects some result from B and when. The way you've written this, you seem to expect that there are 5 sequential steps, represented here by your "print" statements, but you've explicitly and privately subverted the sequence in both of your methods. This is not exactly about documentation. It's about what the methods are for. If B's "completion" method is for doing steps 3 and 4, then dispatching those steps asynchronously is just a bug.


The "code smell" here is that both your methods are called "completion", when neither actually completes anything.