1

I have been learning about threading and DispatchQueues a lot recently and have come to a big question. I have heard many times that GCD makes no guarantees about which thread a given block of work may be executed on. Most of the time, this is a helpful level abstraction. However, I'm hitting a bug which I still don't know the cause of, but which caused me to realize what seems to me to be a potential pitfall of this aspect of GCD.

Example:

let queue1 = DispatchQueue(label: "one")
let queue2 = DispatchQueue(label: "two")
queue1.sync {
    let importantValue1 = "importantValue1"
    let importantValue2 = queue2.sync {
        return "importantValue2"
    }
    print("did important work, got values", importantValue1, importantValue2)
}

My question is, am I at least guaranteed that my queues will not execute on the same thread? From what I've seen it doesn't seem like I have this guarantee. But, without it, am I not in constant jeopardy of deadlock? In the example above, what would happen if both queues execute on Thread 7? Won't the call to queue2.sync cause the app to crash?

jeremyabannister
  • 3,796
  • 3
  • 16
  • 25
  • Like Rob N said, when you use `sync`, as an optimization GCD will just run it on the current thread if it can. If you use `async` you’ll see it will likely end up on an another thread. Bottom line, no, you are not in constant jeopardy of deadlock. With the pattern you have here, it most certainly won’t deadlock, but it will be clever about using the best available worker thread, avoiding context switches if it can. – Rob Feb 18 '19 at 23:38

2 Answers2

1

Tasks from background queues never prevent other tasks from background queues from running, so while you can get deadlocks, to queues executing on the same thread will not cause it.

On the other hand, running your code synchronous on another queue is rather pointless.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
  • The reason I'm doing that is because in my real code I'm using Core Data and need to execute interactions with the object context on the object context's queue, but I want to do it synchronously – jeremyabannister Feb 18 '19 at 21:13
  • The alternative to this with Core Data is to re-fetch objects by their NSManagedObjectID from the current queue's context. Whether you'd want to do this or not will depend on whether you're already operating with multiple NSManagedObjectContexts and which proved to be better for performance—but just noting here this is possible. :) – Duncan Babbage Feb 21 '20 at 02:27
  • 1
    "running your code synchronous on another queue is rather pointless." This statement is false. There are plenty of reasons why you would want to do this. For example, it can be used to avoid race conditions when accessing shared resources. – Peter Schorn Sep 26 '20 at 01:41
1

In most cases, I would expect these two blocks to run on the same queue. In fact, let's see:

import Foundation

let queue1 = DispatchQueue(label: "one")
let queue2 = DispatchQueue(label: "two")
queue1.sync {
    let importantValue1 = "importantValue1"
    print(Thread.current)  // Print the current queue
    let importantValue2: String = queue2.sync {
        print(Thread.current) // Print the current queue
        return "importantValue2"
    }
    print("did important work, got values", importantValue1, importantValue2)
}

<NSThread: 0x6000023b2900>{number = 1, name = main}
<NSThread: 0x6000023b2900>{number = 1, name = main}
did important work, got values importantValue1 importantValue2

Yeah, in my example both run on the main thread, as you'd generally want them to. There's often no reason to impose the large cost of switching threads when calling .sync. The current thread can't do anything until the block completes, so it might as well run that block on the current thread as long as there isn't any restriction against that (for example, blocks submitted to the main queue must run on the main thread).

You're correct that it's possible for this to generate deadlocks if you're not careful, but that's not because of the thread that is used. The deadlock would be inherent in using .sync in a circular way. Whatever underlying thread is used, the queue still has to dispatch blocks in a certain order, and that's what most often creates deadlocks.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Your sentence "The current thread can't do anything until the block completes, so it might as well run that block on the current thread..." is where I get confused. When a thread is blocked due to `sync`, it is still able to do work from other dispatch queues, just not the one that blocked it? – jeremyabannister Feb 18 '19 at 21:03
  • 1
    Not from random other queues, but it can run things from the queue it's waiting on (the one it called `sync` on). But if you're thinking deeply about what thread things are *running* on (rather than what queue blocks are *dispatched* to), then you're probably going to get yourself in trouble. Think about the queues and their dependency graph. Leave the threads to the OS. Threads are about parallelism. Queues are about concurrency, and you usually should be focused on concurrency. – Rob Napier Feb 18 '19 at 21:27