手动目录
- GCD简介
- 信号量
信号量的理解
信号量的使用
信号量的代码实操
信号量阻塞哪里的任务?- 栅栏函数
dispatch_barrier_async
dispatch_barrier_sync
栅栏的实际应用- 调度组
- dispatch_source
dispatch_source_t 的计时器使用
dispatch_source_t 计时器 注意点- 延时执行
- 单利
在iOS底层-- 进程、线程、队列里理清了线程、队列等基本概念。
说了这么多,都是为了多线程线程做准备。
GCD 简介
什么是GCD
全称:Grand Central Dispatch
纯C语言,提供了非常多强大的函数
GCD优势
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
GCD是用的比较多的是用来解决多线程的问题。那么除了多线程,GCD还有那些应用?
信号量(Semaphore)
在头文件中找官方API说明
dispatch_semaphore_wait
的说明 有一个重点。
dispatch_semaphore_wai
的作用是减少信号量。如果信号量小于0,会产生等待
:( If the resulting value is less than zero)
信号量的理解
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
举例来说
以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
以上描述 引用于百度百科-信号量
简单来说 :
信号量小于0则阻塞线程,大于等于0则不会阻塞。则我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程有序进行的目的。
如果创建的信号量为1 那么就可以通过信号量的操作,进行阻塞线程,来达到同步效果。
特别提醒:
信号量<0 才会阻塞线程,>=0 任务可以顺利通过
网上有些博客/文章里面都说信号量为0就会产生阻塞,其实是错误的,在头文件的wait API说明可以看到。
信号量的使用
信号量的使用很简单,就三步
- dispatch_semaphore_create 创建一个semaphore
- dispatch_semaphore_signal 发送一个信号
- dispatch_semaphore_wait 等待信号
// 创建一个信号量 (创建一个同时可以停车50辆的停车场)
dispatch_semaphore_t sema = dispatch_semaphore_create(50);
// 等待信号 (信号总量-1) (停车场里进来一个一辆车)
// DISPATCH_TIME_FOREVER 等待时长
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// 发送一个信号(信号总量+1) (停车场出来了一辆车)
dispatch_semaphore_signal(sema);
信号量的代码实操
有这样的一段代码
- (void)task {
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
a ++;
});
}
NSLog(@"a = %d",a);
}
猜想打印结果是什么?打印的结果是否是a的最终值?
结果是:a的值>=5。
这里有2个注意点: while 类似一个堵塞线程的作用,如果a<5 ,就不可能走出while循环,所以a的值至少=5,同时 async 异步开启线程,需要耗时,所以执行一次a++的时候,可能进行了多次while循环,所以a的值大概率是 >5 小概率=5。
a的值不是最终值。
因为异步耗时,那么while可能循环了1oo次,这1oo次的任务中可能还没有执行完,就进行了打印。
那么,要如何才让其进行有序循环呢?
- 方案1、异步改同步
- (void)task {
__block int a = 0;
while (a < 5) {
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
a ++;
});
}
NSLog(@"a = %d",a);
}
- 方案2、信号量
1、创建一个信号量为1 的信号:
dispatch_semaphore_t sema = dispatch_semaphore_create(1);
2、执行一次任务之后,进行信号量 -1
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
3、一个任务结束后,发送一个信号+1
dispatch_semaphore_signal(sema);
问题在于:在哪里进行 wait,哪里进行 signal?
因为信号量是堵塞线程,起到同步的效果。所以应该在任务来了之后,就要开始阻塞,任务执行完,就要释放信号。最后的结果是:
- (void)task {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block int a = 0;
while (a < 5) {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2");
a ++;
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
NSLog(@"a = %d",a);
}
这样的话,打印的结果就是5
- 错误的使用
思考下面的信号量使用的最终结果
- (void)task {
dispatch_semaphore_t sema = dispatch_semaphore_create(1);
__block int a = 0;
while (a < 5) {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2");
a ++;
dispatch_semaphore_signal(sema);
});
}
// sleep(1);
NSLog(@"a = %d",a);
}
打印结果是
16:33:22.264515+0800 [94754:1216745] 1
16:33:22.264675+0800 [94754:1216745] 1
16:33:22.264692+0800 [94754:1216876] 2
16:33:22.264836+0800 [94754:1216745] 1
16:33:22.264855+0800 [94754:1216876] 2
16:33:22.264956+0800 [94754:1216745] 1
16:33:22.264966+0800 [94754:1216876] 2
16:33:22.265095+0800 [94754:1216745] 1
16:33:22.265110+0800 [94754:1216876] 2
16:33:22.265469+0800 [94754:1216745] 1
16:33:22.265487+0800 [94754:1216876] 2
16:33:22.266427+0800 [94754:1216745] a = 5
16:33:22.266446+0800 [94754:1216876] 2
最后的结果不是我们想要的
因为信号量<0 才会阻塞,所以 线程被阻塞的时候,已经进行了2次循环,第三次循环的时候,被阻塞在wait的位置,也就是说在 a++ 执行之前,有一个a++的任务在等待,所以在a == 5的时候,跳出了循环,但是还有一个a++任务要执行,所以最后 会先打印出 a = 5, 最后在执行一次 a++
信号量阻塞哪里的任务?
信号量阻塞的是
dispatch_semaphore_wait
所在的线程任务。1、验证子线程创建(creat),主线程阻塞(wait)
- (void)task1 {
//在主线程写一段代码,并运行
NSLog(@"1");
__block dispatch_semaphore_t s;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
s = dispatch_semaphore_create(0);
});
sleep(1);
dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
NSLog(@"3");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_signal(s);
});
}
这一段代码中,创建信号量 在子线程, wait 在 主线程,运行发现 主线程上的 NSLog(@"3")不能
被执行 。
- 2、验证主线程创建(creat),子线程阻塞(wait)
- (void)task2 {
//在主线程写一段代码,并运行
NSLog(@"1");
dispatch_semaphore_t s = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
NSLog(@"2");
});
sleep(1);
NSLog(@"3");
}
这一段代码中,创建信号量 在主线程, wait 在 子线程,运行发现 主线程上的 NSLog(@"3")能被执行 。而与wait同一线程的 NSLog(@"2") 不能被执行
信号量阻塞的任务是 wait所在线程上的任务,其他线程的任务没有影响。
更多信号量练习 可看 这里
栅栏函数
先头文件中看官方API说明:
具体内容自查
大概意思是:
- 这个API的机制是提交一个栅栏任务到一个分发队列。类似于
dispatch_async()/dispatch_sync()
栅栏任务 只有在提交到 创建的并发队列 才能起作用
。- 栅栏任务不会运行,只到在栅栏任务之前的任务全部执行完成。并且栅栏之后的任务只有在栅栏任务执行完成之后才会开始执行。
- 如果使用的不是创建的并发队列。将按照
dispatch_async()/dispatch_sync()
的方式进行执行。
dispatch_barrier_async(queue, <#^(void)block#>)
提交一个任务并异步执行
阻塞队列的任务
dispatch_barrier_sync(queue, <#^(void)block#>)
提交一个任务并同步执行
阻塞队列的任务,同时阻塞当前线程的任务(因为同步)
dispatch_barrier_async
1、同一队列的多个任务,中间加栅栏
- (void)task3 {
dispatch_queue_t t = dispatch_queue_create("queue.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_async(t, ^{
NSLog(@"栅栏-------------");
});
dispatch_async(t, ^{
NSLog(@"3");
});
NSLog(@"4");
}
// 打印结果:
18:26:46.116986+0800 [57163:441792] 4
18:26:46.116995+0800 [57163:441931] 1
18:26:47.121611+0800 [57163:441928] 2
18:26:47.122013+0800 [57163:441928] 栅栏-------------
18:26:47.122441+0800 [57163:441928] 3
从打印结果看 1、2、3 和栅栏在同一队列,但是3在栅栏之后,所以即使2的任务耗时长,也依然是要等2执行完,才能执行3。 4在主线程,和其他的不在同一队列,所以4不受影响。
2、不同队列的任务,中间加栅栏。
上面的代码 多加一个不同队列的任务5
- (void)task4 {
dispatch_queue_t t = dispatch_queue_create("queue.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t t1 = dispatch_queue_create("queue.com.test", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_async(t, ^{
NSLog(@"栅栏-------------");
});
dispatch_async(t, ^{
NSLog(@"3");
});
dispatch_async(t1, ^{ // 与上面的代码相比,多加了这个任务 与栅栏不在同一队列
NSLog(@"5");
});
NSLog(@"4");
}
// 打印结果
18:38:39.506841+0800 [57630:448693] 4
18:38:39.506901+0800 [57630:448829] 1
18:38:39.507061+0800 [57630:448982] 5
18:38:40.510435+0800 [57630:448980] 2
18:38:40.510633+0800 [57630:448980] 栅栏-------------
18:38:40.510727+0800 [57630:448980] 3
从打印结果看,5和栅栏不在同一队列,执行顺序不受栅栏影响
,5 在栅栏之前就执行完 3和栅栏在同一队列,虽然耗时比2短,但是依然需要等2执行完才开始执行。
3、 栅栏全局队列
- (void)task4 {
dispatch_queue_t t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 与1 相比,1是创建的队列,这里是全局队列
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_async(t, ^{
NSLog(@"栅栏-------------");
});
dispatch_async(t, ^{
NSLog(@"3");
});
NSLog(@"4");
}
// 打印结果
11:34:16.525421+0800 [93562:905235] 4
11:34:16.525445+0800 [93562:905321] 1
11:34:16.525581+0800 [93562:906088] 栅栏-------------
11:34:16.525541+0800 [93562:906089] 3
11:34:17.528636+0800 [93562:906087] 2
从打印结果看,如果栅栏任务是在全局队列上,表现与dispatch_async()
相同。 也就是说 非创建的队列对起不到栅栏的作用
dispatch_barrier_sync
起到栅栏的作用 ,同时同步执行任务
- (void)task3 {
dispatch_queue_t t = dispatch_queue_create("queue.com", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
sleep(1);
NSLog(@"2");
});
dispatch_barrier_sync(t, ^{
NSLog(@"栅栏-------------");
});
NSLog(@"4");
}
// 打印结果
14:20:49.578362+0800 [2888:45160] 1
14:20:50.580597+0800 [2888:45158] 2
14:20:50.580851+0800 [2888:45015] 栅栏-------------
14:20:50.580971+0800 [2888:45015] 4
从打印结果看,dispatch_barrier_sync
在阻塞相同队列的任务的同时,还在阻塞当前线程的任务。
栅栏的实际应用
对一个数组进行多次异步添加数据
- (void)task5 {
dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0 ; i < 1000; i ++) {
dispatch_async(t, ^{
[self.array addObject:@(1)];
});
}
NSLog(@"3");
}
这段代码运行是报错的,因为多个线程在同时对数组进行操作,必然会造成数据不安全。
为了避免这个情况,可以有多种操作。比如锁
dispatch_async(t, ^{
@synchronized (self) {
[self.array addObject:@(1)];
}
});
还可以用栅栏来处理
dispatch_async(t, ^{
dispatch_barrier_async(t, ^{
[self.array addObject:@"1"];
});
})
这里的执行顺序大概是这样的
-------- | -------------- | ---------- (| 表示的是栅栏任务 - 表示的是 async任务)
调度组
调度组是一个比较重要的知识点,平常开发中也有很多需要是可以用到的。面试中也经常会问
头文件API官方解释:
dispatch_group_create
此函数用于创建可与块关联的新组。调度组可以用来等待任务的完成。
调度组的内存会用 dispatch_release 自动释放掉
dispatch_group_async
将一个给定的调度块提交给一个调度组
queue : 将向其提交块以进行异步
调用的调度队列。
dispatch_group_notify
如果group里没有任务,则notify块将立即提交。
比如说: 有任务1、2、3、4 这几个任务,这几个任务无序,但是我需要在4个任务都完成之后做其他任务。就需要用到调度组。
- (void)task6 {
dispatch_queue_t t1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t t2 = dispatch_queue_create("2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t t3 = dispatch_queue_create("22", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, t1, ^{
NSLog(@" 1 thread = %@",[NSThread currentThread]);
});
dispatch_group_async(group, t2, ^{
NSLog(@" 2 thread = %@",[NSThread currentThread]);
});
dispatch_group_async(group, t3, ^{
NSLog(@" 3 thread = %@",[NSThread currentThread]);
});
//dispatch_group_notify(group, dispatch_get_main_queue(), ^{
dispatch_group_notify(group, t1, ^{
NSLog(@" 4 thread = %@",[NSThread currentThread]);
});
NSLog(@"4");
}
// 打印结果
15:49:27.125111+0800 [5927:94697] 4
15:49:27.125358+0800 [5927:107740] 2 thread = <NSThread: 0x600003d54f80>{number = 10, name = (null)}
15:49:27.125379+0800 [5927:94739] 1 thread = <NSThread: 0x600003d59c80>{number = 3, name = (null)}
15:49:27.125418+0800 [5927:107741] 3 thread = <NSThread: 0x600003d40cc0>{number = 11, name = (null)}
15:49:27.125525+0800 [5927:107741] 4 thread = <NSThread: 0x600003d40cc0>{number = 11, name = (null)}
从打印结果来看
1、dispatch_group_notify 异步执行,不阻塞线程
2、每个组所在的队列并不要求在同一队列。
3、notify 所在的队列 决定最终的回调处于那个线程。一般使用主队列。
dispatch_source
dispatch_source
的作用很多,包括响应式、计时器等,一般我们都是用来做计时器。
dispatch_source_t
的计时器使用
- 创建
1、创建dispatch_source_t 计时器
2、设置定时器时间间隔 单位:NSEC_PER_SEC--秒 NSEC_PER_MSEC--毫秒
3、监听计时器回调
4、开启计时
@property (nonatomic) dispatch_source_t timer;
// 1. 创建
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
// 2. 设置定时器的时间 单位:NSEC_PER_SEC--秒 NSEC_PER_MSEC--毫秒
//dispatch_source_set_timer(_timer,dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 0);
dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0), 1 * NSEC_PER_SEC, 0);
// 3. 监听定时器的回调
dispatch_source_set_event_handler(_timer,^{
// 将定时器的回调,传给外界
NSLog(@"thread - %@",[NSThread currentThread]);
});
// 4. 运行计时器
dispatch_resume(_timer);
- 暂停
_timer 还可以重新开启。直接调用开启(dispatch_resume(_timer);)就可以生效
dispatch_resume(_timer);
- 取消
_timer 被取消,如果还要在用,需要重新创建
,如果直接调用dispatch_resume(_timer); 会导致crash
dispatch_source_cancel(_timer);
dispatch_source_create
参数说明
- 参数1: dispatch_source_type_t type
为设置GCD源方法的类型,计时器使用 DISPATCH_SOURCE_TYPE_TIMER- 参数2: uintptr_t handle
可以看API的参数说明,没用到,传0即可- 参数3: unsigned long mask,
使用DISPATCH_TIMER_STRICT,会引起电量消耗加剧,毕竟要求精确时间,所以一般传0即可,视业务情况而定。- 参数4: dispatch_queue_t queue
定时器事件处理的Block提交到哪个队列之上。可以传Null,默认为全局队列。注意:当提交到全局队列的时候,回调将在子线程进行处理。
dispatch_source_set_timer
参数说明
- 参数1:dispatch_source_t source
这个没什么说的,就是创建的计时器- 参数2:dispatch_time_t start
当我们使用dispatch_time(DISPATCH_TIME_NOW, 0)
时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。
使用dispatch_walltime(NULL, 0)
可以让计时器按照真实时间间隔进行计时。
详细信息可以看dispatch_walltime() 与 dispatch_time() API的说明- 参数3: uint64_t interval
期望的回调间隔时间 比如1 * NSEC_PER_SEC
NSEC_PER_SEC--秒 NSEC_PER_MSEC--毫秒
如果期望只回调一次,可传入DISPATCH_TIME_FOREVER
(在dispatch_source_set_timer的API说明中 有提到此信息)。- 参数4: uint64_t leeway
允许的回调时间间隔误差。一般传入0
比如说 传入 0.5 * NSEC_PER_SEC ,表示的是:理想状态的下一次回调时间是a,那么下一次实际回调时间可能是 a~a + 0.5s
dispatch_source_t 计时器 注意点
1、循环引用问题
回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。
而且因为 Timer 是多次回调,所以系统并没有对这个block 进行 releas 操作。
正确的方法是使用weak+strong 打破循环引用
。2、计时器状态问题
dispatch_resume(_timer) 与 dispatch_suspend(_timer) 调用需要平衡
1)、未暂停的情况下, 连续调用2次 dispatch_resume 会 crash
2)、调用a次 dispatch_suspend,必须要调用 a次 dispatch_resume 才会重新生效。-
3、VC释放问题
dispatch_source_t 计时器 在挂起/未开启
的状态下,如果VC被释放,那么会导致crash。
1)、挂起(暂停)状态下不能直接cancel
(dispatch_suspend)
2)、未开启不能直接cancel
(创建了dispatch_source 但是未调用dispatch_resume(_timer)).因为并没有API可以获得timer的状态(挂起/运行)。所以需要手动添加属性去记录当前timer的状态。如果是挂起状态,可以重新运行一次再取消
⚠️:但是如果重新运行的话,会再次产生一次回调。所以这里还需要自己加属性去判断。
- (void)dealloc { NSLog(@"dealloc"); if (_isSuspend) { dispatch_resume(_timer); // 如果走到这一步,会重新产生一次回调 需要注意 } dispatch_source_cancel(_timer); _timer = nil; }
4、全局变量问题
dispatch_source_t timer
需要作为全局变量去声明。
如果是一个临时变量,那么计时器回调还没开始,tiemr就会被释放掉,就没有意义了。
正确的声明:
@interface NavSecondVC () {
// dispatch_source_t _timer;
}
@property (nonatomic) dispatch_source_t timer;
@end
- 4、线程问题
在创建计时器的时候 有一个 queue的参数 ,
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue);
- 可以传NULL,默认是在全局队列
可以传入 创建并打队列、全局队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
回调结果在子线程 - 可以传
dispatch_get_main_queue()
回调结果在主线程
延时执行
只是一个简单的API。
dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"5秒后 会执行这段代码");
});
单利
static ToolManager*_toolManager;
static dispatch_once_t onceModel;
+ (ToolManager *)standardDefault
{
dispatch_once(&onceModel, ^{
_toolManager = [[super allocWithZone:nil] init];;
});
return _toolManager;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [ToolManager standardDefault];
}
- (id)copyWithZone:(struct _NSZone *)zone {
return [ToolManager standardDefault];
}
- (id)mutableCopyWithZone:(struct _NSZone *)zone {
return [ToolManager standardDefault];
}
// 释放单利
- (void)freeStandardDetault {
_toolManager = nil;
onceModel = 0;
}