3

I'm trying to understand how DispatchQueues really work and I'd like to know if this is safe to assume that DispatchQueues have their own managing thread ? For example let's take the serial queue. In my understanding - since it is serial the new task can be started only after the end of the current one, so "someone" have to dequeue the new task from the queue and submit it for execution! So, basically it seems that the queue have to leave within its own thread which will dispatch the tasks that are stored in the queue. Is this correct or I misunderstood something?

Andre Liberty
  • 707
  • 1
  • 9
  • 17
  • 2
    Gcd is baked into the kernel. So it is controlled outside anything you can think of as a thread. It is controlled in the fundamental underlying world within which threads live and move and have their being. – matt Feb 06 '21 at 20:58
  • @matt So, if I got your point correctly then the next task is dequeued and scheduled directly by the kernel. In simple words, by some essence(basically Grand Central Dispatch) that is the part of the kernel and that manages all queues and able to directly dequeue the next task once the current one have finished and submit it for execution ? Did I got it correctly ? – Andre Liberty Feb 06 '21 at 21:06
  • 1
    That is how I understand it. That is why GCD is so efficient. You are looking for a sort of "God" being whose job is to make GCD queues/threads do what they do. That is the kernel; it is in charge of threads, so it cannot be said, itself, to be _in_ a thread. It just "is", all the time. – matt Feb 06 '21 at 21:41
  • 1
    You can take a look at the source: https://github.com/apple/swift-corelibs-libdispatch. Swift GCD wraps the c level libdispatch which calls functions on the kernel. You should separate threads from queues in your mind, a queue refers to the order of the work, and GCD executes the work appropriately on threads. If more threads are needed then the system will create more. There are a lot of comments in the source code that explains some of this. You can have all your serial/concurrent queues use the same thread, but GCD manages creating more threads when available and as needed to be efficient. – Michael Ozeryansky Feb 06 '21 at 23:02
  • This notion of a “managing thread” doesn’t really make sense (nor matter). The salient issue for application developers is that GCD has pools of worker threads, and as we dispatch code to a queue, GCD will control (a) when and (b) on which worker thread the code will run. (This is how GCD achieves its performance gains over naive `Thread` implementations: It minimizes the costly overhead of spinning of new threads and simply draws upon its existing pool of worker threads.) Are you really asking how GCD works internally? Or do you just want to know on which thread your dispatched code runs? – Rob Feb 09 '21 at 17:37

2 Answers2

5

No, you should not assume that DispatchQueues have their own managed threads, and it doesn't have to execute all tasks on the same thread. It only guarantees that the next task is picked up after the previous one completes:

Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app's main thread, the system makes no guarantees about which thread it uses to execute a task.

(source)

Practically it is very possible, that the same thread will run several or all sequential tasks from the same sequential queue - provided they run close to each other (in time). I will speculate that this is not by a pure coincidence, but by optimization (avoids context switches). But it's not a guarantee.

In fact you can do this little experiment:

let serialQueue = DispatchQueue(label: "my.serialqueue")
var incr: Int = 0

DispatchQueue.concurrentPerform(iterations: 5) { iteration in

    // Rundomize time of access to serial queue
    sleep(UInt32.random(in: 1...30))
    
    // Schedule execution on serial queue
    serialQueue.async {
        incr += 1
        print("\(iteration) \(Date().timeIntervalSince1970) incremented \(incr) on \(Thread.current)")
    }
}

You will see something like this:

3 1612651601.6909518 incremented 1 on <NSThread: 0x600000fa0d40>{number = 7, name = (null)}
4 1612651611.689259 incremented 2 on <NSThread: 0x600000faf280>{number = 9, name = (null)}
0 1612651612.68934 incremented 3 on <NSThread: 0x600000fb4bc0>{number = 3, name = (null)}
2 1612651617.690246 incremented 4 on <NSThread: 0x600000fb4bc0>{number = 3, name = (null)}
1 1612651622.690335 incremented 5 on <NSThread: 0x600000faf280>{number = 9, name = (null)}

Iterations start concurrently, but we make them sleep for a random time, so that they access a serial queue at different times. The result is that it's not very likely that the same thread picks up every task, although task execution is perfectly sequential.

Now if you remove a sleep on top, causing all iterations request access to sequential queue at the same time, you will most likely see that all tasks will run on the same thread, which I think is optimization, not coincidence:

4 1612651665.3658218 incremented 1 on <NSThread: 0x600003c94880>{number = 6, name = (null)}
3 1612651665.366118 incremented 2 on <NSThread: 0x600003c94880>{number = 6, name = (null)}
2 1612651665.366222 incremented 3 on <NSThread: 0x600003c94880>{number = 6, name = (null)}
0 1612651665.384039 incremented 4 on <NSThread: 0x600003c94880>{number = 6, name = (null)}
1 1612651665.3841062 incremented 5 on <NSThread: 0x600003c94880>{number = 6, name = (null)}

Here's an excellent read on topic of iOS Concurrency "Underlying Truth"

timbre timbre
  • 12,648
  • 10
  • 46
  • 77
1

It's true that GCD on macOS includes some direct support from kernel. All the code is open source and can be viewed at: https://opensource.apple.com/source/libdispatch/

However, a Linux implementation of the same Dispatch APIs is available as part of the Swift open source project (swift-corelibs-libdispatch). This implementation does not use any special kernel support, but is just implemented using pthreads. From that project's readme:

libdispatch on Darwin [macOS] is a combination of logic in the xnu kernel alongside the user-space Library. The kernel has the most information available to balance workload across the entire system. As a first step, however, we believe it is useful to bring up the basic functionality of the library using user-space pthread primitives on Linux. Eventually, a Linux kernel module could be developed to support more informed thread scheduling.

To address your question specifically — it's not correct that each queue has a managing thread. A queue is more like a data structure that assists in the management of threads — an abstraction that makes it so you as the developer don't have to think about thread details.

How threads are created and used is up to the system and can vary depending on what you do with your queues. For example, using .sync() on a queue often just acquires a lock and executes the block on the calling thread, even if the queue is a concurrent queue. You can see this by setting a breakpoint and observing which thread you're running on:

let syncQueue = DispatchQueue(label: "syncQueue", attributes: .concurrent)

print("Before sync")
syncQueue.sync {
  print("On queue")
}
print("After sync")

sync execution on concurrent queue

On the other hand, multiple async tasks can run at once on a concurrent queue, backed by multiple threads. In practice, the global queues seem to use up to 64 threads at once (the code prints "used 64 threads"):

var threads: Set<Thread> = []
let threadQueue = DispatchQueue(label: "threads set")

let group = DispatchGroup()

for _ in 0..<100 {
  group.enter()
  DispatchQueue.global(qos: .default).async {
    sleep(2)
    let thisThread = Thread.current
    threadQueue.sync { _ = threads.insert(thisThread) }
    group.leave()
  }
}

group.wait()  // wait for all async() blocks to finish

print("Used \(threads.count) threads")

64 threads with one concurrent queue

But without the sleep(), the tasks finish quickly, and the system doesn't need to use so many threads (the program prints "Used 20 threads", or 30, or some lower number).

The main queue is another serial queue, which runs as part of your application lifecycle, or can be run manually with dispatchMain().

jtbandes
  • 115,675
  • 35
  • 233
  • 266