My notes on semaphores
- Semaphores has the ability to convert-a-callback based method into a returning-style-method. ✨
- Semaphores also has the ability to timeout 👌
- As we already know, unlimited work might lead to a deadlock.
- Here is how we can apply dispatch semaphore to limit a queue to 3 concurrent tasks:
Making async call a return method:
func makeAPICall() -> String {
var result: String = ""
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .background).async { // We need to put this on the main thread or else transition becomes glitchy
sleep(2)
result = "happy days"
semaphore.signal()
}
semaphore.wait() // (wallTimeout: .distantFuture) // timeout: DispatchTime.now() + 1
return result
}
Example (Timeout)
We can also specify a timeout for the wait function.
let sem = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
print("waiting for at least one signal for 1 second")
let res = sem.wait(timeout: DispatchTime.now() + 1) //wait for a signal
if(res == .timedOut){
print("wait timed out")
}else{
print("At least one signal has been received")
}
}
DispatchQueue.global().async {
sleep(3)
print("calling signal")
sem.signal() //send the signal
}
Limiting concurrent tasks:
As we already know, unlimited work might lead to a deadlock. Here is how we can apply dispatch semaphore to limit a queue to 3 concurrent tasks.
let concurrentTasks = 3
let queue = DispatchQueue(label: "Concurrent queue", attributes: .concurrent)
let sema = DispatchSemaphore(value: concurrentTasks)
print("began")
for _ in 0..<999 {
queue.async {
// Do work
sema.signal()
}
sema.wait()
}
print("ended") // Called when all 999 items where processed
Testing network connection:
let url = URL(string: urlString)
// 1
let semaphore = DispatchSemaphore(value: 0)
let _ = DownloadPhoto(url: url!) { _, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
// 2
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)
// 3
if semaphore.wait(timeout: timeout) == .timedOut {
XCTFail("\(urlString) timed out")
}
Array example:
extension Array where Element == DataSource {
func load() throws -> NoteCollection {
let semaphore = DispatchSemaphore(value: 0)
var loadedCollection: NoteCollection?
// We create a new queue to do our work on, since calling wait() on
// the semaphore will cause it to block the current queue
let loadingQueue = DispatchQueue.global()
loadingQueue.async {
// We extend 'load' to perform its work on a specific queue
self.load(onQueue: loadingQueue) { collection in
loadedCollection = collection
// Once we're done, we signal the semaphore to unblock its queue
semaphore.signal()
}
}
// Wait with a timeout of 5 seconds
semaphore.wait(timeout: .now() + 5)
guard let collection = loadedCollection else {
throw NoteLoadingError.timedOut
}
return collection
}
}
let dataSources: [DataSource] = [
localDataSource,
iCloudDataSource,
backendDataSource
]
do {
let collection = try dataSources.load()
output(collection)
} catch {
output(error)
}
Resources:
Todo:
- Make a wrapper for making methods that are async / sync. This seems harder than it sounds. Seems impossible actually.