Objective-C多线程编程之GCD技术详解
一、GCD简介
在Objective-C编程中,多线程编程是一项至关重要的技能,它允许我们同时执行多个任务,提高应用程序的响应性和性能。Grand Central Dispatch(GCD)是苹果公司为iOS和OS X开发的一项强大的多线程编程技术。GCD提供了一种基于队列的异步执行模型,使得多线程编程变得更加简单和高效。
GCD的核心概念是队列(queue)和任务(task)。任务是我们想要执行的代码块,而队列则用于管理这些任务的执行顺序。GCD提供了两种类型的队列:串行队列(serial queue)和并发队列(concurrent queue)。串行队列一次只执行一个任务,当前任务完成后,下一个任务才会开始执行。并发队列可以同时执行多个任务,具体的并发数量由系统自动管理。
二、GCD队列
- 主队列(Main Queue) 主队列是GCD中一个特殊的串行队列,它与应用程序的主线程相关联。所有在主队列中添加的任务都会在主线程中执行。由于主线程负责处理用户界面的更新和事件响应,因此在主队列中执行的任务应该尽量简短,避免阻塞主线程,导致界面卡顿。 以下是向主队列添加任务的代码示例:
dispatch_async(dispatch_get_main_queue(), ^{
// 在主队列中执行的任务代码
NSLog(@"This is a task on the main queue.");
});
在上述代码中,dispatch_async
函数用于将一个任务异步添加到指定的队列中。dispatch_get_main_queue()
函数用于获取主队列。
- 全局队列(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。
- 自定义队列(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任务
- 异步任务(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
语句。
- 同步任务(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)提供了一种机制,用于在并发队列中插入一个同步点。当栅栏任务执行时,并发队列会暂停其他任务的执行,直到栅栏任务完成。这在某些情况下非常有用,例如当我们需要对共享资源进行读写操作时,防止数据竞争。
- 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会被暂停,直到栅栏任务完成。
- 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提供了方便的方法来实现这一点。
- 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组允许我们将多个任务组合在一起,并在所有任务完成后执行一个最终的任务。这在需要等待多个异步任务完成后再进行下一步操作时非常有用。
- 创建组并添加任务
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
函数将任务添加到指定队列,并将任务与组关联。
- 等待组内任务完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 当组内所有任务完成后,在主队列执行此任务
NSLog(@"All tasks in the group have completed.");
});
dispatch_group_notify
函数设置了一个通知,当组内所有任务完成后,会将指定的任务提交到指定队列(这里是主队列)执行。
- 同步等待组内任务完成
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。
- 创建信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
在上述代码中,dispatch_semaphore_create
函数创建了一个初始值为1的信号量。
- 获取信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
NSLog(@"Accessing shared resource.");
dispatch_semaphore_wait
函数用于获取信号量。如果信号量的值为0,它会阻塞当前线程,直到信号量的值大于0。DISPATCH_TIME_FOREVER
表示无限期等待。
- 释放信号量
dispatch_semaphore_signal(semaphore);
NSLog(@"Released the semaphore.");
dispatch_semaphore_signal
函数用于释放信号量,将信号量的值加1。
八、GCD在实际开发中的应用场景
- 网络请求 在进行网络请求时,通常需要在后台线程中执行,以避免阻塞主线程。使用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
});
}
});
- 数据缓存和加载 在加载大量数据时,可以使用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];
});
});
- 图像处理 处理图像时,例如缩放、裁剪、滤镜应用等操作通常比较耗时。可以使用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的同步机制来保证程序的稳定性。