进一步理解GCD(Grand Central Dispatch),我们需要从其核心概念、核心组件、关键特性入手,再结合实际开发场景分析案例。GCD是苹果提供的基于C语言的多线程技术,其核心是通过「队列」管理「任务」,自动调度线程执行,无需手动管理线程生命周期,高效且易用。
一、GCD核心概念与组件
1. 核心目标
GCD的本质是任务调度系统:将任务(代码块)提交到队列,由系统自动分配线程执行任务,开发者只需关注「任务是什么」和「用什么队列执行」,无需关心线程的创建/销毁。
2. 核心组件:Dispatch Queue(调度队列)
队列是GCD的核心,用于存放任务,按「FIFO(先进先出)」原则执行任务。队列分为两类:
队列类型 特点 典型场景
3. 系统预定义队列
开发者无需手动创建所有队列,系统提供了3类常用队列:
• 主队列(Main Queue):
串行队列,仅在主线程执行,用于执行UI操作(如更新UI、刷新列表)。
获取方式:dispatch_get_main_queue()(iOS 10+ 可用 DispatchQueue.main)。
• 全局并发队列(Global Concurrent Queues):
系统提供的并发队列,有4个优先级(从高到低):
DISPATCH_QUEUE_PRIORITY_HIGH、DEFAULT、LOW、BACKGROUND(iOS 8+ 统一为 qos 质量服务等级,如 .userInitiated、.utility 等)。
获取方式:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)(或 DispatchQueue.global())。
• 自定义队列:
开发者可通过 dispatch_queue_create 创建串行/并发队列(指定 DISPATCH_QUEUE_SERIAL 或 DISPATCH_QUEUE_CONCURRENT)。
4. 任务执行方式:同步(sync)vs 异步(async)
任务通过「同步」或「异步」方式提交到队列,决定是否阻塞当前线程:
• 同步(sync):
提交任务后,当前线程会等待任务执行完成才继续往下走(阻塞当前线程)。
用法:dispatch_sync(queue, ^{ 任务代码 });
• 异步(async):
提交任务后,当前线程立即返回,不等待任务执行(非阻塞)。
用法:dispatch_async(queue, ^{ 任务代码 });
5. 关键组合:队列类型 + 执行方式
不同队列与同步/异步的组合,决定任务的执行线程和是否阻塞,这是GCD的核心难点:
死锁案例:在主线程调用主队列的同步执行,会导致死锁。
原因:主队列是串行队列,当前主线程正在执行任务A,同步提交的任务B需等待A完成;但A又在等待B完成(因为sync会阻塞A),形成循环等待。
// 主线程执行以下代码会立即死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务B"); // 永远不会执行
});
6. GCD高级特性
除了基础的队列和任务,GCD还提供了多个工具类API解决复杂场景:
• Dispatch Group(调度组):监听多个任务(可在不同队列)的完成状态,所有任务完成后执行回调。
核心API:dispatch_group_create()、dispatch_group_enter()/leave()(手动标记任务开始/结束)、dispatch_group_notify()(任务全部完成后回调)。
• Dispatch Semaphore(信号量):控制并发数(类似「线程锁」),通过信号量计数限制同时执行的任务数。
核心API:dispatch_semaphore_create(initialValue)(初始信号量)、dispatch_semaphore_wait()(信号量-1,若为0则阻塞)、dispatch_semaphore_signal()(信号量+1)。
• Dispatch Barrier(栅栏):在并发队列中,保证栅栏任务前的所有任务执行完后,再执行栅栏任务,且栅栏任务执行完后才执行后续任务(解决「读写安全」问题)。
核心API:dispatch_barrier_async(queue, ^{ 栅栏任务 });(仅对自定义并发队列有效,全局并发队列无效)。
• dispatch_once:保证代码块仅执行一次(线程安全),常用于单例模式。
用法:static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ 初始化代码 });
• dispatch_after:延迟指定时间后执行任务(注意:是「延迟提交任务」,非「延迟执行完成」)。
二、多线程实际案例
案例1:网络请求后更新UI(必须在主队列)
场景:子线程执行网络请求(耗时),请求完成后需更新UI(如刷新列表),而UI操作必须在主线程执行。
方案:子线程执行网络请求,完成后通过 dispatch_async 提交UI更新任务到主队列。
// 子线程执行网络请求(异步+并发队列)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1. 子线程执行耗时网络请求
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://api.example.com/data"]];
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
// 2. 网络请求完成后,提交UI更新任务到主队列
dispatch_async(dispatch_get_main_queue(), ^{
self.label.text = result[@"content"]; // UI操作必须在主队列
[self.tableView reloadData];
});
});
案例2:多任务并行执行后汇总结果(Group)
场景:需要同时请求3个接口(如用户信息、商品列表、消息通知),全部请求完成后汇总数据并刷新页面。
方案:用并发队列+Group,所有任务完成后通过 group_notify 回调汇总结果。
// 1. 创建调度组和并发队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 2. 定义存储结果的变量(需线程安全,这里简化用原子属性)
__block NSDictionary *userInfo = nil;
__block NSArray *goodsList = nil;
__block NSArray *notifications = nil;
// 3. 添加任务1到组(用户信息请求)
dispatch_group_enter(group); // 手动标记任务开始
dispatch_async(concurrentQueue, ^{
userInfo = [self fetchUserInfo]; // 耗时操作
dispatch_group_leave(group); // 任务完成,标记结束
});
// 4. 添加任务2到组(商品列表请求)
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
goodsList = [self fetchGoodsList];
dispatch_group_leave(group);
});
// 5. 添加任务3到组(消息通知请求)
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
notifications = [self fetchNotifications];
dispatch_group_leave(group);
});
// 6. 所有任务完成后,在主队列更新UI
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有请求完成:%@, %@, %@", userInfo, goodsList, notifications);
[self.refreshControl endRefreshing]; // 更新UI
});
案例3:控制并发数(防止线程爆炸)
场景:同时下载100个文件,若不限定并发数,系统可能创建大量线程导致内存飙升或CPU占用过高。需控制最多同时下载3个文件。
方案:用信号量(初始值=3),每个下载任务开始前「等待信号量」(-1),完成后「发送信号量」(+1),保证同时最多3个任务执行。
// 1. 创建信号量(初始值=3,最多3个并发)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
// 2. 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("com.example.download", DISPATCH_QUEUE_CONCURRENT);
// 3. 模拟100个下载任务
for (int i = 0; i < 100; i++) {
dispatch_async(queue, ^{
// 等待信号量(信号量-1,若为0则阻塞等待)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 执行下载任务(耗时操作)
NSLog(@"开始下载第%d个文件,当前线程:%@", i, [NSThread currentThread]);
[NSThread sleepForTimeInterval:2]; // 模拟下载耗时
NSLog(@"完成下载第%d个文件", i);
// 发送信号量(信号量+1,唤醒等待的任务)
dispatch_semaphore_signal(semaphore);
});
}
案例4:单例模式的线程安全实现
场景:单例需保证全局唯一实例,且在多线程同时初始化时不创建多个实例。
方案:用 dispatch_once,无论多少线程同时调用,代码块仅执行一次,绝对线程安全。
@implementation SingletonManager
+ (instancetype)sharedInstance {
static SingletonManager *instance = nil;
static dispatch_once_t onceToken; // 仅初始化一次的标记
dispatch_once(&onceToken, ^{ // 线程安全,保证代码块只执行一次
instance = [[SingletonManager alloc] init];
});
return instance;
}
@end
案例5:文件读写的线程安全(读写分离)
场景:多个线程可能同时读取文件,偶尔有线程写入文件。需保证「读操作可并行,写操作必须独占(写时不能读,读时不能写)」。
方案:用自定义并发队列+栅栏(barrier),读操作正常提交,写操作通过栅栏提交,保证写时独占队列。
// 1. 创建自定义并发队列(全局并发队列不支持栅栏)
dispatch_queue_t fileQueue = dispatch_queue_create("com.example.file", DISPATCH_QUEUE_CONCURRENT);
// 2. 读操作(可并行)
- (NSString *)readFile {
__block NSString *content = nil;
dispatch_sync(fileQueue, ^{ // 同步执行,等待结果返回
content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
});
return content;
}
// 3. 写操作(栅栏保证独占,写前所有读/写完成,写后再执行后续操作)
- (void)writeFile:(NSString *)content {
dispatch_barrier_async(fileQueue, ^{ // 栅栏任务,独占队列
[content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
});
}
效果:多个读操作可同时执行,写操作会等待所有之前的读/写完成后执行,且写操作执行时,后续的读/写会等待,保证文件读写安全。
三、总结
GCD的核心是「队列(管理任务顺序)」和「任务(执行逻辑)」,通过同步/异步执行方式控制线程行为,结合Group、Semaphore、Barrier等工具解决多线程协同问题。实际开发中需注意:
• UI操作必须在主队列执行,否则会崩溃;
• 避免主队列同步执行导致死锁;
• 控制并发数防止线程爆炸;
• 用合适的工具(如栅栏、信号量)保证线程安全。
掌握GCD可大幅简化多线程开发,提升程序性能和稳定性。