引言

在iOS开发领域,多线程编程是优化应用性能与响应能力的关键技术。而Grand Central Dispatch(GCD)作为苹果推出的强大且高效的多线程解决方案,以其卓越的性能和易用性,成为开发者处理多线程任务的首选。本文将深入剖析GCD的底层原理、核心概念、常用API及其丰富多样的应用场景,并结合大量示例代码进行详细阐述。

一、GCD 基础概念

1.1 什么是 GCD

GCD 是基于 C 语言的多线程技术,它对线程管理进行了高度抽象和封装。开发者无需手动处理线程的创建、销毁和调度等复杂操作,只需将任务封装成 Block 并提交到合适的队列中,GCD 会自动完成后续的线程管理工作,大大简化了多线程编程的复杂度。

1.2 任务与队列

  • 任务:指的是要执行的具体工作,在 GCD 中使用 Block 来表示。Block 是一种闭包,可以捕获上下文变量,方便进行代码的封装和传递。
  • 队列:是用于存放任务的容器,根据任务执行的方式不同,可分为串行队列和并发队列。

1.3 同步与异步执行

  • 同步执行:任务会在当前线程中执行,并且会阻塞当前线程,直到任务执行完成后,才会继续执行后续的代码。
  • 异步执行:会开启新的线程(如果有必要)来执行任务,不会阻塞当前线程,当前线程会继续执行后续的代码。

二、GCD 的核心组件

2.1 队列类型

2.1.1 串行队列

每次只能执行一个任务,只有当前一个任务执行完成后,才会开始执行下一个任务。可以通过 dispatch_queue_create 函数创建自定义的串行队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建自定义串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

// 异步执行任务
dispatch_async(serialQueue, ^{
NSLog(@"Task 1 in serial queue");
sleep(1); // 模拟耗时操作
});

dispatch_async(serialQueue, ^{
NSLog(@"Task 2 in serial queue");
sleep(1); // 模拟耗时操作
});

2.1.2 并发队列

可以同时执行多个任务,任务的开始时间由 GCD 调度决定,但完成顺序不确定。可以通过 dispatch_queue_create 函数创建自定义的并发队列,也可以使用系统提供的全局并发队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建自定义并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

// 异步执行任务
dispatch_async(concurrentQueue, ^{
NSLog(@"Task 1 in concurrent queue");
sleep(1); // 模拟耗时操作
});

dispatch_async(concurrentQueue, ^{
NSLog(@"Task 2 in concurrent queue");
sleep(1); // 模拟耗时操作
});

// 获取全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
NSLog(@"Task in global concurrent queue");
sleep(1); // 模拟耗时操作
});

2.1.3 主队列

是一个特殊的串行队列,专门用于在主线程上执行任务,通常用于更新 UI。可以通过 dispatch_get_main_queue 函数获取主队列。

1
2
3
4
5
6
7
// 异步执行任务到主队列
dispatch_async(dispatch_get_main_queue(), ^{
// 更新 UI
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)];
label.text = @"Hello, GCD!";
[self.view addSubview:label];
});

2.2 任务执行方式

2.2.1 同步执行(dispatch_sync

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
NSLog(@"Synchronous task");
sleep(1); // 模拟耗时操作
});
NSLog(@"This line will be executed after the synchronous task");

2.2.2 异步执行(dispatch_async

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"Asynchronous task");
sleep(1); // 模拟耗时操作
});
NSLog(@"This line will be executed immediately without waiting for the asynchronous task");

2.3 延迟执行(dispatch_after

用于在指定的时间后执行任务,通常用于实现定时任务。

1
2
3
4
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
NSLog(@"Delayed task executed after 2 seconds");
});

2.4 一次性执行(dispatch_once

确保代码块只执行一次,常用于实现单例模式,保证在多线程环境下单例对象的唯一性。

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"This code block will be executed only once");
});

2.5 分组执行(dispatch_group

可以将多个任务分组,当组内所有任务完成后,执行特定的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{
NSLog(@"Task 1 in group");
sleep(1); // 模拟耗时操作
});

dispatch_group_async(group, queue, ^{
NSLog(@"Task 2 in group");
sleep(1); // 模拟耗时操作
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"All tasks in the group are completed");
});

2.6 栅栏函数(dispatch_barrier_asyncdispatch_barrier_sync

用于在并发队列中实现同步操作,确保在栅栏函数之前的任务完成后,才会执行栅栏函数及其后面的任务。

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
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{
NSLog(@"Task 1 before barrier");
sleep(1); // 模拟耗时操作
});

