GCD全程Grand Central Dispath,是苹果提供的一套多核并行运算的解决方案,GCD使用纯C语言的API,提供了非常强大的API,它会自动利用更多的CPU内核(比如双核、四核),自动管理线程的生命周期(创建线程、调度线程、销毁线程),我们只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。
函数、队列与任务
GCD常见的用法
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"GCD");
});
可以看到GCD的使用分为3个部分,函数dispatch_sync
、队列dispatch_get_global_queue(0, 0)
,以及任务block。
函数
在GCD中可以将执行函数分为同步和异步两种
- 同步函数:
dispatch_sync
,必须等待当前任务block执行完毕后才能执行下一语句,同步函数不会开启线程,会在当前线程中执行任务 - 异步函数:
dispatch_async
,不用等待当前任务block执行完毕就可以执行下一语句,可以开启线程执行线程
队列
GCD队列是任务的等待队列,遵循先进先出(FIFO)原则,没调度一个任务就从队列中释放一个任务,队列有两种:串行队列和并发队列,两者的主要区别为执行顺序不同,开启线程数不同
串行队列:每次只能有一个任务执行,任务一个接着一个执行,一个任务执行完毕后,在执行下一个任务
dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_SERIAL);
并发队列:可以让多个任务并发执行,可以开启多想线程
dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_CONCURRENT);
并发队列的并发功能只有在异步函数下才有效
#define DISPATCH_QUEUE_SERIAL NULL
,所以dispatch_queue_create("", NULL)
得到的是同步队列
主队列
GCD提供了一种特殊的串行队列--主队列,所有放在队列中的任务都会放到主队列中执行
全局并发队列
GCD默认提供了全局并发队列,他需要提供两个参数,第一个是队列优先级,通常用DISPATCH_QUEUE_PRIORITY_DEFAULT
,在iOS9之后,已经被服务质量代替,如QOS_CLASS_DEFAULT
,第二个参数是苹果的保留字段,一般写0.
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
在使用多线程开发时,如果对队列没有特殊需求,可以直接使用全局并发队列
函数与队列
根据函数与队列的两两组合,会出现出现以下四种结果
同步函数并发队列:
- 不会开启线程,在当前线程执行任务
- 任务串行执行,任务一个接着一个
- 可能会产生堵塞
同步函数并发队列:
- 不会开启线程,在当前线程执行任务
- 任务一个接着一个
异步函数串行队列:
- 开启一条新线程
- 任务一个接着一个
异步函数并发队列:
- 开启多个线程
- 异步执行任务,没有顺序,与CPU调度有关
GCD的其他方法
除了开启线程的能力,GCD还提供了许多api,我们可以利用这些api实现单例、多读单写、计时器等功能
栅栏函数 dispatch_barrier_async
如果我们需要异步执行两组操作,而且第一组操作执行完成之后才能执行第二组操作,我们就需要一个栅栏一样的东西将这两组操作分割开来,而GCD的dispatch_barrier_async
便可以实现着这一功能。
dispatch_barrier_async
会等待前边追加到并发队列的任务全部执行完毕后,再将指定的任务追加到该异步队列中,然后在dispatch_barrier_async
追加的任务执行完毕之后,异步队列才恢复为正常执行。
例如:
dispatch_queue_t queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1");
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"2");
}
});
dispatch_barrier_async(queue, ^{
for (int i = 0; i < 2; i++) {
NSLog(@"barrier");
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"3");
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"4");
}
});
执行结果为
2020-11-25 16:05:29.664252+0800 GCDTest[91450:4599536] 1
2020-11-25 16:05:29.664294+0800 GCDTest[91450:4599535] 2
2020-11-25 16:05:31.668346+0800 GCDTest[91450:4599535] 2
2020-11-25 16:05:31.668442+0800 GCDTest[91450:4599536] 1
2020-11-25 16:05:31.668615+0800 GCDTest[91450:4599536] barrier
2020-11-25 16:05:31.668735+0800 GCDTest[91450:4599536] barrier
2020-11-25 16:05:33.670656+0800 GCDTest[91450:4599536] 3
2020-11-25 16:05:33.670617+0800 GCDTest[91450:4599535] 4
2020-11-25 16:05:35.675116+0800 GCDTest[91450:4599535] 4
2020-11-25 16:05:35.675144+0800 GCDTest[91450:4599536] 3
利用这一特性,我们可以使用栅栏函数实现多读单写。
- (void)setName:(NSString *)name {
dispatch_barrier_async(self.queue, ^{
self->_name = [name copy];
});
}
- (NSString *)name {
__block NSString *tempName;
dispatch_sync(self.queue, ^{
tempName = self->_name;
});
return tempName;
}
- (dispatch_queue_t)queue {
if (!_queue) {
_queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
}
return _queue;
}
队列组 dispatch_group
有时候我们会有这样的需求,分别执行两个异步耗时任务,然后当两个耗时任务都执行完毕后再回到主线程执行任务,这时候可以用到GCD的队列组。
GCD的队列组有几个关键的api
-
dispatch_group_create
创建队列组 -
dispatch_group_enter
加入队列组 -
dispatch_group_leave
离开队列组 -
dispatch_group_async
将任务加入队列组 -
dispatch_group_notify
回到指定队列执行任务 -
dispatch_group_wait
同上,但会阻塞当前线程
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"3");
});
执行结果:
2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517558] 2
2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517552] 1
2020-11-26 17:33:44.591338+0800 GCDTest[6468:5517552] 3
GCD一次性代码 dispatch_once
我们在创建单例、或者有整个运行过程中只执行一次的代码时,我们可以使用GCD的dispatch_once
实现,即使在多线程的环境,dispatch_once
也可以保证线程安全。
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的代码
});
}
GCD计时器 dispatch_source
dispatch source 是一种处理事件的数据类型,这些被处理的事件为操作系统中的底层级别,而计时器类型是其支持的其中一种类型。
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"source");
});
dispatch_resume(_timer);
-
dispatch_source_create
:创建source,计时器类型使用DISPATCH_SOURCE_TYPE_TIMER
,而且可以指定队列 -
dispatch_source_set_timer
:设置定时器的相关参数,第一个参数为定时器,第二个为开始时间,第三个为回调时间间隔,第四个允许误差范围 -
dispatch_source_set_event_handler
:设置事件处理句柄,一个句柄可以是一个block或者是一个函数,dispatch source会把句柄投放到队列中执行。 -
dispatch_resume
:timer默认是挂起的,需要手动开启
和NSTimer
相比,GCD的timer不需要依赖runloop,不会因为runloop的繁忙而导致及时不准。
GCD快速迭代方法 dispatch_apply
和for循环类似,dispatch_apply
可以快速循环遍历。而相比于普通的for循环,dispatch_apply
按照指定次数将任务追加到指定的队列中,并等待全部队列执行结束。
如果在串行队列中使用dispatch_apply
,那么就和for循环一样按照顺序同步执行,没有快速迭代的意义,我们可以利用并发队列队形进行异步执行,让dispatch_apply
的任务多个线程中不是异步执行,并且无论在串行队列还是异步队列,dispatch_apply
都会等到全部任务执行完成,有点类似dispatch_group_wait
方法。
可以用下面的例子清楚了解:
NSLog(@"start");
dispatch_apply(10, queue, ^(size_t i) {
NSLog(@"%zd-----%@", i, [NSThread currentThread]);
});
NSLog(@"end");
执行结果为:
2020-11-27 15:35:23.341807+0800 GCDTest[44897:5974416] start
2020-11-27 15:35:23.341930+0800 GCDTest[44897:5974416] 0-----{number = 1, name = main}
2020-11-27 15:35:23.341998+0800 GCDTest[44897:5974416] 2-----{number = 1, name = main}
2020-11-27 15:35:23.342015+0800 GCDTest[44897:5974466] 1-----{number = 2, name = (null)}
2020-11-27 15:35:23.342048+0800 GCDTest[44897:5974416] 3-----{number = 1, name = main}
2020-11-27 15:35:23.342096+0800 GCDTest[44897:5974416] 5-----{number = 1, name = main}
2020-11-27 15:35:23.342093+0800 GCDTest[44897:5974466] 4-----{number = 2, name = (null)}
2020-11-27 15:35:23.342180+0800 GCDTest[44897:5974466] 7-----{number = 2, name = (null)}
2020-11-27 15:35:23.342188+0800 GCDTest[44897:5974416] 6-----{number = 1, name = main}
2020-11-27 15:35:23.342316+0800 GCDTest[44897:5974466] 8-----{number = 2, name = (null)}
2020-11-27 15:35:23.342346+0800 GCDTest[44897:5974464] 9-----{number = 3, name = (null)}
2020-11-27 15:35:23.342849+0800 GCDTest[44897:5974416] end
GCD信号量 dispatch_semaphore
GCD中的信号量指的是dispatch_semaphore
,是持有计数的信号,类似高速路收费站的栏杆,可以通过时,打开栏杆,不可以通过时,关闭栏杆。在dispatch_semaphore
中,使用计数来完成这个功能,计数为0时等待,不可通过,计数为1或大于1时,计数减1且不等待,可通过。
dispatch_semaphore
提供了三个函数。
-
dispatch_semaphore_create
:创建一个Semaphore并初始化信号的总量 -
dispatch_semaphore_signal
:发送一个信号,让信号总量加1 -
dispatch_semaphore_wait
:可以使总信号量减1,当信号量为0时就会一直等待(阻塞所有线程),否则可以正常执行
基于dispatch_semaphore
的这一性质,它常常被用来作为锁来实现线程同步,如下
NSLog(@"current thread: %@", [NSThread currentThread]);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int num = 0;
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"current thread: %@", [NSThread currentThread]);
num = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"num = %d", num);
如果没有加入dispatch_semaphore
,那么NSLog(@"num = %d", num);
便不会等待num = 100;
执行完成,而是直接打印当前数值0,而加入dispatch_semaphore_t
后,开始semaphore
为0,执行到
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
时,因为semaphore
为0,所以下面的代码不会执行,而子线程中的任务执行num = 100;
完成后,会调用一遍dispatch_semaphore_signal(semaphore);
将semaphore
加一变为1,此时会通知到dispatch_semaphore
,从而继续往下执行NSLog(@"num = %d", num);
。