Swift. Concurrency

Concurrency Issues

  • Race condition — a result depends on the order of concurrent task execution. The order may be different on each running
  • Priority inversion — priority of a particular task is unexpectedly increased or decreased
  • Deadlock — threads are blocked because their tasks are waiting for resources that are interlocked by them

Grand Central Dispatch (GCD)

let queue = DispatchQueue(label: "queue")
queue.async { print("queue") }
print("main")
// main
// queue
  • .userInteractive — is the highest QoS. It is used for tasks, which are triggered by a user and have to be completed first of all. The tasks shouldn’t take much time. E.g., a user is running a finger on the screen. There is a logic to handle the user action that takes some time for responding. That logic can be added as an async task to the queue with the highest priority to avoid producing UI lags. The main thread itself will proceed to listen to other user actions
  • .userInitiated — is less than the previous one, but still relatively high. The tasks should respond to user actions as soon as possible, but the user may wait for a while (several seconds). E.g., the user has pressed a button and is waiting for the result
  • .utility — is a middle QoS. The tasks aren’t triggered by a user, and they can take more than several seconds. E.g., a user goes on a screen, and it should download and show some image there. Until the image is shown, the user sees an activity indicator
  • .background — is the lowest QoS. The tasks aren’t related to UI and visualization at all. They can take even hours. E.g., synchronization with iCloud
  • .default — trying to take QoS from other sources. Otherwise, it sets the priority between .userInitiated and .utility
