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

Objective-C多线程编程之GCD技术详解

2022-06-217.6k 阅读

一、GCD简介

在Objective-C编程中,多线程编程是一项至关重要的技能,它允许我们同时执行多个任务,提高应用程序的响应性和性能。Grand Central Dispatch(GCD)是苹果公司为iOS和OS X开发的一项强大的多线程编程技术。GCD提供了一种基于队列的异步执行模型,使得多线程编程变得更加简单和高效。

GCD的核心概念是队列(queue)和任务(task)。任务是我们想要执行的代码块,而队列则用于管理这些任务的执行顺序。GCD提供了两种类型的队列:串行队列(serial queue)和并发队列(concurrent queue)。串行队列一次只执行一个任务,当前任务完成后,下一个任务才会开始执行。并发队列可以同时执行多个任务,具体的并发数量由系统自动管理。

二、GCD队列

  1. 主队列(Main Queue) 主队列是GCD中一个特殊的串行队列,它与应用程序的主线程相关联。所有在主队列中添加的任务都会在主线程中执行。由于主线程负责处理用户界面的更新和事件响应,因此在主队列中执行的任务应该尽量简短,避免阻塞主线程,导致界面卡顿。 以下是向主队列添加任务的代码示例:
dispatch_async(dispatch_get_main_queue(), ^{
    // 在主队列中执行的任务代码
    NSLog(@"This is a task on the main queue.");
});

在上述代码中,dispatch_async函数用于将一个任务异步添加到指定的队列中。dispatch_get_main_queue()函数用于获取主队列。

  1. 全局队列(Global Queue) 全局队列是GCD提供的并发队列,系统为我们提供了四个不同优先级的全局队列:高优先级、默认优先级、低优先级和后台优先级。我们可以使用dispatch_get_global_queue函数来获取这些全局队列。 示例代码如下:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 在全局队列中执行的任务代码
    NSLog(@"This is a task on the global queue.");
});

在这段代码中,dispatch_get_global_queue函数的第一个参数指定了队列的优先级,DISPATCH_QUEUE_PRIORITY_DEFAULT表示默认优先级。第二个参数目前保留为0,应始终设置为0。

  1. 自定义队列(Custom Queue) 除了主队列和全局队列,我们还可以创建自己的自定义队列。自定义队列可以是串行队列,也可以是并发队列。通过创建自定义队列,我们可以更好地控制任务的执行顺序和并发行为。 创建串行自定义队列的代码如下:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    // 在串行自定义队列中执行的任务代码
    NSLog(@"This is a task on a serial custom queue.");
});

在上述代码中,dispatch_queue_create函数用于创建一个自定义队列。第一个参数是队列的名称,这是一个字符串,用于标识队列,方便调试。第二个参数DISPATCH_QUEUE_SERIAL表示创建的是串行队列。如果要创建并发队列,将第二个参数设置为DISPATCH_QUEUE_CONCURRENT即可。

三、GCD任务

  1. 异步任务(Asynchronous Task) 异步任务是指不会阻塞当前线程,而是将任务添加到指定队列后立即返回,继续执行后续代码。在GCD中,我们使用dispatch_async函数来提交异步任务。 例如,我们可以在全局队列中执行一个耗时的计算任务,而不会阻塞主线程:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 模拟一个耗时的计算任务
    for (int i = 0; i < 1000000; i++) {
        // 进行一些计算
    }
    NSLog(@"Asynchronous task completed.");
});
NSLog(@"This code is executed immediately after submitting the asynchronous task.");

在这个示例中,dispatch_async函数将计算任务添加到全局队列后,主线程不会等待任务完成,而是继续执行后续的NSLog语句。

  1. 同步任务(Synchronous Task) 同步任务会阻塞当前线程,直到任务执行完成。在GCD中,我们使用dispatch_sync函数来提交同步任务。通常不建议在主线程中使用同步任务提交到主队列,因为这会导致死锁。 以下是一个在自定义串行队列中执行同步任务的示例:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    // 在串行自定义队列中执行的同步任务代码
    NSLog(@"This is a synchronous task on a serial custom queue.");
});
NSLog(@"This code is executed after the synchronous task is completed.");

