Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Last active March 6, 2023 13:01
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save brennanMKE/14c020a5da555b8802f8fabe189b08db to your computer and use it in GitHub Desktop.
Save brennanMKE/14c020a5da555b8802f8fabe189b08db to your computer and use it in GitHub Desktop.
Blocking with Semaphores and DispatchGroups in Swift

Blocking in Swift: DispatchSemaphore and DispatchGroup

Warning

This code is experimental as a study on how to block execution with attempts to break functionality. This code may work incidentally, not by proper execution of a thread-safe solution. This Gist will be updated later when more time can be put into better examples. In the meantime, the following Gist can be referenced.

https://gist.github.com/brennanMKE/e29e6d305cf03978b9f9e084296f2124

Asynchronous code does not operate as simply as code which runs on the main thread linearly. Concurrency can complicate code which must defer execution until the asynchronous work has completed. A common pattern is to include a closure parameter with a function which is executed once the asynchronous work has completed inside the function. This closure, often called a completion handler, requires calls to such functions to use nested closures to allow for deferring execution until the work has completed.

At times it is necessary to add asynchronous work to a code base which did not have it before requiring changes to all of the calls to code which would have to be changed to use completion handlers. This kind of change is disruptive. One way to avoid using a completion handler is to block execution after the asynchronous work has completed and before the end of the function. Outside the function it appears to be a regular, non-asynchronous function.

The snippets of code included here make a function that blocks execution before returning which also runs code asynchronously in a queue off the main thread. The examples include the following.

  1. DispatchSemaphore
  2. DispatchGroup
  3. DispatchWorkItem
  4. Async (non-blocking)

Each example can be copied into a Playground to explore how they function.

The blocking functions behave as though no asynchronous work is being done. No completion handler is needed which simplifies using the function. The example using DispatchSemaphore is the most complex as it requires calling wait and then signal at the end of the closure. Both the DispatchGroup and DispatchWorkItem examples will resume execution automatically when the closure returns.

Most of the time execution is running on the main thread. Any time the UI is updated it must be done on the main thread. With the Dispatch framework threads of execution are organized into queues, with one queue being the main queue. It is a serial queue which operates on the main thread.

Print statements include a count to show the current value of the count property stored with the class. The DispatchSemaphore and DispatchGroup examples print output which orders the "Work" and "Done" print statements in order while the 3rd example, which does not block execution, prints the statements in an unordered sequence. It demonstrates that execution continues without blocking as it shows the current value of the count when it prints the "Done" statement.


import PlaygroundSupport
import Foundation
class Worker {
private let queue = DispatchQueue.global(qos: .background)
private let serialQueue = DispatchQueue(label: "com.acme.serial")
public private(set) var count = 0
func incrementCount() {
serialQueue.sync {
count += 1
}
}
func work() {
queue.async { [weak self] in
guard let s = self else { return }
s.incrementCount()
print("🔵 Work [\(s.count)]")
}
}
}
let worker = Worker()
(0..<100).forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
PlaygroundPage.current.needsIndefiniteExecution = true
// Running the work function from multiple separate queues breaks the sequential ordering.
// The implementation is not working.
let worker = Worker()
let queue1 = DispatchQueue(label: "com.acme.queue1", attributes: .concurrent)
let queue2 = DispatchQueue(label: "com.acme.queue2", attributes: .concurrent)
let queue3 = DispatchQueue(label: "com.acme.queue3", attributes: .concurrent)
let range = 0..<5
queue1.async {
range.forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
}
queue2.async {
range.forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
}
queue3.async {
range.forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
}
import PlaygroundSupport
import Foundation
class Worker {
private let queue = DispatchQueue.global(qos: .background)
private let serialQueue = DispatchQueue(label: "com.acme.serial")
public private(set) var count = 0
func incrementCount() {
serialQueue.sync {
count += 1
}
}
func work() {
let group = DispatchGroup()
queue.async(group: group) { [weak self] in
guard let s = self else { return }
s.incrementCount()
print("🔵 Work [\(s.count)]")
}
group.wait()
}
}
let worker = Worker()
(0..<100).forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
PlaygroundPage.current.needsIndefiniteExecution = true
import PlaygroundSupport
import Foundation
class Worker {
private let queue = DispatchQueue.global(qos: .background)
private let serialQueue = DispatchQueue(label: "com.acme.serial")
public private(set) var count = 0
func incrementCount() {
serialQueue.sync {
count += 1
}
}
func work() {
let workItem = DispatchWorkItem() { [weak self] in
guard let s = self else { return }
s.incrementCount()
print("🔵 Work [\(s.count)]")
}
queue.async(execute: workItem)
workItem.wait()
}
}
let worker = Worker()
(0..<100).forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
PlaygroundPage.current.needsIndefiniteExecution = true
import PlaygroundSupport
import Foundation
class Worker {
private let queue = DispatchQueue.global(qos: .background)
private let serialQueue = DispatchQueue(label: "com.acme.serial")
public private(set) var count = 0
func incrementCount() {
serialQueue.sync {
count += 1
}
}
func work() {
let semaphore = DispatchSemaphore(value: 0)
queue.async { [weak self] in
guard let s = self else { return }
s.incrementCount()
print("🔵 Work [\(s.count)]")
semaphore.signal()
}
semaphore.wait()
}
}
let worker = Worker()
(0..<100).forEach { _ in
worker.work()
print("🔴 Done [\(worker.count)]")
}
PlaygroundPage.current.needsIndefiniteExecution = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment