GCD学习之函数
dispatch_once一次性函数
-
该函数对于block中的任务只执行一次。
-
在iOS开发过程中,经常使用dispatch_once去创建一个单例,来保证对象的唯一性。
-
函数:
dispatch_once(dispatch_once_t * _Nonnull predicate, ^(void)block>)
-
参数1:
dispatch_once_t
类型的变量。其本质是一个长整型的别名,typedef intptr_t dispatch_once_t
。在dispatch_once的底层实现中,需要判断是否是第一次调用该方法,参数1的作用就是提供值供程序进行判断是否第一次被调用。 -
参数2: 是一个block回调。今且只有一次运行的任务,就是放在block中,在block中被执行。
-
-
方法使用,这里的例子是实现一个单例:
//使用dispatch_once制作单例对象 + (instancetype)singleton{ static GCDObject * gcdObj = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ gcdObj = [[GCDObject alloc] init]; }); return gcdObj; }
-
了解dispatch_once的保持唯一性的底层原理
dispatch_once
封装调用了dispatch_once_f
函数,它的源码如下:// 1.应用程序调用的入口 void dispatch_once(dispatch_once_t *val, dispatch_block_t block) { struct Block_basic *bb = (void *)block; // 2. 调用dispatch_once_f函数 dispatch_once_f(val, block, (void *)bb->Block_invoke); } void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) { //vval,是被volatile标记了的val,大概是告诉编译器:这个指针所指向的值,可能随时会被其他线程所改变,使编译器不再对此指针进行代码编译优化。 //vval变量有两个作用。1:标识是否已经被执行过。2:作为各个线程调用dispatch_once这个方法而形成的链表的表头 struct _dispatch_once_waiter_s * volatile *vval = (struct _dispatch_once_waiter_s**)val; // 3. 类似于简单的哨兵位。每次调用dispatch_once方法,都会生成一个dow,而这个dow就是链表的一个节点,即每次调用dispatch_once都是链表的一个节点。 struct _dispatch_once_waiter_s dow = { NULL, 0 }; // 4. 下面两个指针,表示链表的节点 //tail:用来表示,最后一个节点。在整个过程中,程序始终是把第一次调用disatch_once生成的链表节点作为链表的末尾 //tmp:是作为交换链表的指针使用。 struct _dispatch_once_waiter_s *tail, *tmp; // 5.局部变量,用于在遍历链表过程中获取每一个在链表上的更改请求的信号量 _dispatch_thread_semaphore_t sema; // 6. Compare and Swap(用于首次更改请求) //这个函数的含义应该是判断前两个字段是否相等,相等的情况下,把第三个字段赋值给第一个字段,并且返回true if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) { dispatch_atomic_acquire_barrier(); // 7.调用dispatch_once的block //这里就是要执行dispatch_once中block里面,程序员编写的任务代码 _dispatch_client_callout(ctxt, func); //在写入端,dispatch_once在执行了block之后,会调用dispatch_atomic_maximally_synchronizing_barrier() //宏函数,在intel处理器上,这个函数编译出的是cpuid指令。 dispatch_atomic_maximally_synchronizing_barrier(); //dispatch_atomic_release_barrier(); // assumed contained in above // 8. 更改请求成为DISPATCH_ONCE_DONE(原子性的操作) //这里的作用有两个: 1: 是把vval的值改完已经完成 2: 给tmp指针赋值,把vval的地址指针赋值给tmp tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE); //让tail指针指向第一次调用dispatch_once而生成的链表节点的地址 tail = &dow; // 9. 发现还有更改请求,继续遍历 //出现这种情况的原因在于,在首次调用dispatch_once过程中,如果block还没有执行完毕,其他线程也调用了dispatch_once。这种情况下,经过tmp指针的交换,vval指针是指向了最新调用dispatch_once而生成的节点地址,而最新的节点也就变成了链表的头;第一次调用dispatch_once生成的节点,就变成了链表的尾,最后一个节点。 //由于上述情况,这里tail和tmp就是不相等了,就会进入到while循环 //这个while的作用,主要在于---把其他因为调用dispatch_once而被加锁阻塞的线程,解锁 while (tail != tmp) { // 10. 如果这个时候tmp的next指针还没更新完毕,就等待一会,提示cpu减少额外处理,提升性能,节省电力。 //这种情况发生在vval指针的切换过程中 while (!tmp->dow_next) { _dispatch_hardware_pause(); } // 11. 取出当前的信号量,告诉等待者,这次更改请求完成了,轮到下一个了 sema = tmp->dow_sema; tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next; //由于非首次调用dispatch_once在等待的过程中,会被加锁。这里就是解锁的逻辑 _dispatch_thread_semaphore_signal(sema); } } else { // 12. 非首次请求,进入此逻辑块 //给新调用dispatch_once儿生成的节点中的dow_sema赋值,即添加信号量,方便后面加锁使用。 dow.dow_sema = _dispatch_get_thread_semaphore(); // 13. 遍历每一个后续请求,如果状态已经是Done,直接进行下一个 // 同时该状态检测还用于避免在后续wait之前,信号量已经发出(signal)造成的死锁 for (;;) { //把已经存在的链表的表头,赋值给tmp tmp = *vval; //如果节点中表头的值是已经完成,直接跳出无限for循环 if (tmp == DISPATCH_ONCE_DONE) { break; } dispatch_atomic_store_barrier(); // 14. 如果当前dispatch_once执行的block没有结束,那么就将这些后续请求添加到链表当中 //判断vval和tmp是否相等,相等的话,把最新一次调用dispatch_once而生成的节点地址赋值给vval指针 if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) { //拼接链表,最新节点作为表头 dow.dow_next = tmp; //为生成最新节点的线程,加锁 _dispatch_thread_semaphore_wait(dow.dow_sema); } } _dispatch_put_thread_semaphore(dow.dow_sema); } }
学习dispatch_once过程中,参考的文章:滥用单例dispatch_once而造成的死锁问题,可以更深入的了解。
dispatch_after 延时执行方法
-
延时执行方法,是指在指定时间多长的时间间隔后再去执行任务。这里需要注意的是:延时执行,本质是在指定时间后再把任务添加到队列中。
-
延时执行的方法为:
dispatch_after(dispatch_time_t when, dispatch_queue_t _Nonnull queue, ^(void)block)
-
参数1(
dispatch_time_t when
):是时间,执行任务的时间基准,在这个时间的基准上延迟多长的时间间隔去把任务添加到队列中。这个时间有两种形式的构造:-
相对时间
disatch_time
-
构造函数为
dispatch_time(dispatch_time_t when, int64_t delta)
-
参数1(
dispatch_time_t when
):是一个时间常量,取值如下-
#define DISPATCH_TIME_NOW (0ull)
: 表示从现在开始计算时间,一般使用这个常量 -
#define DISPATCH_TIME_FOREVER (~0ull)
:表示永远,这样的话,是不会执行到的
-
-
参数2(
int64_t delta
):延时的时间间隔。可取的值如下-
#define NSEC_PER_SEC 1000000000ull
// 定义一秒=10亿纳秒 -
#define USEC_PER_SEC 1000000ull
// 定义一秒=100万微妙 -
#define NSEC_PER_USEC 1000ull
// 定义一微妙=100纳秒
-
-
-
-
绝对时间
dispatch_walltime
-
构造函数为:
dispatch_walltime(const struct timespec * _Nullable when, int64_t delta)
-
dispatch_walltime
的构造函数是和dispatch_time
构造函数一样的传值hi,两者的参数没有区别-
参数1(
dispatch_time_t when
):是一个时间常量,取值如下-
如果传值为
NULL
,则表示从现在开始计算时间 -
#define DISPATCH_TIME_NOW (0ull)
: 表示从现在开始计算时间,一般使用这个常量 -
#define DISPATCH_TIME_FOREVER (~0ull)
:表示永远,这样的话,是不会执行到的
-
-
参数2(
int64_t delta
):延时的时间间隔。可取的值如下-
#define NSEC_PER_SEC 1000000000ull
// 定义一秒=10亿纳秒 -
#define USEC_PER_SEC 1000000ull
// 定义一秒=100万微妙 -
#define NSEC_PER_USEC 1000ull
// 定义一微妙=100纳秒
-
-
-
-
相对时间和绝对时间的区别在于:dispatch_time跟随设备时钟的时间;而disatch_walltime是跟随实际存在的时间。也就是说,如果设备进入休眠,那么设备的时钟也会休眠,然后dispatch_time就会停止,然而dispatch_walltime是一直在运行,disatch_walltime不会随着设备的休眠而休眠。
-
-
参数2(
dispatch_queue_t _Nonnull queue
):表示需要延时执行的任务执行的队列。 -
参数3(
^(void)block
):需要延时执行的任务。
-
-
延时执行的示例代码
- (void)delayMathod{ NSLog(@"~~~1~~~~"); // * A somewhat abstract representation of time; where zero means "now" and // * DISPATCH_TIME_FOREVER means "infinity" and every value in between is an // * opaque encodin //相对时间dispatch_time的实例化 //dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC); //绝对时间disapatch_walltime的实例化 dispatch_time_t time = dispatch_walltime(NULL, 3 * NSEC_PER_SEC); dispatch_after(time, dispatch_get_main_queue(), ^{ NSLog(@"~~~2~~~~"); }); }
dispatch_barrier_(a)sync栅栏函数
-
barrier意思为“栅栏”,其含义为在任务的前后加上栅栏,以确保提交的任务是指定队列中特定时间段内唯一在执行的任务。所有先于barrier之前的任务全部完成以后,才会执行这个栅栏里面的任务;在栅栏中的任务执行完成以后,其他后续的任务才能开始执行。
dispatch barrier.png -
分为两个类型的栅栏函数
-
同步栅栏函数
dispatch_barrier_sync
-
函数为:
dispatch_barrier_sync(dispatch_queue_t _Nonnull queue, ^(void)block)
-
参数1
dispatch_queue_t _Nonnull queue
:指定任务执行的队列 -
参数2
block
:需要在栅栏中执行的任务。 -
代码实例:
dispatch_barrier_sync(concurrentQueue, ^{ //放在栅栏函数中的任务 });
-
-
异步栅栏函数
di spatch_barrier_async
-
函数为:
dispatch_barrier_async(dispatch_queue_t _Nonnull queue, ^(void)block)
-
参数1
dispatch_queue_t _Nonnull queue
:指定任务执行的队列 -
参数2
block
:需要在栅栏中执行的任务 -
代码实例:
dispatch_barrier_async(concurrentQueue, ^{ //放在栅栏函数中的任务 });
-
-
-
-
-
两种栅栏函数的区别
-
同步栅栏函数disatch_barrier_sync会阻塞队列,后面的任务添加不到队列中,直到栅栏函数的任务执行完成,后面的函数才能添加到队列,然后执行。
-
异步栅栏函数dispatch_barrier_async回不阻塞队列,栅栏函数后面的任务都可以添加到队列中,后面的任务等待栅栏函数任务完成后,再去执行。
-
-
栅栏函数配合“异步队列+并发执行”会达到相应的效果,如果不开启线程并发执行任务的话,作用不会很大。
-
栅栏函数需要在自定义的并发队列中执行,不能使用dispatch_get_global_queue全局并发队列。如果使用dispatch_get_global_queue全局并发队列的话,其效果和dispatch_async是一样的。不能够起到栅栏的作用。
-
原因探究:详细调用过程请参考GCD源码分析(栅栏函数)
- 因为自定义的并发队列底层调用了
_dispatch_lane_wakeup
方法,其内部对栅栏函数进行了判断。判断是否为barrier
形式的,会调用_dispatch_lane_barrier_complete
方法处理。
_dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos, dispatch_wakeup_flags_t flags) { dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE; //判断栅栏 if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) { //如果是栅栏函数,走_dispatch_lane_barrier_complete方法 return _dispatch_lane_barrier_complete(dqu, qos, flags); } if (_dispatch_queue_class_probe(dqu)) { target = DISPATCH_QUEUE_WAKEUP_TARGET; } return _dispatch_queue_wakeup(dqu, qos, flags, target); }
-
而全局并发队列dispatch_get_global_queue调用的是
_dispatch_root_queue_wakeup
方法,其中并没有对barrier
的判断和处理,就是按照正常的并发队列来处理。void _dispatch_root_queue_wakeup(dispatch_queue_global_t dq, DISPATCH_UNUSED dispatch_qos_t qos,dispatch_wakeup_flags_t flags) { if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) { DISPATCH_INTERNAL_CRASH(dq->dq_priority, "Don't try to wake up or override a root queue"); } if (flags & DISPATCH_WAKEUP_CONSUME_2) { return _dispatch_release_2_tailcall(dq); } }
- 因为自定义的并发队列底层调用了
-
-
同步栅栏函数的示例以及其结果打印
- (void)syncBarrierMethod{ ///dispatch barrier 确保提交的block在制定队列中在特定时间段内,是唯一在执行的 ///在所有先于dispatch_barrier_xx之前的任务全部执行完成的情况下,这个任务才会被追加到队列中,执行。 ///在这个block执行的时候,barrier会保证当前队列不会执行其他的任务,当这个对任务完成后,队列才会恢复 //创建一个并发队列,队列只能使用自定义的并发队列 dispatch_queue_t queue = dispatch_queue_create("barrierMethod", DISPATCH_QUEUE_CONCURRENT); //创建第一个异步任务 NSLog(@"加入队列~~~~1~~~~任务1~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务1执行"); }); //创建第二个异步任务 NSLog(@"加入队列~~~~2~~~~任务2~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务2执行"); }); //使用栅栏函数创建一个异步任务 NSLog(@"栅栏加入队列~~~~3~~~~栅栏任务~~~~~~~"); dispatch_barrier_sync(queue, ^{ sleep(3); NSLog(@"<<<<<栅栏任务执行"); sleep(3); }); //创建第四个异步任务 NSLog(@"加入队列~~~~4~~~~任务4~~~~~~~"); dispatch_async(queue, ^{ NSLog(@"~>>>>>>任务4执行"); }); //创建第五个异步任务 NSLog(@"加入队列~~~~5~~~~任务5~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务5执行"); }); }
其打印结果为:
2022-04-04 14:08:01.465281+0800 suanfaProject[5006:192719] 加入队列~~~~1~~~~任务1~~~~~~~ 2022-04-04 14:08:01.465416+0800 suanfaProject[5006:192719] 加入队列~~~~2~~~~任务2~~~~~~~ 2022-04-04 14:08:01.465437+0800 suanfaProject[5006:192914] >>>>>>任务1执行 2022-04-04 14:08:01.465528+0800 suanfaProject[5006:192719] 栅栏加入队列~~~~3~~~~栅栏任务~~~~~~~ 2022-04-04 14:08:01.465538+0800 suanfaProject[5006:192914] >>>>>>任务2执行 2022-04-04 14:08:04.466671+0800 suanfaProject[5006:192719] <<<<<栅栏任务完成 //此处可以看到,任务4是栅栏函数的任务完成以后才加入到的队列,同步的栅栏函数阻塞了队列 2022-04-04 14:08:07.467960+0800 suanfaProject[5006:192719] 加入队列~~~~4~~~~任务4~~~~~~~ 2022-04-04 14:08:07.468484+0800 suanfaProject[5006:192719] 加入队列~~~~5~~~~任务5~~~~~~~ 2022-04-04 14:08:07.468529+0800 suanfaProject[5006:192911] ~>>>>>>任务4执行 2022-04-04 14:08:07.469240+0800 suanfaProject[5006:192911] >>>>>>任务5执行
-
异步栅栏函数的代码示例机器结果打印
//唯一的区别在把disatch_barrier_sync换成了dispatch_barrier_async - (void)asyncBarrierMethod{ ///dispatch barrier 确保提交的block在制定队列中在特定时间段内,是唯一在执行的 ///在所有先于dispatch_barrier_xx之前的任务全部执行完成的情况下,这个任务才会被追加到队列中,执行。 ///在这个block执行的时候,barrier会保证当前队列不会执行其他的任务,当这个对任务完成后,队列才会恢复 //创建一个并发队列,队列只能使用自定义的并发队列 dispatch_queue_t queue = dispatch_queue_create("barrierMethod", DISPATCH_QUEUE_CONCURRENT); //创建第一个异步任务 NSLog(@"加入队列~~~~1~~~~任务1~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务1执行"); }); //创建第二个异步任务 NSLog(@"加入队列~~~~2~~~~任务2~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务2执行"); }); //使用栅栏函数创建一个同步栅栏任务 NSLog(@"栅栏加入队列~~~~3~~~~栅栏任务~~~~~~~"); dispatch_barrier_async(queue, ^{ sleep(3); NSLog(@"<<<<<栅栏任务完成"); sleep(3); }); //创建第四个异步任务 NSLog(@"加入队列~~~~4~~~~任务4~~~~~~~"); dispatch_async(queue, ^{ NSLog(@"~>>>>>>任务4执行"); }); //创建第五个异步任务 NSLog(@"加入队列~~~~5~~~~任务5~~~~~~~"); dispatch_async(queue, ^{ NSLog(@">>>>>>任务5执行"); }); }
其打印结果为:
//由此打印结果,也可以看出,栅栏任务并没有阻塞队列。在栅栏队列执行之前,全部的任务都已经加入到了队列中。 2022-04-04 14:14:09.453628+0800 suanfaProject[5080:196683] 加入队列~~~~1~~~~任务1~~~~~~~ 2022-04-04 14:14:09.453835+0800 suanfaProject[5080:196683] 加入队列~~~~2~~~~任务2~~~~~~~ 2022-04-04 14:14:09.453844+0800 suanfaProject[5080:196790] >>>>>>任务1执行 2022-04-04 14:14:09.453949+0800 suanfaProject[5080:196683] 栅栏加入队列~~~~3~~~~栅栏任务~~~~~~~ 2022-04-04 14:14:09.453964+0800 suanfaProject[5080:196790] >>>>>>任务2执行 2022-04-04 14:14:09.454033+0800 suanfaProject[5080:196683] 加入队列~~~~4~~~~任务4~~~~~~~ 2022-04-04 14:14:09.454113+0800 suanfaProject[5080:196683] 加入队列~~~~5~~~~任务5~~~~~~~ 2022-04-04 14:14:12.459164+0800 suanfaProject[5080:196790] <<<<<栅栏任务完成 2022-04-04 14:14:15.460267+0800 suanfaProject[5080:196790] ~>>>>>>任务4执行 2022-04-04 14:14:15.460284+0800 suanfaProject[5080:196787] >>>>>>任务5执行
-
dispatch semaphore 信号量
-
信号量是基于mach内核的信号量接口实现,基于计数器的一种多线程同步机制,用来管理对资源的并发访问。
-
信号量内部有一个可以原子递增或递减的值(dsema_value)。如果一个动作尝试减少信号量的值,使其小于0,就会阻塞当前线程,直到有其他调用者(在其他线程中)增加该信号量的值。
-
信号量为0则阻塞线程,大于0则不会阻塞。则我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。
-
信号量的方法只有3个,使用比较简单。
-
使用一个初始信号量的值(dsema_value)创建信号量:
dispatch_semaphore_create(intptr_t value)
- 参数
intptr_t value
:代表信号量的值dsema_value,为一个整形数据。
- 参数
-
让信号量的值(dsema_value)原子性减一:
intptr_t dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
-
该函数的作用是,让信号量的值减1,当信号量值小于0时会等待(直到超时),否则正常执行;
-
参数1
dispatch_semaphore_t dsema
: 需要处理值减1的信号量 -
参数2
dispatch_time_t timeout
: 由dispatch_time_t
类型值指定的超时时间。取值如下:-
DISPATCH_TIME_NOW
:若desma_value
小于0,对其加一并返回超时信号KERN_OPERATION_TIMED_OUT,原子性加一是为了抵消dispatch_semaphore_wait
函数开始的减一操作。 -
DISPATCH_TIME_FOREVER
:调用系统的semaphore_wait
方法,直到收到signal
调用。
-
-
-
将dsema_value调用原子方法加1:
intptr_t dispatch_semaphore_signal(dispatch_semaphore_t dsema);
-
将dsema_value调用原子方法加1,如果大于零就立即返回0;如果原值小于0,就唤醒在
dispatch_semaphore_wait
中等待的线程 -
参数
dispatch_semaphore_t dsema
:执行加一操作的信号量
-
-
-
信号量的使用方法
//初始化信号量 _lock = dispatch_semaphore_create(1); //设置信号量减一操作,信号量的值小于0的话,阻塞线程 dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); //处理相关的任务 xxxx //设置信号量加一操作 dispatch_semaphore_signal(_lock);
-
信号量的主要应用于两个方面:保持线程同步和线程锁。
- 线程同步示例,使用信号量进行并发控制:
//控制线程并发数 - (void)concurrentThreadLimit{ //创建一个初始值为2的信号量,限制只能创建2条并发线程来处理任务 dispatch_semaphore_t dsema = dispatch_semaphore_create(2); //获取全局并发队列 dispatch_queue_t globalQueue = dispatch_queue_create("concurrentThreadLimit", DISPATCH_QUEUE_CONCURRENT); //模拟多个任务 for (int i=0; i<5; i++) { //加上限制,执行dsema_value的减一操作,如果小于0,阻塞,并等待dsemo_value大于等于0的信号 dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); //并发执行任务 dispatch_async(globalQueue, ^{ NSLog(@">>>>>i = %d, thread = %@",i,[NSThread currentThread]); sleep(1); //任务完成后,执行信号量的加一操作,通知被阻塞的信号量,正常执行 dispatch_semaphore_signal(dsema); xxxxxxx }); } }
结果打印如下:
//执行时间为10:52:57,并且开了两条线程来执行任务 2022-04-06 10:52:57.155721+0800 suanfaProject[2701:106484] >>>>>i = 0, thread = <NSThread: 0x600001dcc2c0>{number = 7, name = (null)} 2022-04-06 10:52:57.155722+0800 suanfaProject[2701:106482] >>>>>i = 1, thread = <NSThread: 0x600001d88bc0>{number = 8, name = (null)} //执行时间为10:52:587,还是前一秒两条线程来执行任务 2022-04-06 10:52:58.160813+0800 suanfaProject[2701:106482] >>>>>i = 2, thread = <NSThread: 0x600001d88bc0>{number = 8, name = (null)} 2022-04-06 10:52:58.160819+0800 suanfaProject[2701:106484] >>>>>i = 3, thread = <NSThread: 0x600001dcc2c0>{number = 7, name = (null)} //执行时间为10:52:59,是前一秒两条线程中的其中一天执行的任务 2022-04-06 10:52:59.163395+0800 suanfaProject[2701:106482] >>>>>i = 4, thread = <NSThread: 0x600001d88bc0>{number = 8, name = (null)}
由打印结果也可以看出,信号量确实是限制了执行任务的并发线程数。
- 作为互斥线程锁,保护线程安全,示例如下:
- (void)semaphoreLock{ //设置票数 __block int ticketCount = 5; //创建信号量,初始值为1;小于0的时候,会堵塞线程,起到锁的作用 dispatch_semaphore_t semephoreLock = dispatch_semaphore_create(1); //创建一个任务 for (int i=0; i<5; i++) { //先执行减一操作,dsema_value为0,阻塞线程 dispatch_semaphore_wait(semephoreLock, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ticketCount -= 1; NSLog(@"当前票数 = %d",ticketCount); //操作完成任务后,执行加1操作,dsema_value为1,放开线程,继续执行后面的任务 dispatch_semaphore_signal(semephoreLock); }); } }
打印结果为:
2022-04-06 11:22:19.194357+0800 suanfaProject[3204:130741] 当前票数 = 4 2022-04-06 11:22:19.194591+0800 suanfaProject[3204:130741] 当前票数 = 3 2022-04-06 11:22:19.194733+0800 suanfaProject[3204:130741] 当前票数 = 2 2022-04-06 11:22:19.194825+0800 suanfaProject[3204:130741] 当前票数 = 1 2022-04-06 11:22:19.194967+0800 suanfaProject[3204:130741] 当前票数 = 0
即互斥锁是信号量为1或0时候的一种特例。
-
注意点是,如果信号量销毁的时候,信号量还在使用,会导致程序的崩溃。这是因为在销毁信号量的时候会调用
_dispatch_semaphore_dispose
方法,可以参考下面发吗。该方法有对信号量的判断,如果判断是否,就会crash。有两种情况会导致出现crash,一种是重新为信号量赋值,另一种就是信号量置为nil。//释放信号量的函数 void _dispatch_semaphore_dispose(dispatch_object_t dou) { dispatch_semaphore_t dsema = dou._dsema; if (dsema->dsema_value < dsema->dsema_orig) { //Warning:信号量还在使用的时候销毁会造成崩溃 DISPATCH_CLIENT_CRASH( "Semaphore/group object deallocated while in use"); } kern_return_t kr; if (dsema->dsema_port) { kr = semaphore_destroy(mach_task_self(), dsema->dsema_port); DISPATCH_SEMAPHORE_VERIFY_KR(kr); } }
信号量会导致崩溃的示例:
dispatch_semaphore_t semephore = dispatch_semaphore_create(1); dispatch_semaphore_wait(semephore, DISPATCH_TIME_FOREVER); //重新赋值或者将semephore = nil都会造成崩溃,因为此时信号量还在使用中 semephore = dispatch_semaphore_create(0);
disatch_group 调度组
-
dispatch_group的作用就是把一组任务提交到队列中,这些队列可以不相关,然后监听这组任务完成的事件。学习文章:深入理解GCD之dispatch_group
-
常用的方法:
-
dispatch_group_create()
:创建一个任务调度组。查看其源码,可以看出,该方法是创建了一个初始值为
LONG_MAX
的信号量并返回。所以dispatch_group_t本质是一个信号量dispatch_group_t dispatch_group_create(void) { //dispatch_semaphore_create(LONG_MAX)创建一个初始值为LONG_MAX的信号量 return (dispatch_group_t)dispatch_semaphore_create(LONG_MAX); }
-
dispatch_group_async(dispatch_group_t _Nonnull group,dispatch_queue_t _Nonnull queue, ^(void)block)
:把一个任务异步提交到任务组里。这也是把任务添加到任务组的两种方式之一。-
参数1
group
:需要执行的任务调度组 -
参数2
queue
:执行任务的队列 -
参数3
block
:需要执行的任务 -
这个方法可以自己独立完成任务的分组的功能,需要配合其他方法。
-
该方法本质是
dispatch_group_async_f
的封装,代码如下:void dispatch_group_async_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt, dispatch_function_t func) { dispatch_continuation_t dc; _dispatch_retain(dg); //dispatch_group_async函数中调用的也是dispatch_group_enter方法 dispatch_group_enter(dg); dc = fastpath(_dispatch_continuation_alloc_cacheonly()); if (!dc) { dc = _dispatch_continuation_alloc_from_heap(); } dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_GROUP_BIT); dc->dc_func = func; dc->dc_ctxt = ctxt; dc->dc_group = dg; // No fastpath/slowpath hint because we simply don't know if (dq->dq_width != 1 && dq->do_targetq) { return _dispatch_async_f2(dq, dc); } //由于上面调用了dispatch_group_enter方法,必定会有dispatch_group_leave方法,以达到信号量的平衡 //_dispatch_queue_push是调用了dispatch_group_leave方法的 _dispatch_queue_push(dq, dc); }
-
-
-
把任务添加到调度组两种方式中的第二种
//加入调度组 dispatch_group_enter(dispatch_group_t _Nonnull group); //移除调度组 dispatch_group_leave(dispatch_group_t _Nonnull group);
-
这两个函数的实现,本质上还是信号量方法的使用。
-
dispatch_group_enter
方法,有下面的代码可以说明dispatch_group_enter就是对dispatch_semaphore_wait的封装void dispatch_group_enter(dispatch_group_t dg) { //获取到信号量,这里指的就是调度组 dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg; // 调用dispatch_semaphore_wait,该方法在信号量小于0的时候,阻塞线程 (void)dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); }
-
dispatch_group_leave
:该方法将dispatch_group_t
转换成dispatch_semaphore_t
后将dsema_value
的值原子性加1。如果value
为LONG_MIN
程序crash;如果value
等于dsema_orig
表示所有任务已完成,调用_dispatch_group_wake
唤醒group。void dispatch_group_leave(dispatch_group_t dg) { dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg; dispatch_atomic_release_barrier(); long value = dispatch_atomic_inc2o(dsema, dsema_value);//dsema_value原子性加1 if (slowpath(value == LONG_MIN)) {//内存溢出,由于dispatch_group_leave在dispatch_group_enter之前调用 DISPATCH_CLIENT_CRASH("Unbalanced call to dispatch_group_leave()"); } if (slowpath(value == dsema->dsema_orig)) {//表示所有任务已经完成,唤醒group (void)_dispatch_group_wake(dsema); } }
-
-
在信号量的学习中,说到了一种崩溃情况,是由于信号量不平衡导致的。所以dispatch_group_leave与dispatch_group_enter必须要配对使用,以保持信号量的平衡。
-
dispatch_group_leave与dispatch_group_enter配对使用
-
-
监听调度组的任务全部执行完成
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block);
-
参数1
group
:需要监听任务完成的调度组 -
参数2
queue
:监听到任务完成后,接下来要处理的任务所在的队列 -
参数3
block
:监听到任务完成后,接下来要做的处理任务
-
-
调度组的使用示例:
-
方式一,使用
dispatch_group_async
管理相关调度组任务- (void)dispatchGroupAsync{ //创建调度组 dispatch_group_t group = dispatch_group_create(); //创建队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //dispatch_group_async包裹任务 dispatch_group_async(group, queue, ^{ NSLog(@"~~1~~, thread is %@",[NSThread currentThread]); }); //创建第二个任务 dispatch_group_async(group, queue, ^{ NSLog(@"~~2~~,thread is %@",[NSThread currentThread]); }); //创建第三个任务 dispatch_group_async(group, queue, ^{ NSLog(@"~~3~~thread is %@",[NSThread currentThread]); sleep(2); NSLog(@"~~4~~thread is %@",[NSThread currentThread]); }); NSLog(@"group is done ?"); //调用dispatch_group_notify //监听这个调度组的任务全部完成,并且通知主队列,可以去做block的任务 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"group is done"); }); }
结果打印如下:
2022-04-06 17:18:01.161235+0800 group is done ? 2022-04-06 17:18:01.161323+0800 ~~2~~,thread is <NSThread: 0x60000222ae40>{number = 6, name = (null)} 2022-04-06 17:18:01.161346+0800 ~~1~~, thread is <NSThread:0x600002220e40>{number = 7, name = (null)} 2022-04-06 17:18:01.161347+0800 ~~3~~thread is <NSThread: 0x60000220b1c0>{number = 8, name = (null)} 2022-04-06 17:18:03.166743+0800 ~~4~~thread is <NSThread: 0x60000220b1c0>{number = 8, name = (null)} 2022-04-06 17:18:03.167258+0800 group is done
-
方式一,使用
dispatch_group_enter
/dispatch_group_leave
组合来管理相关调度组任务
- (void)dispatchGroupEnterAndLeave{ //创建调度组 dispatch_group_t group = dispatch_group_create(); //创建队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //管理第一个任务 dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"~~1~~, thread is %@",[NSThread currentThread]); dispatch_group_leave(group); }); //创建第二个任务 dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"~~2~~, thread is %@",[NSThread currentThread]); dispatch_group_leave(group); }); //创建第三个任务 dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"~~3~~, thread is %@",[NSThread currentThread]); dispatch_group_leave(group); }); NSLog(@"group is done ?"); //调用dispatch_group_notify //监听这个调度组的任务全部完成,并且通知主队列,可以去做block的任务 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"group is done"); }); }
结果打印如下:
2022-04-06 17:24:58.589212+0800 group is done ? 2022-04-06 17:24:58.589342+0800 ~~1~~,thread is <NSThread: 0x600000432580>{number = 6, name = (null)} 2022-04-06 17:24:58.589348+0800 ~~3~~,thread is <NSThread: 0x60000047d2c0>{number = 4, name = (null)} 2022-04-06 17:24:58.589352+0800 ~~2~~,thread is <NSThread: 0x600000470f80>{number = 5, name = (null)} 2022-04-06 17:24:58.675289+0800 group is done
-
-
注意点,如果
dispatch_group_async
中嵌套使用异步执行和并发队列的时候,dispatch_group_notify
是不会监测到调度组任务的完成的。示例如下
- (void)dispatchGroupHaveAsyncTask{ //创建调度组 dispatch_group_t group = dispatch_group_create(); //创建队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建第一个任务,这次创建异步任务,使用dispatch_group_async包裹 dispatch_group_async(group, queue, ^{ //创建同步任务 dispatch_async(queue, ^{ for (int i=0; i<3; i++) { sleep(1); NSLog(@"~~1~~ value is %d,thread is %@",i,[NSThread currentThread]); } }); }); NSLog(@"group is done ?????"); //使用dispatch_group_notify, dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"group is done"); }); }
结果打印为:
2022-04-06 17:29:58.111502+0800 group is done ????? 2022-04-06 17:29:58.138346+0800 group is done 2022-04-06 17:29:59.116521+0800 ~~1~~value is 0,thread is<NSThread: 0x6000036d0080>{number = 5, name = (null)} 2022-04-06 17:30:00.119073+0800 ~~1~~value is 1,thread is<NSThread: 0x6000036d0080>{number = 5, name = (null)} 2022-04-06 17:30:01.124596+0800 ~~1~~value is 2,thread is<NSThread: 0x6000036d0080>{number = 5, name = (null)}
dispatch_apply 进行快速迭代
-
GCD中进行快速迭代的方法,类似于for方法,但是和for方法也有区别
-
如果运行队列是串行队列(serial queue)的话,是和for循环是一样的效果.
- 任务会在主队列运行。
-
如果运行队列是并发队列(concurrent queue),会并发的执行block的任务。正是由于可以并发执行任务,所以其运行速度会更快。
- 在并发队列的情况下,因为GCD会管理并发避免出现过多的线程,所以dispatch_apply比for更安全。
-
需要注意的地方是,执行快速迭代的任务不能运行在主队列(main queue),会造成死锁
-
-
初始化方法:
-
void dispatch_apply(size_t iterations, dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue, DISPATCH_NOESCAPE void (^block)(size_t iteration))
-
参数1
iterations
:需要快速迭代的次数 -
参数2
queue
:任务执行的队列,是串行队列还是并发执行。不能是主队列。 -
参数3
block
:需要快速迭代的任务
-
-
-
代码实例:
-
运行队列为串行队列的情况:
- (void)dispatchApplySerial{ //创建队列 dispatch_queue_t queue = dispatch_queue_create("concurrentQueueAndSync", DISPATCH_QUEUE_SERIAL); dispatch_apply(5, queue, ^(size_t iteration) { NSLog(@">>>>iteration = %zu, current thread: %@",iteration,[NSThread currentThread]); }); }
结果打印:
2022-07-12 09:29:27.376573+0800 schemeUse[1627:35710] >>>>iteration = 0, current thread: <_NSMainThread: 0x6000014782c0>{number = 1, name = main} 2022-07-12 09:29:27.376651+0800 schemeUse[1627:35710] >>>>iteration = 1, current thread: <_NSMainThread: 0x6000014782c0>{number = 1, name = main} 2022-07-12 09:29:27.376697+0800 schemeUse[1627:35710] >>>>iteration = 2, current thread: <_NSMainThread: 0x6000014782c0>{number = 1, name = main} 2022-07-12 09:29:27.376740+0800 schemeUse[1627:35710] >>>>iteration = 3, current thread: <_NSMainThread: 0x6000014782c0>{number = 1, name = main} 2022-07-12 09:29:27.376773+0800 schemeUse[1627:35710] >>>>iteration = 4, current thread: <_NSMainThread: 0x6000014782c0>{number = 1, name = main}
-
运行队列为并发队列的情况:
- (void)dispatchApplyConcurrent{ //创建队列 dispatch_queue_t queue = dispatch_queue_create("concurrentQueueAndSync", DISPATCH_QUEUE_CONCURRENT); dispatch_apply(5, queue, ^(size_t iteration) { NSLog(@">>>>iteration = %zu, current thread: %@",iteration,[NSThread currentThread]); }); }
结果打印:
2022-04-07 09:54:56.430338+0800 suanfaProject[2626:86197] >>>>iteration = 0, current thread: <_NSMainThread: 0x6000038fc600>{number = 1, name = main} 2022-04-07 09:54:56.430430+0800 suanfaProject[2626:86394] >>>>iteration = 1, current thread: <NSThread: 0x6000038f8840>{number = 3, name = (null)} 2022-04-07 09:54:56.430452+0800 suanfaProject[2626:86392] >>>>iteration = 2, current thread: <NSThread: 0x6000038bd500>{number = 4, name = (null)} 2022-04-07 09:54:56.430480+0800 suanfaProject[2626:86197] >>>>iteration = 3, current thread: <_NSMainThread: 0x6000038fc600>{number = 1, name = main} 2022-04-07 09:54:56.430516+0800 suanfaProject[2626:86394] >>>>iteration = 4, current thread: <NSThread: 0x6000038f8840>{number = 3, name = (null)}
-
dispatch_source调度源
-
Dispatch Source调度源是协调特殊低级别系统事件处理的基本数据类型。也就是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后在指定的队列中可以做自定义的逻辑处理。
-
调度源有多种类型,分别监听对应类型的系统事件,诸如定时器调度源、信号调度源、描述符调度源、进程调度源、端口调度源、自定义调度源等。
-
该方法涉及的内容较多,我这里只拿出来常用的自定义定时器来作为例子。后面有时间有精力的话,在去研究研究相关用法和原理
-
NSTimer和dispatch_source_t的区别
-
NSTimer受 RunLoop 的影响, 由于 RunLoop 需要处理很多任务,所以其精度不高。
-
如果我们对定时器的精度要求很高,可以考虑使用 dispatch_source 去实现。它精度很高,系统会自动触发,系统级别的源,并且不受RunLoopMode的影响。
-
-
dispatch_source监听定时器调度源时相关的方法
-
创建调度源,时间源是调度源的一种:
dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, uintptr_t mask, dispatch_queue_t _Nullable queue);
-
参数1
dispatch_source_type_t type
:生命调度源的类型。这里就是用定时器调度源饿类型DISPATCH_SOURCE_TYPE_TIMER
-
参数2
uintptr_t handle
:可以理解为句柄。参数1调度源类型的参数决定该值。如果是定时器调度源的时候,传0即可。 -
参数3
uintptr_t mask
: 提供更详细的描述,让它知道具体要监听什么。 也是调度源类型决定的。传0即可 -
参数4
dispatch_queue_t queue
:调度源监听到事件后,在该值提供的队列中执行任务。
-
-
设置时间源信息
void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway);
-
参数1
dispatch_source_t source
:调度源 -
参数2
dispatch_time_t start
:控制计时器第一次触发的时刻。是dispatch_time_t
类型的参数,可以参考上面dispatch_after
部分关于该类型的描述-
dispatch_time
:相对时间 -
dispatch_walltime
:绝对时间,让计时器按照真实时间间隔进行计时。
-
-
参数3
uint64_t interval
:时间间隔,隔多久执行调用一次任务执行。 -
参数4
uint64_t leeway
:计时器触发的精准程度,期望的容忍时间。将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
-
-
监听到调度源的事件后的回调,在block处理相关任务
void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);
-
参数1
dispatch_source_t source
:调度源 -
参数2
dispatch_block_t _Nullable handler
:事件处理
-
-
取消调度源的监听
void dispatch_cancel(void *object);
- 参数
void *object
:调度源对象
- 参数
-
取消监听调度源的事件回调
void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);
-
参数1
dispatch_source_t source
:监听的调度源,该调度源的监听被取消后,回调这个方法。 -
参数2
dispatch_block_t _Nullable handler
:取消监听调度源的回调,可以在该回调中处理相关任务
-
-
启动调度源
void dispatch_resume(dispatch_object_t object);
-
刚创建好的Dispatch Source是处于暂停状态的,所以使用时需要用
dispatch_resume
函数将其启动。 -
参数
dispatch_object_t object
:调度源对象
-
-
吊起调度源
void dispatch_suspend(dispatch_object_t object);
-
暂停对调度源的监听
-
参数
dispatch_object_t object
:调度源对象
-
-
-
使用定时器调度源实现一个timer,使用以上的方法基本满足要求。代码实例如下:
@interface GCDTimer(){ dispatch_source_t _dispatch_source_timer; } @end @implementation GCDTimer - (void)dispatch_source_timer{ //创建一个变量,记录基数 __block NSInteger value = 0; //获取全局队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建源 dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); if (_timer) { //创建startTimer dispatch_time_t startTimer = dispatch_time(DISPATCH_TIME_NOW, 0 & NSEC_PER_SEC); //设置时间源信息 dispatch_source_set_timer(_timer, startTimer, 1 * NSEC_PER_SEC, 0); //监听事件,处理相关逻辑 dispatch_source_set_event_handler(_timer, ^{ if (value > 10) { dispatch_cancel(_dispatch_source_timer); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"我在主线程,浪的嗨起"); }); sleep(3); }else if(value == 5){ dispatch_suspend(_timer); sleep(3); NSLog(@"开始继续执行吧"); value += 1; dispatch_resume(_timer); }else{ value += 1; NSLog(@"我的value is %ld,therad is %@", (long)value,[NSThread currentThread]); } }); //取消事件的监听 dispatch_source_set_cancel_handler(_timer, ^{ NSLog(@"我这是被取消了么"); }); //开启source,开始执行dispatch 源 dispatch_resume(_timer); } _dispatch_source_timer = _timer; }
-