DispatchQueue.global(qos: .userInteractive).async { ... }
DispatchQueue.global(qos: .userInitiated).async { ... }
DispatchQueue.global(qos: .utility).async { ... }
DispatchQueue.global(qos: .background).async { ... }
DispatchQueue.global().async { ... }
DispatchQueue.main.async { ... }
// btw, again don't do DispatchQueue.main.sync { ... } ever!
// Experiment 1: main queue === main threadfunc currentQueueName() -> String? {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8)
}
print(currentQueueName()!)
DispatchQueue.main.async {
print(currentQueueName()!)
}
// com.apple.main-thread
// com.apple.main-thread
// Experiment 2: global dispatch queue - sync vs asyncfunc task(_ symbol: String) {
let priority = qos_class_self().rawValue
for i in 0...3 {
print("\(symbol), i: \(i), priority: \(priority)")
}
}
DispatchQueue.global().sync { task("😁") }
task("😈")
/*
😁, i: 0, priority: 33
😁, i: 1, priority: 33
😁, i: 2, priority: 33
😁, i: 3, priority: 33
😈, i: 0, priority: 33
😈, i: 1, priority: 33
😈, i: 2, priority: 33
😈, i: 3, priority: 33
*/
// vsDispatchQueue.global().async { task("😁") }
task("😈")
/*
😈, i: 0, priority: 33
😁, i: 0, priority: 25
😈, i: 1, priority: 33
😁, i: 1, priority: 25
😈, i: 2, priority: 33
😈, i: 3, priority: 33
😁, i: 2, priority: 25
😁, i: 3, priority: 25
*/
// Experiment 3: global dispatch queue - prioritiesDispatchQueue.main.async { task("😁") }
DispatchQueue.global(qos: .userInteractive).async { task("😈") }
DispatchQueue.global(qos: .userInitiated).async { task("👻") }
DispatchQueue.global(qos: .utility).async { task("🤖") }
DispatchQueue.global(qos: .background).async { task("😾") }
/*
👻, i: 0, priority: 25
😈, i: 0, priority: 33
🤖, i: 0, priority: 17
😾, i: 0, priority: 9
👻, i: 1, priority: 25
😈, i: 1, priority: 33
😈, i: 2, priority: 33
😈, i: 3, priority: 33
👻, i: 2, priority: 25
👻, i: 3, priority: 25
🤖, i: 1, priority: 17
🤖, i: 2, priority: 17
🤖, i: 3, priority: 17
😁, i: 0, priority: 33
😁, i: 1, priority: 33
😁, i: 2, priority: 33
😁, i: 3, priority: 33
😾, i: 1, priority: 9
😾, i: 2, priority: 9
😾, i: 3, priority: 9
*/
// Experiment 4: private dispatch queue by default is seriallet myQueue = DispatchQueue(label: "com.myApp.myQueue")
myQueue.async { task("😁") }
myQueue.async { task("😈") }
/*
😁, i: 0, priority: 25
😁, i: 1, priority: 25
😁, i: 2, priority: 25
😁, i: 3, priority: 25
😈, i: 0, priority: 25
😈, i: 1, priority: 25
😈, i: 2, priority: 25
😈, i: 3, priority: 25
*/
// Experiment 4: serial dispatch queue - asynclet myQueue = DispatchQueue(label: "com.myApp.myQueue") // serial
myQueue.async { task("😁") }
task("😈")
/*
😈, i: 0, priority: 33
😁, i: 0, priority: 25
😁, i: 1, priority: 25
😈, i: 1, priority: 33
😁, i: 2, priority: 25
😁, i: 3, priority: 25
😈, i: 2, priority: 33
😈, i: 3, priority: 33
*/
// Experiment 5: private dispatch queue - prioritieslet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .userInteractive)
let yourQueue = DispatchQueue(label: "com.myApp.yourQueue", qos: .background)
myQueue.async { task("😁") }
yourQueue.async { task("😈") }
/*
😈, i: 0, priority: 9
😁, i: 0, priority: 33
😁, i: 1, priority: 33
😁, i: 2, priority: 33
😁, i: 3, priority: 33
😈, i: 1, priority: 9
😈, i: 2, priority: 9
😈, i: 3, priority: 9
*/
// Experiment 6: concurrent private dispatch queuelet myQueue = DispatchQueue(label: "com.myApp.myQueue", attributes: [.concurrent])myQueue.async { task("😁") }
myQueue.async { task("😈") }
/*
😈, i: 0, priority: 25
😁, i: 0, priority: 25
😁, i: 1, priority: 25
😈, i: 1, priority: 25
😁, i: 2, priority: 25
😈, i: 2, priority: 25
😁, i: 3, priority: 25
😈, i: 3, priority: 25
*/
let myQueue = DispatchQueue(label: "com.myApp.myQueue", attributes: [.concurrent, .initiallyInactive])myQueue.async { task("😁") }
myQueue.async { task("😈") }
myQueue.activate()
// Experiment 7: DispatchWorkItem and .enforceQoSlet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .background,  attributes: [.concurrent])let workItem = DispatchWorkItem(qos: .userInteractive, flags: [.enforceQoS]) {
task("😁")
}
myQueue.async { task("😈") }
myQueue.async(execute: workItem)
/*
😈, i: 0, priority: 9
😁, i: 0, priority: 33
😁, i: 1, priority: 33
😁, i: 2, priority: 33
😁, i: 3, priority: 33
😈, i: 1, priority: 9
😈, i: 2, priority: 9
😈, i: 3, priority: 9
*/
class DebounceActionService {
private let debounceQueue = DispatchQueue(
label: UUID().uuidString,
attributes: [.concurrent]
)
private var debounceWorkItem: DispatchWorkItem?
public func asyncAfter(
delay: DispatchTimeInterval,
_ callback: @escaping () -> Void = {}
) {
debounceWorkItem?.cancel()
debounceWorkItem = DispatchWorkItem(block: callback)
debounceQueue.asyncAfter(
deadline: .now() + delay,
execute: debounceWorkItem!
)
}
}
// Experiment 8: DispatchWorkItem and notifylet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .background, attributes: [.concurrent]
let workItem = DispatchWorkItem(qos: .userInteractive, flags: [.enforceQoS]) { task("😁") }
myQueue.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("work item has been finished")
}
task("😈")
/*
😁, i: 0, priority: 33
😈, i: 0, priority: 33
😁, i: 1, priority: 33
😈, i: 1, priority: 33
😁, i: 2, priority: 33
😈, i: 2, priority: 33
😁, i: 3, priority: 33
😈, i: 3, priority: 33
work item has been finished
*/
// Experiment 9: DispatchGroup and notifylet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .background,  attributes: [.concurrent])let dispatchGroup = DispatchGroup()
myQueue.async(group: dispatchGroup) { task("😁") }
myQueue.async(group: dispatchGroup) { task("😈") }
dispatchGroup.notify(queue: DispatchQueue.main) { print("done") }
/*
😁, i: 0, priority: 9
😈, i: 0, priority: 9
😈, i: 1, priority: 9
😁, i: 1, priority: 9
😈, i: 2, priority: 9
😈, i: 3, priority: 9
😁, i: 2, priority: 9
😁, i: 3, priority: 9
done
*/
// Experiment 9: DispatchGroup and notify - enter and leavelet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .background,  attributes: [.concurrent])let dispatchGroup = DispatchGroup()
myQueue.async(group: dispatchGroup) {
dispatchGroup.enter()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
task("😁")
dispatchGroup.leave()
}
}
myQueue.async(group: dispatchGroup) {
dispatchGroup.enter()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
task("😈")
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
print("done")
}
/*
😈, i: 0, priority: 9
😁, i: 0, priority: 9
😈, i: 1, priority: 9
😁, i: 1, priority: 9
😈, i: 2, priority: 9
😁, i: 2, priority: 9
😈, i: 3, priority: 9
😁, i: 3, priority: 9
done
*/
// Experiment 9: .barrierlet myQueue = DispatchQueue(label: "com.myApp.myQueue", qos: .background,  attributes: [.concurrent])myQueue.async { task("😁") }
myQueue.async { task("😈") }
myQueue.async(flags: .barrier) { task("✋")}
myQueue.async { task("👻") }
/*
😈, i: 0, priority: 9
😁, i: 0, priority: 9
😁, i: 1, priority: 9
😁, i: 2, priority: 9
😈, i: 1, priority: 9
😁, i: 3, priority: 9
😈, i: 2, priority: 9
😈, i: 3, priority: 9
✋, i: 0, priority: 9
✋, i: 1, priority: 9
✋, i: 2, priority: 9
✋, i: 3, priority: 9

👻, i: 0, priority: 9
👻, i: 1, priority: 9
👻, i: 2, priority: 9
👻, i: 3, priority: 9
*/