dispatch_async(concurrentQueue, ^{
NSLog(@"Task 2 before barrier");
sleep(1); // 模拟耗时操作
});

dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"Barrier task");
sleep(1); // 模拟耗时操作
});

dispatch_async(concurrentQueue, ^{
NSLog(@"Task 1 after barrier");
sleep(1); // 模拟耗时操作
});

dispatch_async(concurrentQueue, ^{
NSLog(@"Task 2 after barrier");
sleep(1); // 模拟耗时操作
});

2.7 信号量(dispatch_semaphore

用于控制并发任务的数量,通过信号量的计数来限制同时执行的任务数。

1
2
3
4
5
6
7
8
9
10
11
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 允许同时执行 2 个任务
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"Task %d is executing", i);
sleep(1); // 模拟耗时操作
dispatch_semaphore_signal(semaphore);
});
}

三、GCD 的底层原理

3.1 线程池管理

GCD 维护了一个高效的线程池,用于管理线程的创建、复用和销毁。线程池的大小会根据系统的负载和任务的需求动态调整。当有任务提交到队列时,GCD 会从线程池中获取可用的线程来执行任务。如果线程池中没有可用的线程,且当前线程数量未达到系统限制,GCD 会创建新的线程;如果达到系统限制,则任务会等待直到有线程可用。

线程池的优势在于避免了频繁创建和销毁线程带来的性能开销。线程的创建和销毁是一个相对昂贵的操作,会消耗大量的系统资源和时间。通过线程池,GCD 可以复用已有的线程,提高了线程的利用率,从而提升了系统的整体性能。

3.2 任务调度算法

GCD 使用了复杂而高效的任务调度算法,根据任务的优先级、队列类型和线程池的状态等因素,决定任务的执行顺序和线程分配。

3.2.1 优先级调度

GCD 中的任务可以有不同的优先级,全局并发队列提供了四个不同优先级的队列:高、默认、低和后台。高优先级的任务会优先被调度执行,但需要注意的是,过高的优先级可能会导致低优先级的任务长时间得不到执行,产生饥饿现象。

3.2.2 队列类型影响

串行队列中的任务会按照提交的顺序依次执行,而并发队列中的任务会根据系统资源和调度算法并行执行。在并发队列中,GCD 会尽量充分利用多核 CPU 的优势,同时执行多个任务,提高系统的并发处理能力。

3.2.3 线程池状态

线程池的状态也是任务调度的重要考虑因素。如果线程池中有空闲线程,GCD 会优先将任务分配给这些空闲线程;如果线程池已满,任务会被放入队列中等待。

3.3 锁机制

在多线程环境下,为了保证线程安全,GCD 使用了多种锁机制。例如,在访问共享资源时,会使用互斥锁来避免数据竞争。互斥锁确保同一时间只有一个线程可以访问共享资源,从而防止多个线程同时修改数据导致的数据不一致问题。

另外,GCD 还使用了读写锁来优化对共享资源的读写操作。读写锁允许多个线程同时进行读操作,但在进行写操作时,会独占资源,防止其他线程进行读写操作。这种机制可以提高并发性能,尤其是在读操作频繁的场景下。

3.4 工作队列与内核

GCD 的工作队列与内核紧密协作,通过内核的调度机制来实现任务的高效执行。当任务提交到队列后,GCD 会将任务封装成一个工作项,并将其添加到工作队列中。内核会根据系统的负载和调度策略,从工作队列中取出工作项并分配给合适的线程执行。

GCD 与内核的协作还体现在线程的创建和销毁上。当需要创建新线程时,GCD 会向内核请求资源,内核会分配相应的线程资源;当线程不再使用时,GCD 会通知内核释放这些资源。

四、GCD 的应用场景

4.1 网络请求

在开发中,网络请求通常是一个耗时的操作,如果在主线程中进行网络请求,会导致界面卡顿,影响用户体验。因此,我们通常会在子线程中进行网络请求,完成后回到主线程更新 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 网络请求
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"Network request error: %@", error.localizedDescription);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
// 更新 UI
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 30)];
label.text = responseString;
[self.view addSubview:label];
});
}
}];
[task resume];
});

4.2 图片处理