在上述代码中,dispatch_sync函数将任务添加到串行自定义队列,并阻塞当前线程,直到任务执行完毕,然后才会执行后续的NSLog语句。

四、GCD栅栏函数

GCD的栅栏函数(barrier)提供了一种机制,用于在并发队列中插入一个同步点。当栅栏任务执行时,并发队列会暂停其他任务的执行,直到栅栏任务完成。这在某些情况下非常有用,例如当我们需要对共享资源进行读写操作时,防止数据竞争。

  1. dispatch_barrier_async dispatch_barrier_async函数用于将一个栅栏任务异步提交到并发队列。 示例代码如下:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 提交一些异步任务
dispatch_async(concurrentQueue, ^{
    // 异步任务1
    NSLog(@"Asynchronous task 1");
});
dispatch_async(concurrentQueue, ^{
    // 异步任务2
    NSLog(@"Asynchronous task 2");
});
// 提交栅栏任务
dispatch_barrier_async(concurrentQueue, ^{
    // 栅栏任务,进行数据写入操作
    NSLog(@"Barrier task for writing data");
});
// 提交更多异步任务
dispatch_async(concurrentQueue, ^{
    // 异步任务3
    NSLog(@"Asynchronous task 3");
});

在这个示例中,栅栏任务会等待前面的异步任务1和异步任务2完成后才开始执行。在栅栏任务执行期间,后续的异步任务3会被暂停,直到栅栏任务完成。

  1. dispatch_barrier_sync dispatch_barrier_sync函数用于将一个栅栏任务同步提交到并发队列。与dispatch_barrier_async类似,但会阻塞当前线程,直到栅栏任务完成。
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 提交一些异步任务
dispatch_async(concurrentQueue, ^{
    // 异步任务1
    NSLog(@"Asynchronous task 1");
});
dispatch_async(concurrentQueue, ^{
    // 异步任务2
    NSLog(@"Asynchronous task 2");
});
// 提交同步栅栏任务
dispatch_barrier_sync(concurrentQueue, ^{
    // 栅栏任务,进行数据写入操作
    NSLog(@"Barrier task for writing data");
});
// 提交更多异步任务
dispatch_async(concurrentQueue, ^{
    // 异步任务3
    NSLog(@"Asynchronous task 3");
});

在这个例子中,主线程会等待栅栏任务完成后,才会继续执行后续代码。

五、GCD延迟执行

有时候我们需要延迟执行某个任务,GCD提供了方便的方法来实现这一点。

  1. dispatch_after dispatch_after函数可以将一个任务延迟指定的时间后提交到指定队列。 示例代码如下:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
dispatch_after(delayTime, mainQueue, ^{
    // 延迟2秒后在主队列执行的任务
    NSLog(@"This task is executed after a 2 - second delay.");
});

在上述代码中,dispatch_time函数用于计算延迟时间。DISPATCH_TIME_NOW表示当前时间,(int64_t)(2.0 * NSEC_PER_SEC)表示延迟2秒。NSEC_PER_SEC是每秒的纳秒数。

六、GCD组(Group)

GCD组允许我们将多个任务组合在一起,并在所有任务完成后执行一个最终的任务。这在需要等待多个异步任务完成后再进行下一步操作时非常有用。

  1. 创建组并添加任务
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, globalQueue, ^{
    // 任务1
    NSLog(@"Task 1 started");
    for (int i = 0; i < 1000000; i++) {
        // 进行一些计算
    }
    NSLog(@"Task 1 completed");
});
dispatch_group_async(group, globalQueue, ^{
    // 任务2
    NSLog(@"Task 2 started");
    for (int i = 0; i < 1000000; i++) {
        // 进行一些计算
    }
    NSLog(@"Task 2 completed");
});

