MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

学会在Objective-C中使用GCD进行多线程编程

2021-03-216.5k 阅读

一、GCD基础概念

  1. 什么是GCD GCD(Grand Central Dispatch)是苹果公司开发的一个多核编程的解决方案,它基于队列(queue)和任务(task)的概念。在Objective-C开发中,GCD提供了一种简单而高效的方式来进行多线程编程,大大简化了传统多线程编程中诸如线程管理、同步等复杂的操作。

  2. 队列(Queue)

    • 类型
      • 串行队列(Serial Queue):也称为私有队列,同一时间只有一个任务在执行。当一个任务完成后,队列才会从等待队列中取出下一个任务执行。例如,我们可以使用dispatch_queue_create函数创建一个串行队列:
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(后台优先级)。高优先级的任务会优先于低优先级的任务在队列中执行,但这并不意味着高优先级任务一定会先完成,因为实际执行还受到系统资源等多种因素的影响。
  1. 任务(Task)
    • 类型
      • 同步任务(Synchronous Task):使用dispatch_sync函数提交任务到队列,该函数会阻塞当前线程,直到任务执行完毕。例如,向串行队列提交一个同步任务:
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中的应用场景

  1. 耗时操作与主线程分离 在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的代码提交到主队列执行。

  1. 提高性能的并发处理 对于一些可以并行处理的任务,如图片处理、数据计算等,可以使用并发队列来充分利用多核处理器的性能。
    • 示例:图片处理 假设我们有一组图片需要进行模糊处理,我们可以将每张图片的模糊处理任务提交到并发队列中:
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];
    });
}

在这个例子中,每张图片的模糊处理和保存操作在并发队列中异步执行,这样可以同时处理多张图片,提高整体处理效率。

  1. 任务依赖与顺序执行 有时候我们需要任务按照一定的顺序执行,或者一个任务依赖于另一个任务的完成。可以通过串行队列或者使用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的高级特性

  1. 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中的所有任务完成后,在主队列中执行数据合并和处理的代码。

  1. 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,允许下一个任务获取信号量进行下载。

  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的性能优化与注意事项

  1. 性能优化

    • 合理选择队列类型:根据任务的性质选择合适的队列类型。如果任务之间没有依赖关系且需要充分利用多核性能,应选择并发队列;如果任务有严格的顺序要求,应选择串行队列;如果任务涉及到UI更新,必须使用主队列。例如,在处理大量数据计算任务时,使用全局并发队列可以显著提高处理速度,而在更新UI时,使用主队列是唯一正确的选择,否则可能会导致界面显示异常。
    • 避免过度并发:虽然并发队列可以提高性能,但过多的并发任务可能会导致资源竞争和系统开销增大。例如,在进行网络请求时,如果同时发起过多的请求,可能会导致网络拥堵,反而降低整体性能。可以通过dispatch_semaphore等机制来限制并发任务的数量,如前面下载文件的例子中,通过设置信号量初始值为3,合理控制了同时下载的文件数量。
    • 减少任务创建开销:创建任务(无论是同步还是异步)都有一定的开销。如果有大量相似的小任务,可以考虑将它们合并成一个较大的任务。例如,在处理图片时,如果每张图片都有一个非常简单的处理操作,可以将多张图片的处理合并到一个任务中,减少任务创建的次数。
  2. 注意事项

    • 线程安全问题:虽然GCD简化了多线程编程,但在处理共享资源时仍需要注意线程安全。例如,多个任务同时访问和修改同一个可变数组时,可能会导致数据不一致。可以使用dispatch_barrier或者锁机制(如NSLockdispatch_mutex等)来保证对共享资源的安全访问。在前面的DataStore示例中,通过dispatch_barrier_async保证了数据写入的线程安全。
    • 内存管理:在多线程环境下,内存管理也需要格外小心。例如,当一个对象在一个线程中被释放,但另一个线程可能还在使用它时,就会导致野指针错误。确保对象的生命周期管理正确,在对象不再被使用时,要保证没有其他线程在访问它。可以使用dispatch_group等机制来等待相关任务完成后再释放对象。
    • 死锁问题:死锁是多线程编程中常见的问题。在GCD中,死锁通常发生在同步任务提交到当前线程所在的队列时。例如,在主队列中同步提交一个任务到主队列,就会导致死锁,因为同步任务会阻塞当前线程,而任务又需要当前线程来执行。要避免死锁,需要仔细规划任务的提交和执行顺序,确保不会出现相互等待的情况。

在Objective-C开发中,GCD为我们提供了强大而灵活的多线程编程工具。通过深入理解GCD的基础概念、应用场景、高级特性以及性能优化和注意事项,我们可以编写出高效、稳定且响应迅速的多线程应用程序。无论是处理耗时操作、提高性能还是管理复杂的任务依赖关系,GCD都能帮助开发者轻松应对各种多线程编程挑战。