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

解析Objective-C中GCD(Grand Central Dispatch)的语法与应用

2023-09-167.2k 阅读

1. GCD 概述

在 Objective-C 编程中,Grand Central Dispatch(GCD)是一项非常重要的技术。GCD 是苹果公司为多核处理器开发的一种异步任务执行模型,它为开发者提供了一种简单而高效的方式来管理并发编程。

在传统的多线程编程中,开发者需要手动创建线程、管理线程生命周期以及处理线程同步问题,这不仅复杂而且容易出错。而 GCD 则通过队列(queue)和块(block)的概念,将这些复杂的操作进行了封装,让开发者能够专注于业务逻辑的实现。

GCD 的核心优势在于它能够自动管理线程的创建、调度和销毁,开发者只需要定义任务并将其添加到适当的队列中,GCD 就会根据系统资源和任务优先级来合理地分配线程执行任务。这种方式大大简化了并发编程,提高了程序的性能和稳定性。

2. GCD 中的队列

2.1 队列类型

GCD 中有两种主要类型的队列:串行队列(Serial Queue)和并发队列(Concurrent Queue)。

  • 串行队列:串行队列一次只处理一个任务。当一个任务完成后,队列才会从队列中取出下一个任务并执行。这意味着在串行队列中的任务是按照它们被添加到队列中的顺序依次执行的。串行队列适用于那些需要顺序执行的任务,比如对数据库的写入操作,以避免数据冲突。
  • 并发队列:并发队列可以同时处理多个任务。GCD 会根据系统的 CPU 核心数和可用资源来决定同时执行多少个任务。并发队列适用于那些可以并行处理的任务,比如同时下载多个图片。

除了这两种自定义队列外,GCD 还提供了两个特殊的队列:主队列(Main Queue)和全局队列(Global Queue)。

  • 主队列:主队列是与应用程序的主线程相关联的串行队列。主线程负责处理用户界面的更新、事件处理等操作。将任务添加到主队列中,这些任务会在主线程中依次执行。由于主线程是单线程的,因此在主队列中执行耗时操作会导致界面卡顿,所以耗时操作一般不应该放在主队列中。
  • 全局队列:全局队列是系统提供的并发队列,供整个应用程序使用。全局队列有不同的优先级,包括高优先级、默认优先级、低优先级和后台优先级。开发者可以根据任务的性质选择合适优先级的全局队列来执行任务。

2.2 创建队列

在 Objective-C 中,可以使用 dispatch_queue_create 函数来创建自定义队列。该函数的原型如下:

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

其中,label 是队列的名称,用于标识队列,通常使用反向域名格式,例如 com.example.myqueueattr 用于指定队列的属性,例如队列是串行还是并发队列。如果 attrNULL,则创建一个串行队列。

以下是创建串行队列和并发队列的示例代码:

// 创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialqueue", NULL);

// 创建并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentqueue", DISPATCH_QUEUE_CONCURRENT);

对于主队列和全局队列,可以使用以下函数获取:

// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

// 获取全局队列(默认优先级)
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

3. GCD 中的块(Block)

块(Block)是 GCD 中定义任务的方式。块是一种匿名函数,可以捕获其定义时所在作用域的变量。在 GCD 中,任务以块的形式被提交到队列中执行。

块的定义格式如下:

^{
    // 任务代码
};

例如,下面是一个简单的块定义:

void (^myBlock)(void) = ^{
    NSLog(@"This is a block.");
};

可以通过调用块来执行其中的代码:

myBlock();

在 GCD 中,通常会将块直接作为参数传递给队列相关的函数,例如:

dispatch_async(serialQueue, ^{
    NSLog(@"This task is executed asynchronously in a serial queue.");
});

4. GCD 的基本应用

4.1 异步执行任务

使用 GCD 进行异步任务执行是其最常见的应用场景之一。通过 dispatch_async 函数,可以将任务提交到指定的队列中异步执行,而不会阻塞当前线程。dispatch_async 函数的原型如下:

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

例如,将一个耗时操作放在并发队列中异步执行:

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
    // 模拟耗时操作,例如网络请求或文件读取
    sleep(2);
    NSLog(@"Asynchronous task completed.");
});
NSLog(@"This is the main thread, not blocked.");

在上述代码中,dispatch_async 将任务添加到并发队列 concurrentQueue 中执行,主线程不会被阻塞,会继续执行后面的代码,输出 This is the main thread, not blocked.。而在并发队列中的任务会在 2 秒后执行完毕并输出 Asynchronous task completed.

4.2 同步执行任务

与异步执行相对的是同步执行任务,使用 dispatch_sync 函数。dispatch_sync 函数会将任务提交到指定队列中,并等待任务执行完毕后才返回。其原型如下:

void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

例如,将一个任务同步提交到串行队列中执行:

dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialqueue", NULL);
dispatch_sync(serialQueue, ^{
    NSLog(@"Synchronous task in serial queue.");
});
NSLog(@"This is after the synchronous task.");

