Reputation: 757
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?
Upvotes: 4
Views: 2894
Reputation: 118761
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")
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")
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()
.
Upvotes: 2
Reputation: 14005
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"
Upvotes: 5