在上述代码中,dispatch_group_create函数创建了一个GCD组。dispatch_group_async函数将任务添加到指定队列,并将任务与组关联。

  1. 等待组内任务完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 当组内所有任务完成后,在主队列执行此任务
    NSLog(@"All tasks in the group have completed.");
});

dispatch_group_notify函数设置了一个通知,当组内所有任务完成后,会将指定的任务提交到指定队列(这里是主队列)执行。

  1. 同步等待组内任务完成
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"All tasks in the group have completed (synchronous wait).");

dispatch_group_wait函数会阻塞当前线程,直到组内所有任务完成。DISPATCH_TIME_FOREVER表示无限期等待。

七、GCD信号量(Semaphore)

GCD信号量是一种计数器,用于控制对共享资源的访问。信号量的值表示当前可用的资源数量。当一个任务获取信号量时,如果信号量的值大于0,则信号量的值减1,任务可以继续执行;如果信号量的值为0,则任务会被阻塞,直到信号量的值大于0。

  1. 创建信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

在上述代码中,dispatch_semaphore_create函数创建了一个初始值为1的信号量。

  1. 获取信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
NSLog(@"Accessing shared resource.");

dispatch_semaphore_wait函数用于获取信号量。如果信号量的值为0,它会阻塞当前线程,直到信号量的值大于0。DISPATCH_TIME_FOREVER表示无限期等待。

  1. 释放信号量
dispatch_semaphore_signal(semaphore);
NSLog(@"Released the semaphore.");

dispatch_semaphore_signal函数用于释放信号量,将信号量的值加1。

八、GCD在实际开发中的应用场景

  1. 网络请求 在进行网络请求时,通常需要在后台线程中执行,以避免阻塞主线程。使用GCD可以很方便地将网络请求任务提交到全局队列或自定义队列中执行。例如:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    NSURL *url = [NSURL URLWithString:@"https://example.com/api"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    if (data) {
        // 处理网络响应数据
        dispatch_async(dispatch_get_main_queue(), ^{
            // 在主队列中更新UI
        });
    }
});
  1. 数据缓存和加载 在加载大量数据时,可以使用GCD在后台线程中进行数据的缓存和加载。例如,从数据库或文件中读取数据,并在后台进行处理,然后在主线程中更新UI显示加载后的数据。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 从数据库或文件中读取数据
    NSArray *dataArray = [self loadDataFromDatabase];
    // 对数据进行处理
    NSArray *processedArray = [self processData:dataArray];
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主队列中更新UI显示数据
        self.tableView.dataSource = processedArray;
        [self.tableView reloadData];
    });
});
  1. 图像处理 处理图像时,例如缩放、裁剪、滤镜应用等操作通常比较耗时。可以使用GCD将这些图像处理任务提交到并发队列中执行,以提高处理效率。
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.imageProcessingQueue", DISPATCH_QUEUE_CONCURRENT);
UIImage *originalImage = [UIImage imageNamed:@"example.jpg"];
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
    UIImage *scaledImage = [self scaleImage:originalImage toSize:CGSizeMake(100, 100)];
    // 处理缩放后的图像
});
dispatch_group_async(group, concurrentQueue, ^{
    UIImage *croppedImage = [self cropImage:originalImage toRect:CGRectMake(50, 50, 100, 100)];
    // 处理裁剪后的图像
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有图像处理完成后,在主队列中更新UI
});

通过合理使用GCD的各种功能,我们可以在Objective-C开发中高效地实现多线程编程,提升应用程序的性能和用户体验。无论是简单的任务调度,还是复杂的资源管理和异步操作,GCD都提供了强大而灵活的解决方案。在实际应用中,需要根据具体的需求和场景,选择合适的队列、任务类型以及GCD的其他特性,以确保程序的正确性和高效性。同时,要注意避免多线程编程中常见的问题,如死锁、数据竞争等,通过合理的设计和使用GCD的同步机制来保证程序的稳定性。