Quantcast
Channel: Call Me Fishmeal.
Viewing all articles
Browse latest Browse all 32

Pimp My Code, Book 2: Grand Central Dispatch's Achilles Heel.

$
0
0

(Aka: “GCD’s Big Fat Greek Flaw”)

I mostly like Grand Central Dispatch (“GCD”) in theory and in practice — among its many roles it adds a relatively simple, clean, and uniform way of dispatching arbitrary blocks of code on various threads, which is the hairiest area of programming I know. (Plus GCD’s ‘blocks’ were my real introduction to closures, and they are indeed very powerful and wonderful to use.)

GCD also adds it own versions of functions that already exist in different forms, and I support this. For example, there are other ways to do semaphores on Darwin/POSIX (c.f.: ‘sem_wait()’ in sys/semaphore.h or ‘semaphore_wait()’ in mach/semaphore.h) but having them in GCD puts all the functionality I might use to write multi-threaded apps in one place, and provides a consistency in their interfaces. And there’s a lot to be said for only having to learn (and later re-learn) one framework.

As an extra bonus, in Swift the interfaces to GCD and blocks got retooled to not be so…C-ish, so using GCD is generally quite beautiful now, and beauty means readability means better code yadda yadda yadda you’ve heard me give this speech a billion times.

So, yay for GCD!


Under iOS and macOS there is a thread that’s more important than the others. The “main thread” is where Views must be created and destroyed, (non-CoreAnimation) animations must be started, drawRect(_:) is called, and setNeedsDisplay() needs to be called.

Most AppKit and UIKit objects need to be created, destroyed, and modified only in the main thread. And the vast, vast majority of apps written for macOS and iOS use either AppKit or UIKit.

So, if your app is doing some work in any non-main thread (“background threads”), and you want to somehow show the user the results from that work, you’re going to have to use AppKit/UIKit to do do so. Which means you have to figure out a way to call a method or change a property on an AppKit/UIKit object from a background thread, which I just said you can’t do.

Luckily, this isn’t actually very hard — it’s GCD’s main job, after all, to allow a programmer to dispatch blocks to various queues (which map roughly to threads if you squint, kinda), and the main queue (which runs only on the main thread) is one of those queues you can use. (I’m a poet and I’m not aware.)

So you might have a function like this, running in a background thread:

Contrived example of changing interface widget from background thread [Swift]
@IBOutlet var label: NSTextField!

