Swift Threading with Dispatchgroup

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.

  1. The main queue has a task executing (the one executing the snippet)
  2. Then you call asyncAfter which will enqueue another task (the closure containing group.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 specific DispatchQueue 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



Leave a reply



Submit