学会在Objective-C中使用GCD进行多线程编程
一、GCD基础概念
-
什么是GCD GCD(Grand Central Dispatch)是苹果公司开发的一个多核编程的解决方案,它基于队列(queue)和任务(task)的概念。在Objective-C开发中,GCD提供了一种简单而高效的方式来进行多线程编程,大大简化了传统多线程编程中诸如线程管理、同步等复杂的操作。
-
队列(Queue)
- 类型:
- 串行队列(Serial Queue):也称为私有队列,同一时间只有一个任务在执行。当一个任务完成后,队列才会从等待队列中取出下一个任务执行。例如,我们可以使用
dispatch_queue_create
函数创建一个串行队列:
- 串行队列(Serial Queue):也称为私有队列,同一时间只有一个任务在执行。当一个任务完成后,队列才会从等待队列中取出下一个任务执行。例如,我们可以使用
- 类型:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", NULL);
这里"com.example.serialQueue"
是队列的标识符,用于唯一标识这个队列,NULL
表示默认属性。
- 并发队列(Concurrent Queue):可以同时执行多个任务,但任务仍然是按照添加到队列的顺序开始执行。系统提供了全局并发队列,可以通过dispatch_get_global_queue
函数获取不同优先级的全局并发队列。例如,获取默认优先级的全局并发队列:
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
DISPATCH_QUEUE_PRIORITY_DEFAULT
是优先级参数,0
是一个保留参数,目前必须设置为0
。
- 主队列(Main Queue):这是一个特殊的串行队列,它与应用程序的主线程相关联。在主队列中执行的任务会在主线程上运行。可以通过dispatch_get_main_queue
函数获取主队列:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
- 队列的优先级:
- 全局并发队列有不同的优先级,除了
DISPATCH_QUEUE_PRIORITY_DEFAULT
(默认优先级)外,还有DISPATCH_QUEUE_PRIORITY_HIGH
(高优先级)、DISPATCH_QUEUE_PRIORITY_LOW
(低优先级)和DISPATCH_QUEUE_PRIORITY_BACKGROUND
(后台优先级)。高优先级的任务会优先于低优先级的任务在队列中执行,但这并不意味着高优先级任务一定会先完成,因为实际执行还受到系统资源等多种因素的影响。
- 全局并发队列有不同的优先级,除了
- 任务(Task)
- 类型:
- 同步任务(Synchronous Task):使用
dispatch_sync
函数提交任务到队列,该函数会阻塞当前线程,直到任务执行完毕。例如,向串行队列提交一个同步任务:
- 同步任务(Synchronous Task):使用
- 类型:
dispatch_sync(serialQueue, ^{
// 任务代码
NSLog(@"Synchronous task in serial queue");
});
这里,当前线程会等待这个任务在serialQueue
中执行完后才会继续执行后续代码。
- 异步任务(Asynchronous Task):通过dispatch_async
函数提交任务到队列,该函数不会阻塞当前线程,会立即返回,任务会在队列中排队等待执行。例如,向并发队列提交一个异步任务:
dispatch_async(globalConcurrentQueue, ^{
// 任务代码
NSLog(@"Asynchronous task in concurrent queue");
});
提交这个任务后,当前线程会继续执行后续代码,而这个任务会在globalConcurrentQueue
中按照队列规则执行。
二、GCD在Objective-C中的应用场景
- 耗时操作与主线程分离
在iOS应用开发中,主线程主要负责处理用户界面的更新和响应。如果在主线程中执行耗时操作,如网络请求、文件读写等,会导致界面卡顿,影响用户体验。通过GCD可以将这些耗时操作放到后台队列中执行,主线程则可以继续处理界面相关的任务。
- 示例:网络请求 假设我们要进行一个简单的网络请求,可以使用AFNetworking框架(这里简化示例,假设AFNetworking已正确导入和配置):
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"https://example.com/api/data" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// 网络请求成功,更新UI需要回到主队列
dispatch_async(dispatch_get_main_queue(), ^{
// 假设这里有一个UILabel来显示数据
self.dataLabel.text = [NSString stringWithFormat:@"%@", responseObject];
});
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Error: %@", error);
}];
});
在这个示例中,网络请求在全局并发队列中异步执行,当请求成功后,由于更新UI必须在主线程进行,所以使用dispatch_async
将更新UI的代码提交到主队列执行。
- 提高性能的并发处理
对于一些可以并行处理的任务,如图片处理、数据计算等,可以使用并发队列来充分利用多核处理器的性能。
- 示例:图片处理 假设我们有一组图片需要进行模糊处理,我们可以将每张图片的模糊处理任务提交到并发队列中:
NSArray *imagePaths = @[@"path/to/image1.jpg", @"path/to/image2.jpg", @"path/to/image3.jpg"];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSString *path in imagePaths) {
dispatch_async(concurrentQueue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:path];
// 这里假设我们有一个自定义的模糊处理函数
UIImage *blurredImage = [self blurImage:image];
// 处理完后,比如保存图片
NSData *imageData = UIImageJPEGRepresentation(blurredImage, 1.0);
NSString *newPath = [NSString stringWithFormat:@"%@_blurred.jpg", [path stringByDeletingPathExtension]];
[imageData writeToFile:newPath atomically:YES];
});
}
在这个例子中,每张图片的模糊处理和保存操作在并发队列中异步执行,这样可以同时处理多张图片,提高整体处理效率。
- 任务依赖与顺序执行
有时候我们需要任务按照一定的顺序执行,或者一个任务依赖于另一个任务的完成。可以通过串行队列或者使用
dispatch_group
来实现。- 示例:任务依赖 假设我们有三个任务,任务B依赖于任务A的完成,任务C依赖于任务B的完成。我们可以使用串行队列来实现:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.dependentTasksQueue", NULL);
dispatch_async(serialQueue, ^{
// 任务A
NSLog(@"Task A is running");
// 模拟一些耗时操作
sleep(2);
NSLog(@"Task A is done");
});
dispatch_async(serialQueue, ^{
// 任务B
NSLog(@"Task B is running");
// 模拟一些耗时操作
sleep(2);
NSLog(@"Task B is done");
});
dispatch_async(serialQueue, ^{
// 任务C
NSLog(@"Task C is running");
// 模拟一些耗时操作
sleep(2);
NSLog(@"Task C is done");
});
在这个串行队列中,任务A、B、C会依次执行,因为串行队列同一时间只有一个任务在执行,并且任务是按照添加的顺序执行的。
三、GCD的高级特性
- Dispatch Group
dispatch_group
用于管理一组任务,当这组任务全部完成后可以执行一个回调。它可以用来实现任务的同步,比如等待一组异步任务全部完成后再进行下一步操作。- 示例:等待多个网络请求完成 假设我们有多个网络请求,需要在所有请求完成后进行数据合并和处理:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSArray *urls = @[@"https://example.com/api/data1", @"https://example.com/api/data2", @"https://example.com/api/data3"];
NSMutableArray *allData = [NSMutableArray array];
for (NSString *urlString in urls) {
dispatch_group_async(group, globalQueue, ^{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:urlString parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[allData addObject:responseObject];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Error: %@", error);
}];
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 所有网络请求完成,进行数据合并和处理
NSLog(@"All requests are done, data: %@", allData);
// 这里可以进行数据合并等操作
});
在这个示例中,dispatch_group_async
将每个网络请求任务添加到group
中,dispatch_group_notify
设置当group
中的所有任务完成后,在主队列中执行数据合并和处理的代码。
- Dispatch Semaphore
dispatch_semaphore
是一种信号量机制,用于控制对共享资源的访问。它可以通过设置信号量的初始值来限制同时访问资源的线程数量。- 示例:限制同时下载的文件数量 假设我们要从服务器下载多个文件,但为了避免网络拥堵,我们希望同时只能有3个文件在下载:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
NSArray *fileUrls = @[@"https://example.com/file1.zip", @"https://example.com/file2.zip", @"https://example.com/file3.zip", @"https://example.com/file4.zip", @"https://example.com/file5.zip"];
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSString *urlString in fileUrls) {
dispatch_async(globalQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 下载文件
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *fileName = [url lastPathComponent];
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [documentsPath stringByAppendingPathComponent:fileName];
[data writeToFile:filePath atomically:YES];
NSLog(@"Downloaded %@", fileName);
dispatch_semaphore_signal(semaphore);
});
}
在这个示例中,dispatch_semaphore_create(3)
创建了一个初始值为3的信号量,意味着最多可以有3个任务同时执行下载操作。dispatch_semaphore_wait
会等待信号量的值大于0,然后将信号量的值减1,开始下载文件。下载完成后,dispatch_semaphore_signal
将信号量的值加1,允许下一个任务获取信号量进行下载。
- Dispatch Barrier
dispatch_barrier
用于在并发队列中插入一个任务,这个任务会等待队列中在它之前的任务全部执行完,然后再单独执行,执行完后队列恢复正常并发执行。它常用于读写操作场景,比如在并发队列中进行数据读取操作可以并发执行,但数据写入操作需要在其他读取操作完成后单独执行,以保证数据一致性。- 示例:读写操作 假设我们有一个自定义的数据存储类,使用一个并发队列进行读写操作:
@interface DataStore : NSObject
@property (nonatomic, strong) NSMutableArray *dataArray;
@property (nonatomic, strong) dispatch_queue_t dataQueue;
@end
@implementation DataStore
- (instancetype)init {
self = [super init];
if (self) {
self.dataArray = [NSMutableArray array];
self.dataQueue = dispatch_queue_create("com.example.dataStoreQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)readData {
dispatch_async(self.dataQueue, ^{
NSLog(@"Reading data: %@", self.dataArray);
});
}
- (void)writeData:(id)newData {
dispatch_barrier_async(self.dataQueue, ^{
[self.dataArray addObject:newData];
NSLog(@"Written data: %@", newData);
});
}
@end
在这个示例中,readData
方法使用dispatch_async
向并发队列提交读取任务,这些读取任务可以并发执行。而writeData:
方法使用dispatch_barrier_async
提交写入任务,这个写入任务会等待队列中之前的读取任务全部完成后才执行,保证了数据写入时的一致性。
四、GCD的性能优化与注意事项
-
性能优化
- 合理选择队列类型:根据任务的性质选择合适的队列类型。如果任务之间没有依赖关系且需要充分利用多核性能,应选择并发队列;如果任务有严格的顺序要求,应选择串行队列;如果任务涉及到UI更新,必须使用主队列。例如,在处理大量数据计算任务时,使用全局并发队列可以显著提高处理速度,而在更新UI时,使用主队列是唯一正确的选择,否则可能会导致界面显示异常。
- 避免过度并发:虽然并发队列可以提高性能,但过多的并发任务可能会导致资源竞争和系统开销增大。例如,在进行网络请求时,如果同时发起过多的请求,可能会导致网络拥堵,反而降低整体性能。可以通过
dispatch_semaphore
等机制来限制并发任务的数量,如前面下载文件的例子中,通过设置信号量初始值为3,合理控制了同时下载的文件数量。 - 减少任务创建开销:创建任务(无论是同步还是异步)都有一定的开销。如果有大量相似的小任务,可以考虑将它们合并成一个较大的任务。例如,在处理图片时,如果每张图片都有一个非常简单的处理操作,可以将多张图片的处理合并到一个任务中,减少任务创建的次数。
-
注意事项
- 线程安全问题:虽然GCD简化了多线程编程,但在处理共享资源时仍需要注意线程安全。例如,多个任务同时访问和修改同一个可变数组时,可能会导致数据不一致。可以使用
dispatch_barrier
或者锁机制(如NSLock
、dispatch_mutex
等)来保证对共享资源的安全访问。在前面的DataStore
示例中,通过dispatch_barrier_async
保证了数据写入的线程安全。 - 内存管理:在多线程环境下,内存管理也需要格外小心。例如,当一个对象在一个线程中被释放,但另一个线程可能还在使用它时,就会导致野指针错误。确保对象的生命周期管理正确,在对象不再被使用时,要保证没有其他线程在访问它。可以使用
dispatch_group
等机制来等待相关任务完成后再释放对象。 - 死锁问题:死锁是多线程编程中常见的问题。在GCD中,死锁通常发生在同步任务提交到当前线程所在的队列时。例如,在主队列中同步提交一个任务到主队列,就会导致死锁,因为同步任务会阻塞当前线程,而任务又需要当前线程来执行。要避免死锁,需要仔细规划任务的提交和执行顺序,确保不会出现相互等待的情况。
- 线程安全问题:虽然GCD简化了多线程编程,但在处理共享资源时仍需要注意线程安全。例如,多个任务同时访问和修改同一个可变数组时,可能会导致数据不一致。可以使用
在Objective-C开发中,GCD为我们提供了强大而灵活的多线程编程工具。通过深入理解GCD的基础概念、应用场景、高级特性以及性能优化和注意事项,我们可以编写出高效、稳定且响应迅速的多线程应用程序。无论是处理耗时操作、提高性能还是管理复杂的任务依赖关系,GCD都能帮助开发者轻松应对各种多线程编程挑战。