func channelDidReceiveData(data: Data) { // background thread runningCount += data.count let threadSafeCount = runningCount DispatchQueue.main.async { self.label.stringValue = "Got bytes: \(threadSafeCount)" } }

private var runningCount = 0

This code works as you might expect; there’s no gotcha here. Programmers can and do use asynchronous blocks to update their UI elements in real world code.

But what if a background thread needs something from the main thread before it can continue? A common case for this is when opening a network connection (which usually happens in a background thread so the main interface doesn’t freeze up for however long it takes to connect) and the connection requires the user to enter a password. Here’s what code might look like that handles this.

Example of background thread needing input before continuing [Swift]
@IBOutlet var passwordField: NSSecureTextField!

func channelRequiresPassword() -> String { // background thread var password: String = "" DispatchQueue.main.sync { promptForPasswordModally() password = passwordField.stringValue } return password }

Great! This code also works. So far, so good.

BUT! Now comes the flaw. What happens if you’re writing a function that can be called from a background thread or the main thread? (Hint: BEDLAM!)

If you call the above code from the main thread, it will…deadlock forever!

To perform a block with sync() GCD just adds the block to the list of blocks to execute on the target queue, and then pauses the current queue until the target queue has processed the block. But if you enqueue a block synchronously on the current queue, then the target queue is the current queue, so the current queue is paused waiting for a the destination queue (itself) to be proceed, which it never will because it’s paused.

Is this a frustrating default behavior? Yes. Does it lead to tons of bugs? Yes. Did I just talk to an Apple engineer about this two days ago and literally they came back to me a day later and said, “I just encountered a bug in code caused by that problem you described with GCD?” Yes.

So why would GCD work this way? I suspect it has something to do with guaranteeing blocks are executed in a certain order. That is to say, if you add a block to the current queue using async() and then you add one the current queue using sync(), the latter block would execute first if this were fixed, which could cause arguably confusion.

Except, I can’t imagine cases where I as a programmer encode one block async() and one sync() and have any reasonable expectations of ordering. Also, Apple could just document this corner case so people don’t rely on it when adding to their own queue. Also also, I’d like to point out, HANGS the app right now, so it’s not like the behavior I propose would break any existing apps, unless there are apps out there that rely on hanging.


You might argue, “Well, a responsible programmer should know what thread they’re calling from!” This isn’t practical or desirable. For instance, if you’re writing a networking framework, you really want the customer to be able to call it from any thread. It’s not acceptable to say, “Hey, if you happen to do a read() call from the main thread your app will hang! Good luck LOLZ!”

Also, Apple documents that some delegate methods can be called from any thread. So when you write, say, an SCNSceneRendererDelegate, you won’t know if your renderer(_:, willRenderScene:, atTime:) will be called on the main thread or not.


I don’t know much about the internals of GCD so I can’t speak with authority, but it seems like this could be solved with a couple of minor changes to sync(): figure out if the destination queue is the current queue, and if so just execute the submitted block immediately and return. This wouldn’t even be a source or binary-breaking change, because, again, the current behavior is HANG the app.

And, in fact, this is the workaround third-party programmers have made for the last several years. If you do a Google search for ‘dispatch_get_current_queue’ you’ll see a bunch of developers complaining about that call disappearing because they were using it for this hack.

Here’s code showing how this works, from Robbie Hanson’s very popular GCDAsyncSocket project on GitHub (modified slightly by me to show how things used to work before iOS 6):

Modified real-world code sample (before deprecation of get_current) [Obj-C]
- (id)delegate { __block id result;

if (dispatch_get_current_queue() == socketQueue) { result = delegate; } else { dispatch_sync(socketQueue, ^{ result = delegate; }); }

return result; }

This worked before iOS 6, but the code got even more complicated when Apple deprecated dispatch_get_current_queue(), so developers had to get uglier and more creative. To check if the program is on the main thread one can still use AppKit’s “NSThread.isMainThread” (in Obj-C — yes they added class properties, yay!) or “Thread.isMainThread” (Swift), but it feels kinda strange and ugly having to reach across frameworks like this instead of using a GCD-native approach. GCDAsyncSocket went native with a lot more code but no dependencies on AppKit:

Actual real-world code sample (after deprecation of get_current) [Obj-C]
// ... in init code IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;

void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); //

- (id)delegate { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegate; } else { __block id result;

dispatch_sync(socketQueue, ^{ result = delegate; }); return result; } }

This code works, but you must do this if/else dance for EVERY function where you didn’t know what queue it’s in. Also, this is just plain ugly and hard to read. If dispatch_sync() worked as it should, this would look like:

Same code sample, in a better world [Swift]
func delegate() -> AnyObject? { var result: AnyObject? DispatchQueue.main.sync { result = delegate } return result }

Ahhh. Noice.

Also notice that in the two real-world examples the code inside the ‘if’ clause is exact the same as the code inside the dispatch_sync(): in this case it’s just the single line ‘result = delegate’. What if you have some complicated code for the main thread?

You’re going to either duplicate all those lines or you get to make an ugly single-shot block to work around this. Here’s some actual shipping code from one of my projects:

Real-world code sample (multiple lines to execute synchronously) [Obj-C]
dispatch_block_t clearPasswordAndRetry = ^{ self.serverAccountPassword = nil; [self processOperationOnSecureChannelForProtocolVersion:protocolVersion currentQueue:outerQueue operation:operation handler:channelHandler finally:finallyBlock]; };

if (NSThread.isMainThread) clearPasswordAndRetry(); else dispatch_sync(dispatch_get_main_queue(), clearPasswordAndRetry);

Hopefully you’re as horrified by this mess as I am. This is the very model of spaghetti code. Last week I ported this file from macOS 10.8 to 10.12 and honestly I still couldn’t come up with a good way to re-architect it. I’m bending over backwards to interact with the main thread in multiple places in this codebase and I’m not sure if I’m on the main thread or not and it’s a nightmare.

If you’re writing any body of code that can be called from any thread then many of its functions are going to have to do this ugly spaghetti dance just to sync up with the main thread.

In an ideal world we wouldn’t have to worry about what thread we were on – all threads would be equal and we’d only worry about what priority level our current thread was and stuff like that. But we’re not in that world — we’re in a world where the interface layer only can be touched from the main thread, and we need for our threading packages to understand that limitation and help us work around it, not make our code uglier.


Viewing all articles
Browse latest Browse all 32

Latest Images

Trending Articles





Latest Images