在上述代码中,dispatch_sync 将任务提交到串行队列 serialQueue 中执行,主线程会等待该任务执行完毕后才会继续执行后面的代码,所以会先输出 Synchronous task in serial queue.,然后输出 This is after the synchronous task.

需要注意的是,如果在主线程中使用 dispatch_sync 提交任务到主队列中,会导致死锁。因为 dispatch_sync 会等待任务执行完毕,而任务又在主队列中等待主线程空闲才能执行,这样就形成了死锁。例如:

// 下面的代码会导致死锁
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"This will cause a deadlock.");
});

4.3 延迟执行任务

有时候需要在一定时间后执行某个任务,GCD 提供了 dispatch_after 函数来实现延迟执行。dispatch_after 函数的原型如下:

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

其中,when 是任务开始执行的时间点,以 dispatch_time 类型表示。queue 是任务执行的队列,block 是要执行的任务块。

例如,延迟 2 秒在主队列中执行一个任务:

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.");
});

在上述代码中,dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)) 表示从现在开始 2 秒后执行任务。任务会被添加到主队列中,2 秒后在主线程中执行并输出 Delayed task executed.

4.4 队列组(Dispatch Group)

队列组用于管理一组任务,当这组任务全部完成后,可以执行一个完成块。通过 dispatch_group_create 创建队列组,dispatch_group_async 将任务添加到队列组中,dispatch_group_wait 等待队列组中的所有任务完成,dispatch_group_notify 在所有任务完成后执行一个通知块。

以下是一个使用队列组的示例:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, concurrentQueue, ^{
    // 第一个任务
    sleep(1);
    NSLog(@"Task 1 completed.");
});

dispatch_group_async(group, concurrentQueue, ^{
    // 第二个任务
    sleep(2);
    NSLog(@"Task 2 completed.");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有任务完成后执行的通知块
    NSLog(@"All tasks in the group are completed.");
});

在上述代码中,两个任务被异步添加到并发队列中执行,dispatch_group_notify 会在两个任务都完成后,在主队列中执行通知块,输出 All tasks in the group are completed.

5. GCD 中的线程同步

在并发编程中,线程同步是一个重要的问题,以避免多个线程同时访问和修改共享资源导致的数据不一致。GCD 提供了几种机制来实现线程同步。

5.1 信号量(Dispatch Semaphore)

信号量是一种计数器,用于控制对共享资源的访问。在 GCD 中,可以使用 dispatch_semaphore_create 创建信号量,dispatch_semaphore_wait 等待信号量,dispatch_semaphore_signal 释放信号量。

例如,假设有一个共享资源,多个线程需要访问,使用信号量来控制访问:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 创建一个初始值为 1 的信号量

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 5; i++) {
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 访问共享资源
        NSLog(@"Thread %d is accessing the shared resource.", i);
        sleep(1);
        dispatch_semaphore_signal(semaphore);
    });
}

在上述代码中,信号量初始值为 1,表示同一时间只有一个线程可以访问共享资源。dispatch_semaphore_wait 会等待信号量的值大于 0,如果信号量为 0,则线程会阻塞。当一个线程访问完共享资源后,通过 dispatch_semaphore_signal 释放信号量,让其他线程可以访问。

5.2 栅栏函数(Dispatch Barrier)

栅栏函数用于在并发队列中插入一个屏障,确保在屏障之前的任务全部执行完毕后,才执行屏障之后的任务。使用 dispatch_barrier_asyncdispatch_barrier_sync 函数来实现。

例如,在一个并发队列中,先执行一些读取操作,然后执行一个写入操作,最后再执行读取操作,使用栅栏函数来保证写入操作的原子性:

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentqueue", DISPATCH_QUEUE_CONCURRENT);

// 读取操作 1
dispatch_async(concurrentQueue, ^{
    NSLog(@"Read operation 1.");
});

// 读取操作 2
dispatch_async(concurrentQueue, ^{
    NSLog(@"Read operation 2.");
});

// 写入操作
dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"Write operation.");
});

// 读取操作 3
dispatch_async(concurrentQueue, ^{
    NSLog(@"Read operation 3.");
});

在上述代码中,dispatch_barrier_async 插入了一个屏障,保证了在写入操作之前的所有读取操作都执行完毕后,才执行写入操作。写入操作完成后,才会执行后面的读取操作,从而避免了读取和写入操作之间的数据冲突。

6. GCD 在实际项目中的应用场景

6.1 网络请求

在 iOS 开发中,网络请求是非常常见的操作。由于网络请求通常是耗时的,不应该阻塞主线程,因此可以使用 GCD 将网络请求任务放在并发队列中异步执行。例如,使用 AFNetworking 进行网络请求时,可以结合 GCD 来处理请求结果:

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"https://example.com/api/data" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程中更新 UI
        self.dataLabel.text = [NSString stringWithFormat:@"Received data: %@", responseObject];
    });
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程中显示错误信息
        self.errorLabel.text = [NSString stringWithFormat:@"Error: %@", error];
    });
}];