Thread-Safe Variable Pattern

// Problemvar result = 1let queue = DispatchQueue(label: "com.queue", attributes: [.concurrent])queue.async {
for _ in 0..<3 {
// let's say that 2 is a quite expensive operation
result = result * 2
}
}
queue.async {
for _ in 0..<3 {
result = result * 2
}
}
sleep(1)
print(result)
// We want to make concurrent computing of result,
// because 2 is expensive for doing it in a serial manner

// But here is a race condition. The result can be 8, 16, 32...
// However the correct result is 64
// Solutionclass ThreadSafeResult {
// serial queue
private let queue = DispatchQueue(label: UUID().uuidString)
private var resultValue = 0
public init(_ value: Int) {
set(value: value)
}
public func set(value: Int) {
queue.async {
self.resultValue = value
}
}
public func add(value: Int) {
queue.async {
self.resultValue = self.resultValue * value
}
}
public var value: Int {
var resultValue = 0
queue.sync { resultValue = self.resultValue }
return resultValue
}
}
var result = ThreadSafeResult(1)let queue = DispatchQueue(label: "com.queue", attributes: [.concurrent])queue.async {
for _ in 0..<3 {
result.add(value: 2) // still concurrent computing of 2
}
}
queue.async {
for _ in 0..<3 {
result.add(value: 2) // still concurrent computing of 2
}
}
sleep(1)
print(result.value)
// 64, no race condition
  • Each write operation is async. There cannot be two or more write operations at the same time
  • Each read operation is sync. Any time when the operation is being executed, all write operations started before must be completed

