I was reading this warning about releasing a lock across different threads:
The NSLock class uses POSIX threads to implement its locking behavior. When sending an unlock message to an NSLock object, you must be sure that message is sent from the same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
Do such issues not affect other concurrency primitives like NSRecursiveLock
and DispatchGroup
?
For example, here the wait happens on the main thread and leave on the background thread. And there is no issue of entering a deadlock due to missed leave notifications.
func parallelDownload(urls: [URL]) -> [Data] {
let session = URLSession.shared
var fetchedImages = [Data]()
let group = DispatchGroup()
for url in urls {
let request = URLRequest(url: URL)
let task = session.dataTask(with: request) { maybeData, maybeResponse, maybeError in
defer {
print("Leave in completion handler \(Thread.current): Is main thread? \(Thread.current.isMainThread)")
group.leave()
} // leave after block completes
guard maybeError == nil, let response = maybeResponse as? HTTPURLResponse, let data = maybeData else { return }
if !(200...299 ~= response.statusCode) { return }
fetchedImages.append(data)
}
group.enter()
task.resume()
}
// Waiting for last task to finish
print("Before wait \(Thread.current): Is main thread? \(Thread.current.isMainThread)")
group.wait()
print("After wait \(Thread.current): Is main thread? \(Thread.current.isMainThread)")
return fetchedImages
}
let urls = Array(repeating: "https://picsum.photos/200/300", count: 3).compactMap { URL(string: $0) }
let startTime = Date()
let images = parallelDownload(urls: urls)
let endTime = Date()
print("Download time \(endTime.timeIntervalSinceNow - startTime.timeIntervalSinceNow)")
Output:
Before wait <_NSMainThread: 0x600002494180>{number = 1, name = main}: Is main thread? true
Leave in completion handler <NSThread: 0x60000249a540>{number = 5, name = (null)}: Is main thread? false
Leave in completion handler <NSThread: 0x600002491000>{number = 4, name = (null)}: Is main thread? false
Leave in completion handler <NSThread: 0x600002496880>{number = 8, name = (null)}: Is main thread? false
After wait <_NSMainThread: 0x600002494180>{number = 1, name = main}: Is main thread? true
Download time 0.6723159551620483
Re NSRecursiveLock
, the docs say, “A lock that may be acquired multiple times by the same thread without causing a deadlock” (emphasis added).
You did not ask about it, but even with unfair locks, “you must call unlock()
from the same thread you use to call lock()
.”
However, DispatchGroup
has no such caveat. It is safe to use from different threads. If you are really interested in the details, see the source code.
FWIW, DispatchSemphore
(another anti-pattern in most situations) can also be used “across multiple execution contexts”.
By the way, using wait
of either DispatchSemaphore
or DispatchGroup
is an anti-pattern as it blocks a thread. This is especially problematic when invoked from the main thread. With DispatchGroup
, we can avoid this problem by using notify
with asynchronous pattern (e.g., a completion handler closure parameter) instead of wait
.
Or, better, nowadays we would use Swift concurrency’s async
-await
.