在上述代码中,网络请求在后台线程中执行,当请求成功或失败后,通过 dispatch_async 将更新 UI 的操作放在主队列中执行,以确保 UI 的更新在主线程中进行,避免界面卡顿。

6.2 图片加载与处理

在处理图片加载和处理时,也可以利用 GCD 的并发特性来提高效率。例如,同时下载多个图片,并在下载完成后进行图片处理和显示:

NSArray *imageURLs = @[@"https://example.com/image1.jpg", @"https://example.com/image2.jpg", @"https://example.com/image3.jpg"];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSString *urlString in imageURLs) {
    NSURL *url = [NSURL URLWithString:urlString];
    dispatch_group_async(group, concurrentQueue, ^{
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:imageData];
        // 图片处理,例如缩放
        UIImage *scaledImage = [self scaleImage:image toSize:CGSizeMake(200, 200)];
        dispatch_async(dispatch_get_main_queue(), ^{
            // 在主线程中显示图片
            UIImageView *imageView = [[UIImageView alloc] initWithImage:scaledImage];
            [self.view addSubview:imageView];
        });
    });
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"All images are loaded and processed.");
});

在上述代码中,通过队列组和并发队列,多个图片的下载和处理可以同时进行,提高了整体的效率。处理完成后,在主队列中显示图片,确保 UI 操作的安全性。

6.3 数据缓存

在应用程序中,数据缓存是提高性能的重要手段。可以使用 GCD 来管理缓存的读写操作,以确保数据的一致性。例如,在一个简单的内存缓存中,使用信号量来控制对缓存的访问:

@interface DataCache : NSObject
@property (nonatomic, strong) NSMutableDictionary *cache;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end

@implementation DataCache
- (instancetype)init {
    self = [super init];
    if (self) {
        self.cache = [NSMutableDictionary dictionary];
        self.semaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)setObject:(id)object forKey:(NSString *)key {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    self.cache[key] = object;
    dispatch_semaphore_signal(self.semaphore);
}

- (id)objectForKey:(NSString *)key {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    id object = self.cache[key];
    dispatch_semaphore_signal(self.semaphore);
    return object;
}
@end

在上述代码中,DataCache 类使用信号量来保证对缓存字典的读写操作是线程安全的,避免了多个线程同时访问和修改缓存数据导致的数据不一致问题。

7. GCD 的性能优化

虽然 GCD 已经大大简化了并发编程,但在实际应用中,仍需要注意一些性能优化的要点。

7.1 合理选择队列类型

根据任务的性质选择合适的队列类型非常重要。对于需要顺序执行的任务,如数据库写入操作,应使用串行队列;对于可以并行处理的任务,如下载多个文件,应使用并发队列。同时,要避免在主线程中执行耗时操作,尽量将耗时任务放在全局队列或自定义的并发队列中执行。

7.2 控制任务粒度

任务粒度指的是任务的大小和复杂度。如果任务粒度太小,创建和管理任务的开销可能会超过任务本身的执行时间,导致性能下降。例如,将大量非常小的任务添加到队列中,会增加系统的调度开销。相反,如果任务粒度太大,可能无法充分利用多核处理器的性能。因此,需要根据实际情况合理控制任务粒度,以达到最佳的性能。

7.3 避免过度同步

虽然线程同步是必要的,但过度同步会导致性能瓶颈。例如,在不需要同步的情况下使用信号量或其他同步机制,会限制任务的并发执行能力。因此,要仔细分析共享资源的访问情况,只在必要时进行同步。

8. 总结 GCD 的优势与注意事项

GCD 为 Objective-C 开发者提供了一种强大而简洁的并发编程方式,它具有以下优势:

  • 简化并发编程:通过队列和块的概念,开发者无需手动管理线程的创建、调度和销毁,大大降低了并发编程的复杂度。
  • 提高性能:GCD 能够自动根据系统资源和任务优先级合理分配线程执行任务,充分利用多核处理器的性能,提高程序的执行效率。
  • 线程安全:提供了多种线程同步机制,如信号量、栅栏函数等,帮助开发者确保共享资源的安全访问。

在使用 GCD 时,也需要注意以下事项:

  • 避免死锁:在主线程中使用 dispatch_sync 提交任务到主队列会导致死锁,要特别注意这种情况。
  • 合理使用同步机制:过度使用同步机制会降低程序的并发性能,要根据实际需求合理选择和使用同步机制。
  • 注意任务粒度:任务粒度的大小会影响程序的性能,要根据任务的特点合理划分任务。

总之,熟练掌握 GCD 的语法和应用,能够帮助开发者编写出高效、稳定的并发程序,提升应用程序的性能和用户体验。在实际项目中,应根据具体需求合理运用 GCD 的各种特性,充分发挥其优势,同时避免常见的陷阱。通过不断的实践和优化,开发者可以更好地利用 GCD 来实现复杂的并发任务,为用户带来更流畅、高效的应用体验。无论是网络请求、图片处理还是数据缓存等场景,GCD 都能提供有效的解决方案,成为开发者在 Objective-C 开发中不可或缺的工具。