Operation and OperationQueue

let operationQueue = OperationQueue()
operationQueue.addOperation { print("hello world") }
let someOperation = Operation()
someOperation.start()
let operationQueue = OperationQueue()
let anotherOperationQueue = OperationQueue(
let operation = Operation()
operationQueue.addOperation(operation)
anotherOperationQueue.addOperation(operation) // runtime error
// orlet operation = Operation()
operation.start()
operation.start() // runtime error
let blockOperation = BlockOperation {
sleep(1)
print("hello")
}
blockOperation.addExecutionBlock {
print("world")
}
let operationQueue = OperationQueue()
operationQueue.addOperation(blockOperation)
// world
// hello
class CustomOperation: Operation {
public var operationName = ""

override func main() {
print("hello from \(operationName)")
}
}
let operation = CustomOperation()
operation.operationName = "custom operation"
operation.start()
// hello from custom operation
operation.completionBlock = {
print("hello from completion block")
}

Async Operation Pattern

class AsyncOperation: Operation {

public enum State: String {
case isReady, isExecuting, isFinished
}
public var state: State = .isReady {
willSet {
willChangeValue(forKey: newValue.rawValue)
willChangeValue(forKey: state.rawValue)
}
didSet {
didChangeValue(forKey: oldValue.rawValue)
didChangeValue(forKey: state.rawValue)
}
}
override var isReady: Bool {
// super.isReady is used to preserve dependencies logic
return super.isReady && state == .isReady
}
override var isExecuting: Bool {
return state == .isExecuting
}
override var isFinished: Bool {
return state == .isFinished
}
override var isAsynchronous: Bool {
return true
}
override func start() {
if isCancelled {
state = .isFinished
return
}
main()
state = .isExecuting
}
override func cancel() {
super.cancel() // marks the operation isCancelled
state = .isFinished
}
}
// Custom async operation exampleclass DownloadImageOperation: AsyncOperation {

public var url: URL
public var image: UIImage?
init(url: URL) {
self.url = url
super.init()
}
override func main() {
if isCancelled {
return
}
downloadImage()
}
private func downloadImage() {
let task = createDownloadImageTask()
task.resume()
}
private func createDownloadImageTask() -> URLSessionDataTask {
return URLSession.shared.dataTask(with: url) { (data, _, _) in
if self.isCancelled {
return
}
guard let data = data else {
self.state = .isFinished
return
}
self.image = UIImage(data: data)
self.state = .isFinished
}
}
}
// ...let operation = DownloadImageOperation(url: url)
operation.qualityOfService = .utility
operation.completionBlock = {
if operation.isCancelled {
return
}
callback(operation.image, url)
}
let queue = OperationQueue()
queue.addOperation {
print("current thread is \(OperationQueue.current!.name!)")
print("main thread is \(OperationQueue.main.name!)")
}
// current thread is NSOperationQueue 0x7fb4ee6080a0
// main thread is NSOperationQueue Main Queue
queue.maxConcurrentOperationCount = 1 // makes the queue serial
queue.cancelAllOperations()
queue.waitUntilAllOperationsAreFinished() // blocks current thread
queue.qualityOfService = .utility // .background by default
queue.isSuspended = true
let operationA = BlockOperation { print("A") }
let operationB = BlockOperation { print("B") }
let operationC = BlockOperation { print("C") }
operationA.addDependency(operationB)
operationA.addDependency(operationC)
let queue = OperationQueue()
queue.addOperation(operationA)
queue.addOperation(operationB)
queue.addOperation(operationC)
// B
// C
// A
// somewhere inside in custom operationoverride func main() {
for operation in dependencies.filter({ $0 is SomeOperation }) {
print((operation as! SomeOperation).data)
}
}
queue.addBarrierBlock { print("barrier") }

GCD vs OperationQueue

Resources

--

--

Software Developer

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store