图片处理,如解码、缩放、裁剪等操作,通常比较耗时。我们可以在子线程中进行这些操作,完成后回到主线程显示图片,避免阻塞主线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 加载图片
UIImage *image = [UIImage imageNamed:@"largeImage"];
// 图片处理
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, 0.0);
[image drawInRect:CGRectMake(0, 0, 100, 100)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

dispatch_async(dispatch_get_main_queue(), ^{
// 显示图片
UIImageView *imageView = [[UIImageView alloc] initWithImage:scaledImage];
imageView.frame = CGRectMake(0, 0, 100, 100);
[self.view addSubview:imageView];
});
});

4.3 数据处理

在处理大量数据时,如文件读写、数据库操作等,会消耗大量的时间和系统资源。我们可以将这些操作放在子线程中进行,避免阻塞主线程,提高应用的响应性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 数据处理
NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/largeFile.txt"];
NSString *fileContent = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
// 处理文件内容
NSArray *lines = [fileContent componentsSeparatedByString:@"\n"];
NSLog(@"Number of lines: %lu", (unsigned long)lines.count);

dispatch_async(dispatch_get_main_queue(), ^{
// 更新 UI 或显示结果
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 30)];
label.text = [NSString stringWithFormat:@"Number of lines: %lu", (unsigned long)lines.count];
[self.view addSubview:label];
});
});

4.4 后台任务

在某些情况下,我们需要在应用进入后台后继续执行一些任务,如上传文件、下载数据等。GCD 可以帮助我们实现后台任务的管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)applicationDidEnterBackground:(UIApplication *)application {
__block UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
}];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 后台任务
for (int i = 0; i < 10; i++) {
NSLog(@"Background task: %d", i);
sleep(1);
}

[application endBackgroundTask:backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
});
}

4.5 预加载数据

为了提高应用的响应速度,我们可以在应用启动或用户操作之前,提前加载一些数据。使用 GCD 可以在后台线程中进行数据的预加载,避免阻塞主线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 预加载数据
NSArray *data = [self loadDataFromServer];
// 将数据存储到本地或缓存中
[self cacheData:data];
});

return YES;
}

- (NSArray *)loadDataFromServer {
// 模拟从服务器加载数据
sleep(3);
return @[@"Data 1", @"Data 2", @"Data 3"];
}

- (void)cacheData:(NSArray *)data {
// 缓存数据
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:data forKey:@"preloadedData"];
[defaults synchronize];
}

4.6 并发计算

在进行复杂的计算任务时,如数学计算、图像处理算法等,可以利用 GCD 的并发队列将任务分解为多个子任务并行执行,提高计算效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentCalculation", DISPATCH_QUEUE_CONCURRENT);
__block int result = 0;

dispatch_group_t group = dispatch_group_create();

// 分解任务
for (int i = 0; i < 10; i++) {
dispatch_group_async(group, concurrentQueue, ^{
int partialResult = [self calculatePartialResult:i];
@synchronized(self) {
result += partialResult;
}
});
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"Final result: %d", result);
});

- (int)calculatePartialResult:(int)index {
// 模拟复杂计算
sleep(1);
return index * 2;
}

五、GCD 的注意事项

5.1 死锁问题

在使用 dispatch_sync 时要特别注意死锁问题。例如,在主队列中同步执行主队列的任务会导致死锁,因为主线程会等待同步任务执行完成,而同步任务又在等待主线程空闲,从而形成死锁。

1
2
3
4
// 以下代码会导致死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"This code will cause a deadlock");
});

5.2 线程安全

在多线程环境下访问共享资源时,要确保线程安全。可以使用信号量、锁等机制来避免数据竞争和不一致问题。例如,在多个线程同时访问和修改一个数组时,需要使用锁来保证同一时间只有一个线程可以进行操作。

5.3 内存管理

在使用 GCD 时,要注意 Block 的内存管理,避免循环引用。Block 会捕获其上下文中的变量,如果不小心,可能会导致对象之间的循环引用,从而造成内存泄漏。可以使用弱引用(如 __weak)来避免循环引用。

六、总结

GCD 是 iOS 开发中强大且高效的多线程技术,通过合理运用 GCD 的队列、任务执行方式和各种高级特性,开发者可以轻松实现多线程编程,提升应用的性能和响应能力。同时,在使用 GCD 时要注意死锁、线程安全和内存管理等问题,以确保代码的健壮性。希望本文能帮助你深入理解 GCD 的底层原理和应用场景,在实际开发中更好地运用 GCD 技术。

参考资料