06. Structured Concurrency

It will take about 5 minutes to finish reading this article.

1. Concept of Structured-Concurrency

Structured-Concurrency is a programming paradigm that aims to improve code clarity and efficiency by using structured parallel programming methods.

Suppose there is a function that does a lot of work on the CPU. We want to optimize this by spreading the work across the two cores; so now the function creates multiple new threads, does a portion of the work in each thread, and then lets the original thread wait for the new thread to finish. There is some relationship (dependency, priority, synchronization, etc.) between the work done by these threads, but the system does not know it. This requires developers to write high-quality code to ensure.

In other words, Structured-Concurrency can help us better leverage the synergy of multi-core processors and greatly improve the performance of our code, but it may not be that easy to implement it manually.

In fact, in the Objective-C era, we can also implement Structured-Concurrency through some code, such as the following code we use GCD to implement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#import <Foundation/Foundation.h>

//Define a task to be executed asynchronously
void performTask(int taskNumber) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Task %d starts executing", taskNumber);

// Simulate time-consuming operations
[NSThread sleepForTimeInterval:2.0];

NSLog(@"Task %d completed", taskNumber);
});
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
//Create a concurrent queue group
dispatch_group_t group = dispatch_group_create();

// Start multiple tasks and add them to the queue group
for (int i = 1; i <= 3; i++) {
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
performTask(i);
});
}

// Wait for all tasks in the queue group to be completed
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

//The main thread can continue to perform other work
NSLog(@"The main thread continues to perform other work");

// Sleep for a while to wait for the asynchronous task to complete
[NSThread sleepForTimeInterval:5.0];
}
return 0;
}

It is important to note that when using GCD in Objective-C, you need to carefully manage communication and synchronization between threads and tasks to avoid potential race conditions and data sharing issues.

GCD provides various scheduling queues, semaphores, and other tools to help you implement more complex concurrency logic. However, this approach is not as intuitive or type-safe as Swift’s Structured-Concurrency. If you need more advanced concurrency control and error handling, you may want to consider writing your application in Swift.

2. Structured-Concurrency in Swift

Structured concurrency is mainly implemented in Swift through async let and task group.

The following is an asynchronous common code for downloading images:

1
2
3
4
5
6
7
8
9
10
11
func fetchImage(from url: URL) async throws -> UIImage {
// Simulate asynchronous download of pictures
print("fetchImage----- begin: \(Thread.current)")
try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulate a download time of 1 second
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw NSError(domain: "ImageDownloadError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to download image"])
}
print("fetchImage----- end: \(Thread.current)")
return image
}

2.1 Implement Structured-Concurrency by ‘async let’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func downloadImages() async {
let imageURLs = [
URL(string: "https://example.com/image1.jpg")!,
URL(string: "https://example.com/image2.jpg")!,
URL(string: "https://example.com/image3.jpg")!
]
var images: [UIImage] = []
do {
print("fetchImage ----- begin")
async let fetchImage1 = fetchImage(from: imageURLs[0])
async let fetchImage2 = fetchImage(from: imageURLs[1])
async let fetchImage3 = fetchImage(from: imageURLs[2])
print("fetchImage ----- going")//这里先执行
let (image1, image2, image3) = await (try fetchImage1, try fetchImage2, try fetchImage3)
print("fetchImage ----- end")
images.append(contentsOf: [image1, image2, image3])
} catch {

}
}

Result:

1
2
3
4
5
6
7
8
9
fetchImage ----- begin
fetchImage ----- going
fetchImage----- begin: <NSThread: 0x7b100003fe40>{number = 6, name = (null)}
fetchImage----- begin: <NSThread: 0x7b100004a840>{number = 7, name = (null)}
fetchImage----- begin: <NSThread: 0x7b100004a840>{number = 7, name = (null)}
fetchImage----- end: <NSThread: 0x7b100004a840>{number = 7, name = (null)}
fetchImage----- end: <NSThread: 0x7b100005bd40>{number = 8, name = (null)}
fetchImage----- end: <NSThread: 0x7b100004a840>{number = 7, name = (null)}
fetchImage ----- end

Note: the above code in the assignment expression of the leftmost plus async let instead of await, called async let binding, it is because of this binding operation, so that the previous fetchImage1, fetchImage2, fetchImage3 are encapsulated in a subtask waiting to be completed, this is the This is the key point of Structured-Concurrency.

2.2 Implement Structured-Concurrency by ‘task group’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func downloadImages() async {
let imageURLs = [
URL(string: "https://picsum.photos/300")!,
URL(string: "https://picsum.photos/300")!,
URL(string: "https://picsum.photos/300")!
]

var images: [UIImage] = []

do {
print("fetchImage ----- begin")
try await withThrowingTaskGroup(of: UIImage.self) { group in

group.addTask {
try await fetchImage(from: imageURLs[0])
}
group.addTask {
try await fetchImage(from: imageURLs[1])
}
group.addTask {
try await fetchImage(from: imageURLs[2])
}
print("fetchImage ----- going")
for try await image in group {
images.append(image)
}
print("fetchImage ----- end")
}
} catch {

}
}

Result:

1
2
3
4
5
6
7
8
9
fetchImage ----- begin
fetchImage ----- going
fetchImage----- begin: <NSThread: 0x7b1000048300>{number = 5, name = (null)}
fetchImage----- begin: <NSThread: 0x7b1000046d00>{number = 7, name = (null)}
fetchImage----- begin: <NSThread: 0x7b1000048300>{number = 5, name = (null)}
fetchImage----- end: <NSThread: 0x7b100004a800>{number = 4, name = (null)}
fetchImage----- end: <NSThread: 0x7b1000048300>{number = 5, name = (null)}
fetchImage----- end: <NSThread: 0x7b1000062c80>{number = 8, name = (null)}
fetchImage ----- end

2.3 The difference between the two

First, not only the number of subtasks created by async let is static, but the order in which the subtasks are completed is also fixed, so it cannot obtain the results in the order in which the subtasks are completed. This determines that it is lighter and more intuitive. So if it can meet the needs, developers should give priority to async let.

Second, task groups can dynamically create subtasks and have more flexible operations, but they also require unified Closure encapsulation;

3. Unstructured tasks

In Apple’s 0304-structured-concurrency article, there is a mention of the unstructured concept. Compared to structured tasks (which mainly refer to parent-child tasks), then unstructured tasks mainly refer to some scenarios that run independently without relationship (parent-child). Unstructured tasks are mainly created by Task.init and Task.detached. These two ways to create the task, you can get the handle of the task, can be canceled, which is contrary to the structured concurrency. Specific sample code please see Task which chapter introduction.

Reference

[1] https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md
[2] https://juejin.cn/post/7084640887250092062
[3] https://en.wikipedia.org/wiki/Structured_concurrency
[4] http://chuquan.me/2023/03/11/structured-concurrency