How do I use DispatchGroup in background thread?
Given what you've described, I probably wouldn't use a dispatch group at all here. I'd just chain the methods:
@objc func doWorkFunctions() {
DispatchQueue.global(qos: .background).async {
self.firstFunction {
self.secondFunction {
DispatchQueue.main.async {
print("All tasks completed")
}
}
}
}
But assuming you have a good reason for a group here, what you need to do is to use .notify
to synchronize them. .notify
says "when the group is empty, submit this block to this queue."
@objc func doWorkFunctions(){
let queue = DispatchQueue.global(qos: .background)
taskGroup.enter()
queue.async {
self.firstFunction {
self.taskGroup.leave()
}
}
taskGroup.notify(queue: queue) {
self.taskGroup.enter()
self.secondFunction {
self.taskGroup.leave()
}
self.taskGroup.notify(queue: .main) {
print("All tasks completed")
}
}
}
(You probably don't need taskGroup
to be an instance property here. You could make it a local variable and have fewer self.
references required. Each block has a reference to the group, so it will live until all the blocks have completed.)
Why does DispatchGroup interfere with main queue?
I am going to assume that you are running these snippets on the main thread, as this is most probably the case from the issue description.
Dispatch queues are basically task queues, so a queue has some tasks enqueued. Let's see what is on the main queue when you are executing the snippet generating a deadlock.
- The main queue has a task executing (the one executing the snippet)
- Then you call
asyncAfter
which will enqueue another task (the closure containinggroup.leave()
) on the main queue, after the specified deadline.
Now the task being executed (1.) is being blocked by the call to group.wait()
, and it's going to block the whole main queue until it finishes execution. This means the enqueued task (2.) will have to wait until the first one finishes. You can see here that the 2 tasks will block each other:
- the first one (1.) will wait until the second one (2.) releases the dispatch group
- the second one (2.) will wait until the first one (1.) finishes execution so it can be scheduled
For question number 2, using a global queue (or literally any other queue other than the one our current code is being executed on - in this example the main queue), will not block the asyncAfter
task (obviously, because it's being scheduled on another queue which is not blocked, thus it gets the chance to be executed).
This is true for serial dispatch queues (the main queue being a serial queue as well). Serial dispatch queues will execute their tasks serially, that is only one at a time.
On the other hand, for a concurrent dispatch queue, this scenario won't result in a deadlock, because the asyncAfter
task won't be blocked by the waiting task. That's because concurrent dispatch queues don't wait for the task being executed to be finished to schedule the next enqueued task.
This would even be a good exercise to try to run this scenario on a serial queue, and then on a concurrent queue to observe the differences
How to make a common resource thread safe when using dispatch group?
You asked:
But I am not clear about the thread-safety of my user object. Since it will be updated in three different callbacks, would thread safety be an issue here?
There are no thread-safety issues in your code snippet because Alamofire calls its completion handlers on the main thread. They do that to help mitigate multithreading concerns. There is no need for any DispatchQueue.main.async
in this case. As the Alamofire documentation says:
Closures passed to response handlers are executed on the
.main
queue by default, but a specificDispatchQueue
can be passed on which to execute the closure.
So unless you did something unusual (such as overriding the default .main
queue with some concurrent DispatchQueue
), Alamofire will run its completion handlers on the main thread, mitigating the thread-safety concerns.
If you were using a different API that didn’t call its completion handlers on the main thread (e.g. URLSession.shared
calls its completion handlers on a background queue), then there might be concerns, but not with Alamofire. (And even URLSession
uses a serial background queue, so there wouldn’t be issues using your pattern, where you are updating a local variable.)
Bottom line, as long as you’re not mutating/accessing a variable from multiple threads at the same time, threads-safety concerns are largely mitigated.
Swift DispatchQueue: Serial or parallel
Your second example will run them sequentially. It is doing a single dispatch, running them one after another. Your first example will run them in parallel, dispatching each to a different worker thread. Unfortunately, though, neither is using the dispatch group correctly.
Regarding the dispatch group, you should define it before your loop and enter
before you call async
. But the manual calling of enter
and leave
is only needed if you're calling an asynchronous process from within the async
call. But given that decompress
is likely a synchronous process, you can just supply the group to async
, and it will take care of everything for you:
let group = DispatchGroup()
for file in compressedFiles {
DispatchQueue.global(qos: .userInteractive).async(group: group) {
file.decompress()
}
}
group.notify(queue: .main) {
// all done
}
But rather than worrying about the dispatch group logic, there is a deeper problem in the parallel example. Specifically, it suffers from thread explosion, where it can exceed the number of available cores on your CPU. Worse, if you had a lot of files to decompress, you can even exceed the limited number of worker threads that GCD has in its pool. And when that happens, it can prevent anything else from running on GCD for that QoS. Instead, you want to run it in parallel, but you want to constrain it to a reasonable degree of concurrency, while still enjoying parallelism, to avoid exhausting resources for other tasks.
If you want it to run in parallel, but also avoid thread explosion, one would often reach for concurrentPerform
. That offers maximum parallelism supported by the CPU, but preventing problems that can result from thread explosion:
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.concurrentPerform(iterations: compressedFiles.count) { index in
compressedFiles[index].decompress()
}
DispatchQueue.main.async {
// all done
}
}
That will constrain the parallelism to the maximum permitted by the cores on your device. It also eliminates the need for the dispatch group.
Alternatively, if you want to enjoy parallelism, but with a lower degree of concurrency (e.g. to leave some cores available for other tasks, to minimize peak memory usage, etc.), you might use use operation queues and maxConcurrentOperationCount
:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4 // a max of 4 decompress tasks at a time
let completion = BlockOperation {
// all done
}
for file in compressedFiles {
let operation = BlockOperation {
file.decompress()
}
completion.addDependency(operation)
queue.addOperation(operation)
}
OperationQueue.main.addOperation(completion)
Or at matt points out, in iOS 13 (or macOS 10.15) and later, you can do:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
for file in compressedFiles {
queue.addOperation {
file.decompress()
}
}
queue.addBarrierBlock {
DispatchQueue.main.async {
// all done
}
}
Asynchronous Function with DispatchGroup
The call to async(group:os:flags:execute:)
API that you’re using is used when performing tasks that are, themselves, synchronous. E.g. let's say you were going to process a bunch of images in some slow, synchronous API, but wanted to run this in parallel, on background threads to avoid blocking the main thread. Then you’d use async(group:execute:)
API:
let group = DispatchGroup()
for image in images {
DispatchQueue.global().async(group: group) {
self.process(image)
}
}
group.notify(queue: .main) {
// all done processing the images
}
In this case, though, the individual, block-based UIView
animation API calls run asynchronously. So you can’t use this async(group:execute:)
pattern. You have to manually enter
before starting each asynchronous process and then leave
inside the respective completion closures, matching them up one-to-one:
animationGroup.enter()
UIView.animate(withDuration: 1, animations: {
box.center = CGPoint(x: 150, y: 150) // Move box to lower right corner
}, completion: { _ in
UIView.animate(withDuration: 2, animations: {
box.transform = CGAffineTransform(rotationAngle: .pi / 4) // Rotate box 45 degrees
}, completion: { _ in
animationGroup.leave()
})
})
animationGroup.enter()
UIView.animate(withDuration: 4, animations: {
view.backgroundColor = .blue // Change background color to blue
}, completion: { _ in
animationGroup.leave()
})
animationGroup.notify(queue: .main) {
print("Animations Completed!")
}
Does DispatchGroup wait forever?
Yes, it will wait forever.
The group
is captured, so it will not get released. And, obviously, you will end up lock that particular worker thread. As worker threads are quite limited, this is a bigger concern than the amount of memory consumed by the DispatchGroup
.
If you want it to timeout, use wait(timeout:)
.
Related Topics
Core Data: Rename Attribute Without Having Issues with Users and Their Current Data
Xctestcase Optional Instance Variable
Using State Variables as Inputs to a Func in Swiftui
Is There a Technical Reason to Use Swift's Caseless Enum Instead of Real Cases
iOS 12 Errors: Appears to Be from a Different Nsmanagedobjectmodel Than This Context'S
Cannot Preview in This File -- Message Send Failure
Swiftui Scroll/List Scrolling Events
Non Exhaustive List When Handling Errors Inside a Class Function in Swift
Anyobject Try Cast to Equatable
How to Call Non-Escaping Closure Inside a Local Closure
Apple Vision - Barcode Detection Doesn't Work for Barcodes with Different Colours
Swift: Visual Glitches When Presenting a Main and Alternative (Login/Onboarding) Flow
Multidimensional Dictionaries Possible in Swift
From Any Utf-16 Offset, Find the Corresponding String.Index That Lies on a Character Boundary