What is concurrency?
The system can perform multiple tasks simultaneously. By tasks, I mean code or instructions
.
Modern computer chips have multiple cores that allow developers to create and run various tasks on multiple cores. Even if your chip has one core operating system it will provide context switching mechanism by enabling it to execute multiple tasks concurrently.
Material about processes, threads
I will skip explaining concepts about processes and threads because it is a vast topic, and it will take a lot of time to explain it. I attached links to the material to help you understand it more deeply. https://youtu.be/4rLW7zg21gI?si=49hq8Wrbpmeev41k https://youtu.be/r2__Rw8vu1M?si=b7b257Qu4Bty7OxA I will focus on implementation.
The old
and modern
way of implementing concurrency
You can divide concurrency implementation into old
or unstructured
and modern
or structured
.
By old
, I mean GCD (Grand Central Dispatch).
By modern
, I mean async/await, actor, and Task.
In this article, I will talk about the old
way.
GCD helps you keep your distance from manually managing threads and avoid unnecessary complexity, and it does it by providing API.
One of these APIs is DispatchQueue.
DispatchQueue
By default, DispatchQueue
is serial; all work on this queue will be executed sequentially.
DispatchQueue
has access to the main
property and the global()
method.
The main
property returns the serial queue associated with the main thread of the current process.
The global()
method returns a concurrent queue specified by quality-of-service level.
public class func global(qos: DispatchQoS.QoSClass = .default) -> DispatchQueue
You can pass many parameters when you try to initialize a new queue.
public convenience init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)
Let’s talk about three of them (label, qos, attributes).
The first is label,
which is used mainly for debugging and identification.
let queue = DispatchQueue(label: "com.example.myqueue")
The second one is qos
(Quality Of Service) allows you to choose the priority in which you like to run your task. You can choose between background
, utility
, default
, userInitiated
, userIneractive
, and unspecified
priorities.
/// qos_class_t
public struct DispatchQoS : Equatable {
public let qosClass: DispatchQoS.QoSClass
public let relativePriority: Int
@available(macOS 10.10, iOS 8.0, *)
public static let background: DispatchQoS
@available(macOS 10.10, iOS 8.0, *)
public static let utility: DispatchQoS
@available(macOS 10.10, iOS 8.0, *)
public static let `default`: DispatchQoS
@available(macOS 10.10, iOS 8.0, *)
public static let userInitiated: DispatchQoS
@available(macOS 10.10, iOS 8.0, *)
public static let userInteractive: DispatchQoS
public static let unspecified: DispatchQoS
}
userIneractive
has the highest priority; it usually calls when you need to display UI almost immediately.
background
, on the other hand, has the lowest priority.
How to achieve concurrency with DispatchQueue API?
You can use a serial queue with the following:
sync
functionality allows you to wait until the block you passed finishes its work.
let serialQueue = DispatchQueue(label: "com.example.myqueue.serial")
serialQueue.sync {}
async
functionality will schedule
your work and be executed later in time.
let serialQueue = DispatchQueue(label: "com.example.myqueue.serial")
serialQueue.async {}
Or you can use a concurrent queue with similar methods but running your task in parallel.
let concurrentQueue = DispatchQueue(label: "com.example.myqueue.concurrent", attributes: .concurrent)
concurrentQueue.sync {}
concurrentQueue.async {}
The difference between serial queue and concurrent queue
The difference between a serial and concurrent queue is that you should not wait until the concurrent operation finishes work in the async
block.
let concurrentQueue = DispatchQueue(label: "com.example.myqueue.concurrent", attributes: .concurrent)
concurrentQueue.sync {
for i in 1...5 {
print("Task \(i) is running on Concurrent Queue")
sleep(1) // Simulate some work
}
}
concurrentQueue.sync {
for i in 6...10 {
print("Task \(i) is running on Concurrent Queue")
sleep(1) // Simulate some work
}
}
// prints
Task 1 is running on Concurrent Queue
Task 6 is running on Concurrent Queue
Task 2 is running on Concurrent Queue
Task 7 is running on Concurrent Queue
Task 3 is running on Concurrent Queue
Task 8 is running on Concurrent Queue
Task 4 is running on Concurrent Queue
Task 9 is running on Concurrent Queue
Task 5 is running on Concurrent Queue
Task 10 is running on Concurrent Queue
The serial queue executes tasks in order, and you should wait until the first async
block finishes its work to start the second block.
let serialQueue = DispatchQueue(label: "com.example.myqueue.serual")
serialQueue.sync {
for i in 1...5 {
print("Task \(i) is running on Serial Queue")
sleep(1) // Simulate some work
}
}
serialQueue.sync {
for i in 6...10 {
print("Task \(i) is running on Serial Queue")
sleep(1) // Simulate some work
}
}
// prints
Task 1 is running on Serial Queue
Task 2 is running on Serial Queue
Task 3 is running on Serial Queue
Task 4 is running on Serial Queue
Task 5 is running on Serial Queue
Task 6 is running on Serial Queue
Task 7 is running on Serial Queue
Task 8 is running on Serial Queue
Task 9 is running on Serial Queue
Task 10 is running on Serial Queue
When you try to use the sync
functionality, it behaves similarly on serial and concurrent queues by executing each task step by step and waiting till each block finishes its work.
let serialQueue = DispatchQueue(label: "com.example.myqueue.serial")
serialQueue.sync {
for i in 1...5 {
print("Task \(i) is running on Serial Queue")
sleep(1) // Simulate some work
}
}
serialQueue.sync {
for i in 6...10 {
print("Task \(i) is running on Serial Queue")
sleep(1) // Simulate some work
}
}
// prints
Task 1 is running on Serial Queue
Task 2 is running on Serial Queue
Task 3 is running on Serial Queue
Task 4 is running on Serial Queue
Task 5 is running on Serial Queue
Task 6 is running on Serial Queue
Task 7 is running on Serial Queue
Task 8 is running on Serial Queue
Task 9 is running on Serial Queue
Task 10 is running on Serial Queue
let concurrentQueue = DispatchQueue(label: "com.example.myqueue.concurrent", attributes: .concurrent)
concurrentQueue.sync {
for i in 1...5 {
print("Task \(i) is running on Concurrent Queue")
sleep(1) // Simulate some work
}
}
concurrentQueue.sync {
for i in 6...10 {
print("Task \(i) is running on Concurrent Queue")
sleep(1) // Simulate some work
}
}
// prints
Task 1 is running on Concurrent Queue
Task 2 is running on Concurrent Queue
Task 3 is running on Concurrent Queue
Task 4 is running on Concurrent Queue
Task 5 is running on Concurrent Queue
Task 6 is running on Concurrent Queue
Task 7 is running on Concurrent Queue
Task 8 is running on Concurrent Queue
Task 9 is running on Concurrent Queue
Task 10 is running on Concurrent Queue