GCD(Grand Central Dispatch)是一套相对底层的C语言API接口,用来在多核硬件上进行多线程编程,是iOS中应用最广的多线程编程技术。使用GCD时,开发者无需关心线程的管理,GCD会自动维护一个线程池,开发者只需要把要执行的任务,按需分配到不同的队列中即可。
一、GCD调度机制
调度队列是GCD中非常重要的一个概念,执行多线程任务实际是由调度队列完成的,开发者只需要将任务添加到合适的调度队列中即可。
GCD中的调度队列有3中类型:主队列、全局队列、自定义队列。
1、主队列
主队列中的任务,都将在主线程中执行。
在应用程序中,主线程仅有1个,因此主队列是一个简单的串行队列,其中的任务会在主线程中一次执行。使用函数dispatch_get_main_queue()
可以获取主队列。
2、全局队列
全局队列是由系统定义的一组任务队列,均为并行队列,其中的任务会并行执行,但是执行的顺序会严格遵循FIFO(先进先出)策略。使用下面的函数可以获取全局队列:
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags)
;
其中第1个参数identifier
用来指定要获取的全局队列标识,对应不同优先级:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 优先级高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 优先级中
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 优先级低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台的全局队列,优先级最低
第2个参数flags
是GCD预留参数,目前无实质意义,传0即可。
3、自定义队列
GCD支持开发者根据需要,创建自定义队列。自定义队列可以是串行,也可以是并行。若是串行队列,放入其中的任务会一次执行,并行队列,则不会按照顺序,而是会直接执行。创建自定义队列的函数如下:
dispatch_queue_t;
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t _Nullable attr);
其中第1个参数label
指定队列的名称,第2个参数attr
指定队列的类型,定义如下:
DISPATCH_QUEUE_SERIAL // 串行队列
DISPATCH_QUEUE_CONCURRENT // 并行队列
示例:
dispatch_queue_t queue =dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
二、添加任务到GCD队列
1、常用的向队列中添加任务的函数有如下两个:
// 添加与当前线程同步的任务
dispatch_sync(dispatch_queue_t _Nonnull queue, ^(void)block);
// 添加与当前线程异步的任务
dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block);
这两个函数也是GCD多线程编程的核心。示例:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"%@:1", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"%@:2", [NSThread currentThread]);
});
}
运行代码,控制台输出如下:
2021-11-20 10:53:30.398419+0800 MyProject[19214:1253521] <NSThread: 0x600002318680>{number = 1, name = main}:1
2021-11-20 10:53:30.398642+0800 MyProject[19214:1255254] <NSThread: 0x60000232f3c0>{number = 7, name = (null)}:2
可见,dispatch_sync
函数指定的任务虽然在自定义队列中执行,但是其设定为与当前线程同步,因此也被调度到主线程中执行。
2、执行顺序
dispatch_queue_t serialQueue = dispatch_queue_create("MyQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
// 异步任务
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(serialQueue, ^{
// 同步任务
NSLog(@"4");
});
NSLog(@"5");
打印顺序为 13245 。
首先打印1;
接下来将任务2添加到串行队列上,由于任务2是异步的,不会阻塞线程,所以继续向下执行,打印3;
然后执行到任务4,将任务4添加到该串行队列上,根据队列FIFO(先进先出)原则,任务4需等任务2执行后才会执行,又因为任务4是同步的,会阻塞线程,所以只有任务4完成后才会继续向下执行,打印5。
这里任务4在主线程中执行,任务2在子线程中执行。
如果任务4是添加到与任务2不同的队列(串行或并行),则任务2和4无序执行。
总结:串行队列先异步后同步。
三、使用调度组
调度组是GCD中基于信号量更高一层的封装功能,使用调度组可以将某些任务绑定在一起,无论这些任务是在串行还是并行,也无论是否在同一个线程中,调度组都可以保证其按照开发者逾期的顺序去执行。
比如,我们常会遇到依赖于两个或更多个网络请求的返回值,去进行数据的处理或UI展示。我们模拟有两个自定义的串行队列,两个耗时任务A和B分别在其中执行,当A、B两个任务都执行完成后再执行任务C,任务A、B完成的顺序并不确定,此时就非常适合使用调度组。首先将任务A、B绑定到同一个调度组中,等待调度组中的所有任务执行完成后,再执行任务C,示例代码如下:
dispatch_queue_t queue1 = dispatch_queue_create("myQueue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_SERIAL);
dispatch_group_t group = dispatch_group_create(); // 创建调度组
// 绑定任务到调度组
dispatch_group_async(group, queue1, ^{
// 耗时任务A
[NSThread sleepForTimeInterval:1];
NSLog(@"任务A完成");
});
dispatch_group_async(group, queue2, ^{
// 耗时任务B
[NSThread sleepForTimeInterval:2];
NSLog(@"任务B完成");
});
// 阻塞线程,直到队列中的任务执行完成
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 任务C
NSLog(@"任务C完成");
dispatch_group_wait()
函数的作用是阻塞当前线程,等待调度组中所有任务完成后,再向下执行,其中第1个参数为调度组对象,第二个参数则是设置最长等待时间。DISPATCH_TIME_FOREVER
表示一直等待,直至调度组任务完成。如果要设置为最长等待5s,改为:
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * (int64_t)1000000000)); // 单位为纳秒
dispatch_group_wait()
函数会阻塞当前线程进行等待,在实际开发中,很少会用到。更多的场景是最后要执行的任务也是一个耗时任务。例如有A、B、C三个耗时任务,都在并行的队列中执行,但是C必须依赖于A和B先完成,可以使用dispatch_group_notify()
函数,示例代码如下:
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 耗时任务A
[NSThread sleepForTimeInterval:2];
NSLog(@"任务A完成");
});
dispatch_group_async(group, queue, ^{
// 耗时任务B
[NSThread sleepForTimeInterval:2];
NSLog(@"任务B完成");
});
dispatch_group_notify(group, queue, ^{
// 耗时任务C
[NSThread sleepForTimeInterval:2];
NSLog(@"任务C完成");
});
// 任务D
NSLog(@"任务D完成");
dispatch_group_notify()
函数的作用是当调度组中的任务都执行完成后,再执行指定的任务。
上面的例子中,耗时任务都是用线程休眠模拟的。实际开发中,遇到更多的场景是耗时任务本身就是异步的,这时还需要配合dispatch_group_enter()
和dispatch_group_leave()
,示例代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[self asyncTask1:^{
dispatch_group_leave(group);
}];
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[self asyncTask2:^{
dispatch_group_leave(group);
}];
});
dispatch_group_notify(group, queue, ^{
NSLog(@"任务完成");
});
}
- (void)asyncTask1:(void (^)(void))block {
// 异步任务1
block();
}
- (void)asyncTask2:(void (^)(void))block {
// 异步任务2
block();
}
dispatch_group_enter()
函数告诉调度组,即将有一个任务开始执行,dispatch_group_leave()
函数告诉调度组,有一个任务执行完成。这两个函数必须成对使用。
四、使用GCD进行快速迭代
常用的循环方式如while循环、do-while循环、for循环,以及更加快速的for-in循环,都是在当前线程中执行的,哪怕是多和CPU设备,其调用的资源仍然是单核的。GCD提供了一种更加高效的循环方法,那就是使用dispatch_apply()
函数,可以最大限度的利用多核CPU的优势,GCD会自动分配线程来执行迭代任务。示例:
NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g"];
dispatch_apply(array.count , dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(size_t iteration) {
NSLog(@"%@ || %zu:%@", [NSThread currentThread], iteration, array[iteration]);
});
dispatch_apply()
函数中第1个参数为要迭代的次数,第2个为执行任务的队列,第3个参数则为要执行的迭代任务。
由于dispatch_apply()
函数中迭代任务分别被分配到不同线程中执行,所以其执行顺序是不可预测的。而且该函数是同步的,会阻塞当前线程,如果需要异步执行,就将其放入到另一个非主队列中执行。
五、使用GCD监听事件源
监听事件源是指当某些事件发生时,在指定的队列中执行回调任务。在实际应用中,使用GCD的事件源可以方便地创建自动聚合的自定义事件以及精度更高的定时器。
使用下面的函数创建事件源:
dispatch_source_t;
dispatch_source_create(dispatch_source_type_t _Nonnull type, uintptr_t handle, uintptr_t mask, dispatch_queue_t _Nullable queue);
第1个参数type
指定事件源的类型;第2个参数handle
为事件句柄,取决于第1个参数的实践类型,例如如果是内核端口事件,则将这个参数设置为端口号;第3个参数mask
也取决于第1个参数的事件类型,例如如果是文件操作相关的事件,则将这个参数设置为要监听的文件属性;第4个参数queue
设置执行事件回调任务的队列。事件源类型的定义如下:
// 自定义事件,触发事件后的数据会被叠加运算
#define DISPATCH_SOURCE_TYPE_DATA_ADD
// 自定义事件,触发事件后的数据会被按位或运算
#define DISPATCH_SOURCE_TYPE_DATA_OR
// 自定义事件,触发事件后的数据会被替换
#define DISPATCH_SOURCE_TYPE_DATA_REPLACE
// 内核端口发送数据事件
#define DISPATCH_SOURCE_TYPE_MACH_SEND
// 内核端口接收数据事件
#define DISPATCH_SOURCE_TYPE_MACH_RECV
// 内存压力事件
#define DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
// 进程相关事件
#define DISPATCH_SOURCE_TYPE_PROC
// 读文件相关事件
#define DISPATCH_SOURCE_TYPE_READ
// 信号相关事件
#define DISPATCH_SOURCE_TYPE_SIGNAL
// 定时器事件
#define DISPATCH_SOURCE_TYPE_TIMER
// 文件属性修改事件
#define DISPATCH_SOURCE_TYPE_VNODE
// 写文件相关事件
#define DISPATCH_SOURCE_TYPE_WRITE
创建了事件源后,还需要对齐设置一个回调任务。当事件发生时,会在指定的队列中执行回调任务。设置回调任务的函数如下:
dispatch_source_set_event_handler(dispatch_source_t _Nonnull source, ^(void)handler);
实际开发中,事件源在两种场景下应用最为广泛。当某个事件会频繁发生,我们需要将其聚合进行处理时,可以进行自定义事件源的监听。例如,某个页面由多个数据源控制,当其中的数据源变化时需要刷新页面,同时要避免数据源频繁变化导致多次刷新而影响其性能。要解决这个问题,可以使用自定义事件,示例:
// 创建事件源
dispatch_source_t source_t = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_REPLACE, 0, 0, dispatch_get_main_queue());
// 设置回调任务
dispatch_source_set_event_handler(source_t, ^{
NSLog(@"接收到自定义事件%lu", dispatch_source_get_data(source_t));
});
// 激活事件源监听
dispatch_resume(source_t);
for (int i = 0; i < 10; i++) {
// 合并自定义事件源的数据
dispatch_source_merge_data(source_t, 1);
}
示例中虽然在for循环中调用了10次事件合并,但最终只触发了一次回调任务。
使用定时器事件源可以创建精度更高的定时器,示例代码如下:
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"定时器%@", timer);
});
dispatch_resume(timer);
dispatch_source_set_timer ()
函数用来设置定时器事件的回调,第1个参数为定时器事件源对象,第2个参数为定时器任务的执行时间间隔,第3个参数为延时多久后开始执行。
六、Dispatch Semaphore - 信号量
GCD中的调度组实际上是基于信号量的封装,信号量本身的作用是通过信号来触发任务的执行。在GCD中,与信号量有关的函数只有3个:
1、dispatch_semaphore_create()
创建一个Semaphore并初始化信号的总量;
2、dispatch_semaphore_signal()
发送一个信号,使信号总量加1
3、dispatch_semaphore_wait()
使信号总量减1,当信号总量为0时,就会一直等待(阻塞所在线程),否则就可以正常执行。
示例如下:
// 创建信号量,参数为信号量初始值
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(0);
// 发送信号,会使指定的信号量值加1
dispatch_semaphore_signal(semaphore_t);
while (1) {
/**
阻塞函数
当信号量大于0时,会穿透阻塞函数往后执行,并使信号量 减1
当信号量为0时,会一直阻塞
可以通过函数的地2个参数设置阻塞的超时时长
*/
dispatch_semaphore_wait(semaphore_t, DISPATCH_TIME_FOREVER);
NSLog(@"%d", count++);
}
Dispatch Semaphore在实际开发中主要用于以下2点:
1、保持线程同步,将异步执行的任务转换为同步执行
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger num = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
num = 666;
// 解锁
dispatch_semaphore_signal(semaphore);
});
// wait函数 加锁
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end, num = %zd", num);
dispatch_semaphore_wait
阻塞了当前线程,当dispatch_semaphore_signal
解锁后,当前线程才继续执行。此处输出num为666,如果不加锁,则输出0。
2、保证线程安全,为线程加锁
在线程安全中,可以将dispatch_semaphore_wait
看作加锁,dispatch_semaphore_signal
看作解锁。
创建全局变量:
_semaphore = dispatch_semaphore_create(1); // 注意! 初始信号量为1
_count = 0;
创建异步任务:
- (void)asyncTask {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
_count++;
sleep(1);
NSLog(@"执行任务:%d", _count);
dispatch_semaphore_signal(_semaphore);
}
异步并发调用asyncTask:
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self asyncTask];
});
}
打印结果为从1顺序执行到100,不存在两个任务同事执行的情况。
在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量为0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后,调用dispatch_semaphore_signal
将信号量加1,才能继续执行接下来的任务,以此类推,从而达到线程加锁的目的。
七、执行延时任务
GCD提供的dispatch_after()
函数可以让我们添加进队列的任务延时执行,并且这个函数本身是异步的。该函数并不是在指定时间后执行,而只是在指定时间后追加处理到dispatch_queue
。
示例如下:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"延时任务");
});
dispatch_after(dispatch_time_t when, dispatch_queue_t _Nonnull queue, ^(void)block)
函数第1个参数when
设置执行任务的时刻,第2个参数queue
设置执行任务的队列,第3个参数block
设置要执行的任务。
由于其内部使用的是dispatch_time_t
管理时间,而不是NSTimer,所以如果在子线程中调用,相比performSelector:afterDelay
,不需要关心RunLoop是否开启。
八、栅栏函数
GCD中的栅栏函数可以在并行队列中使某段逻辑独立执行。在并行队列中有需要保证线程安全的执行任务时,使用栅栏函数非常方便。
例如,某个并行任务队列专门用来处理数据的读写任务。对于读任务,可以多个任务并行执行;对于写任务,需要保证其独立性,即在执行写逻辑时,不能有读的任务以及其他写任务在执行。编写如下测试代码:
dispatch_queue_t queue = dispatch_queue_create("MyQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务1:%d", i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务2:%d", i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务3:%d", i);
}
});
运行代码,控制台输出如下:
2021-12-22 16:42:04.779427+0800 MyProject[24792:295858] 读任务2:0
2021-12-22 16:42:04.779429+0800 MyProject[24792:295870] 读任务1:0
2021-12-22 16:42:04.779432+0800 MyProject[24792:295863] 读任务3:0
2021-12-22 16:42:04.779593+0800 MyProject[24792:295870] 读任务1:1
2021-12-22 16:42:04.779601+0800 MyProject[24792:295858] 读任务2:1
2021-12-22 16:42:04.779605+0800 MyProject[24792:295863] 读任务3:1
2021-12-22 16:42:04.779753+0800 MyProject[24792:295858] 读任务2:2
2021-12-22 16:42:04.779756+0800 MyProject[24792:295870] 读任务1:2
2021-12-22 16:42:04.779767+0800 MyProject[24792:295863] 读任务3:2
2021-12-22 16:42:04.779891+0800 MyProject[24792:295858] 读任务2:3
2021-12-22 16:42:04.780036+0800 MyProject[24792:295870] 读任务1:3
2021-12-22 16:42:04.780726+0800 MyProject[24792:295863] 读任务3:3
可以看出,3个读任务是并行执行的。此时如果直接添加写任务,那么读写任务将并行执行,造成线程安全问题。使用栅栏函数的代码如下:
dispatch_queue_t queue = dispatch_queue_create("MyQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务1:%d", i);
}
});
dispatch_barrier_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"写任务1:%d", i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务2:%d", i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
NSLog(@"读任务3:%d", i);
}
});
再次运行代码,控制台输出如下:
2021-12-22 16:46:31.061717+0800 MyProject[24953:299357] 读任务1:0
2021-12-22 16:46:31.061888+0800 MyProject[24953:299357] 读任务1:1
2021-12-22 16:46:31.062022+0800 MyProject[24953:299357] 读任务1:2
2021-12-22 16:46:31.062151+0800 MyProject[24953:299357] 读任务1:3
2021-12-22 16:46:31.062302+0800 MyProject[24953:299357] 写任务1:0
2021-12-22 16:46:31.062449+0800 MyProject[24953:299357] 写任务1:1
2021-12-22 16:46:31.062600+0800 MyProject[24953:299357] 写任务1:2
2021-12-22 16:46:31.062738+0800 MyProject[24953:299357] 写任务1:3
2021-12-22 16:46:31.062992+0800 MyProject[24953:299357] 读任务2:0
2021-12-22 16:46:31.063001+0800 MyProject[24953:299361] 读任务3:0
2021-12-22 16:46:31.063780+0800 MyProject[24953:299357] 读任务2:1
2021-12-22 16:46:31.064288+0800 MyProject[24953:299361] 读任务3:1
2021-12-22 16:46:31.064834+0800 MyProject[24953:299357] 读任务2:2
2021-12-22 16:46:31.065189+0800 MyProject[24953:299361] 读任务3:2
2021-12-22 16:46:31.065551+0800 MyProject[24953:299357] 读任务2:3
2021-12-22 16:46:31.065938+0800 MyProject[24953:299361] 读任务3:3
dispatch_barrier_async()
函数就像一个栅栏,一旦使用栅栏函数添加了任务,之后再添加的并行任务会被阻塞,等待已经执行的任务执行完成后单独执行栅栏函数设置的任务,当栅栏函数设置的任务执行完成后,才会继续执行后面添加的任务,保证了栅栏函数指定任务执行的独立性和安全性。
dispatch_barrier_async()
函数与当前线程异步执行。
dispatch_barrier_sync()
函数与当前线程同步执行。
可以利用栅栏函数实现多读单写,示例如下:
- (id)readDataForKey:(NSString *)key {
__block id result;
dispatch_async(_queue, ^{
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString *)key {
dispatch_barrier_async(_queue, ^{
[self setValue:data forKey:key];
});
}
九、单例实现
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
static id instance = nil;
dispatch_once(&onceToken, ^{
// 代码只会执行一次,线程安全
instance = [[self alloc] init];
});
return instance;
}