美文网首页
GCD学习之函数

GCD学习之函数

作者: 心中有光啊 | 来源:发表于2022-04-07 10:47 被阅读0次

    dispatch_once一次性函数

    1. 该函数对于block中的任务只执行一次。

    2. 在iOS开发过程中,经常使用dispatch_once去创建一个单例,来保证对象的唯一性。

    3. 函数: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中被执行。

    4. 方法使用,这里的例子是实现一个单例:

      //使用dispatch_once制作单例对象
      + (instancetype)singleton{
       static GCDObject * gcdObj = nil;
       static dispatch_once_t onceToken;
       dispatch_once(&onceToken, ^{
       gcdObj = [[GCDObject alloc] init];
       });
       return gcdObj;
      }
      
    5. 了解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.jpg

    学习dispatch_once过程中,参考的文章:滥用单例dispatch_once而造成的死锁问题,可以更深入的了解。

    dispatch_after 延时执行方法

    1. 延时执行方法,是指在指定时间多长的时间间隔后再去执行任务。这里需要注意的是:延时执行,本质是在指定时间后再把任务添加到队列中。

    2. 延时执行的方法为: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):需要延时执行的任务。

    3. 延时执行的示例代码

      - (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栅栏函数

      1. barrier意思为“栅栏”,其含义为在任务的前后加上栅栏,以确保提交的任务是指定队列中特定时间段内唯一在执行的任务。所有先于barrier之前的任务全部完成以后,才会执行这个栅栏里面的任务;在栅栏中的任务执行完成以后,其他后续的任务才能开始执行。

        dispatch barrier.png
      2. 分为两个类型的栅栏函数

        • 同步栅栏函数dispatch_barrier_sync

          • 函数为:dispatch_barrier_sync(dispatch_queue_t _Nonnull queue, ^(void)block)

            • 参数1dispatch_queue_t _Nonnull queue:指定任务执行的队列

            • 参数2block:需要在栅栏中执行的任务。

            • 代码实例:

              dispatch_barrier_sync(concurrentQueue, ^{
                 //放在栅栏函数中的任务
              
              });
              
          • 异步栅栏函数di spatch_barrier_async

            • 函数为:dispatch_barrier_async(dispatch_queue_t _Nonnull queue, ^(void)block)

              • 参数1dispatch_queue_t _Nonnull queue:指定任务执行的队列

              • 参数2block:需要在栅栏中执行的任务

              • 代码实例:

              dispatch_barrier_async(concurrentQueue, ^{
                //放在栅栏函数中的任务
              
              });
              
      3. 两种栅栏函数的区别

        • 同步栅栏函数disatch_barrier_sync会阻塞队列,后面的任务添加不到队列中,直到栅栏函数的任务执行完成,后面的函数才能添加到队列,然后执行。

        • 异步栅栏函数dispatch_barrier_async回不阻塞队列,栅栏函数后面的任务都可以添加到队列中,后面的任务等待栅栏函数任务完成后,再去执行。

      4. 栅栏函数配合“异步队列+并发执行”会达到相应的效果,如果不开启线程并发执行任务的话,作用不会很大。

      5. 栅栏函数需要在自定义的并发队列中执行,不能使用dispatch_get_global_queue全局并发队列。如果使用dispatch_get_global_queue全局并发队列的话,其效果和dispatch_async是一样的。不能够起到栅栏的作用。

        1. 原因探究:详细调用过程请参考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);
               }
            }
            
      6. 同步栅栏函数的示例以及其结果打印

        - (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执行
        
      7. 异步栅栏函数的代码示例机器结果打印

        //唯一的区别在把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 信号量

    1. 信号量是基于mach内核的信号量接口实现,基于计数器的一种多线程同步机制,用来管理对资源的并发访问。

    2. 信号量内部有一个可以原子递增或递减的值(dsema_value)。如果一个动作尝试减少信号量的值,使其小于0,就会阻塞当前线程,直到有其他调用者(在其他线程中)增加该信号量的值。

    3. 信号量为0则阻塞线程,大于0则不会阻塞。则我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。

    4. 信号量的方法只有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时会等待(直到超时),否则正常执行;

        • 参数1dispatch_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:执行加一操作的信号量

    5. 信号量的使用方法

       //初始化信号量
       _lock = dispatch_semaphore_create(1); 
       //设置信号量减一操作,信号量的值小于0的话,阻塞线程
       dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 
      
       //处理相关的任务
       xxxx
      
       //设置信号量加一操作
       dispatch_semaphore_signal(_lock);
      
    6. 信号量的主要应用于两个方面:保持线程同步和线程锁。

      1. 线程同步示例,使用信号量进行并发控制:
        //控制线程并发数
        - (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)}
      

      由打印结果也可以看出,信号量确实是限制了执行任务的并发线程数。

      1. 作为互斥线程锁,保护线程安全,示例如下:
        - (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时候的一种特例。

    7. 注意点是,如果信号量销毁的时候,信号量还在使用,会导致程序的崩溃。这是因为在销毁信号量的时候会调用_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 调度组

    1. dispatch_group的作用就是把一组任务提交到队列中,这些队列可以不相关,然后监听这组任务完成的事件。学习文章:深入理解GCD之dispatch_group

    2. 常用的方法:

      1. 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);
        }
        
      2. dispatch_group_async(dispatch_group_t _Nonnull group,dispatch_queue_t _Nonnull queue, ^(void)block):把一个任务异步提交到任务组里。这也是把任务添加到任务组的两种方式之一。

        • 参数1group:需要执行的任务调度组

        • 参数2queue:执行任务的队列

        • 参数3block:需要执行的任务

        • 这个方法可以自己独立完成任务的分组的功能,需要配合其他方法。

          • 该方法本质是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);
            }
            
      3. 把任务添加到调度组两种方式中的第二种

        //加入调度组
        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。如果valueLONG_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配对使用

      4. 监听调度组的任务全部执行完成void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block);

        1. 参数1group:需要监听任务完成的调度组

        2. 参数2queue:监听到任务完成后,接下来要处理的任务所在的队列

        3. 参数3block:监听到任务完成后,接下来要做的处理任务

      5. 调度组的使用示例:

        1. 方式一,使用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
          
        2. 方式一,使用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
        
      6. 注意点,如果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 进行快速迭代

      1. GCD中进行快速迭代的方法,类似于for方法,但是和for方法也有区别

        • 如果运行队列是串行队列(serial queue)的话,是和for循环是一样的效果.

          • 任务会在主队列运行。
        • 如果运行队列是并发队列(concurrent queue),会并发的执行block的任务。正是由于可以并发执行任务,所以其运行速度会更快。

          • 在并发队列的情况下,因为GCD会管理并发避免出现过多的线程,所以dispatch_apply比for更安全。
        • 需要注意的地方是,执行快速迭代的任务不能运行在主队列(main queue),会造成死锁

      2. 初始化方法:

        • void dispatch_apply(size_t iterations, dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue, DISPATCH_NOESCAPE void (^block)(size_t iteration))

          • 参数1iterations:需要快速迭代的次数

          • 参数2queue:任务执行的队列,是串行队列还是并发执行。不能是主队列。

          • 参数3block:需要快速迭代的任务

      3. 代码实例:

        • 运行队列为串行队列的情况:

          - (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调度源

      1. Dispatch Source调度源是协调特殊低级别系统事件处理的基本数据类型。也就是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后在指定的队列中可以做自定义的逻辑处理。

      2. 调度源有多种类型,分别监听对应类型的系统事件,诸如定时器调度源、信号调度源、描述符调度源、进程调度源、端口调度源、自定义调度源等。

      3. 该方法涉及的内容较多,我这里只拿出来常用的自定义定时器来作为例子。后面有时间有精力的话,在去研究研究相关用法和原理

      4. NSTimer和dispatch_source_t的区别

        • NSTimer受 RunLoop 的影响, 由于 RunLoop 需要处理很多任务,所以其精度不高。

        • 如果我们对定时器的精度要求很高,可以考虑使用 dispatch_source 去实现。它精度很高,系统会自动触发,系统级别的源,并且不受RunLoopMode的影响。

      5. dispatch_source监听定时器调度源时相关的方法

        1. 创建调度源,时间源是调度源的一种:

          dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, uintptr_t mask, dispatch_queue_t _Nullable queue);
          
          • 参数1dispatch_source_type_t type:生命调度源的类型。这里就是用定时器调度源饿类型DISPATCH_SOURCE_TYPE_TIMER

          • 参数2uintptr_t handle:可以理解为句柄。参数1调度源类型的参数决定该值。如果是定时器调度源的时候,传0即可。

          • 参数3uintptr_t mask: 提供更详细的描述,让它知道具体要监听什么。 也是调度源类型决定的。传0即可

          • 参数4dispatch_queue_t queue:调度源监听到事件后,在该值提供的队列中执行任务。

        2. 设置时间源信息

          void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start,  uint64_t interval,  uint64_t leeway);
          
          • 参数1dispatch_source_t source:调度源

          • 参数2dispatch_time_t start:控制计时器第一次触发的时刻。是dispatch_time_t类型的参数,可以参考上面dispatch_after部分关于该类型的描述

            • dispatch_time:相对时间

            • dispatch_walltime:绝对时间,让计时器按照真实时间间隔进行计时。

          • 参数3uint64_t interval:时间间隔,隔多久执行调用一次任务执行。

          • 参数4uint64_t leeway:计时器触发的精准程度,期望的容忍时间。将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。

        3. 监听到调度源的事件后的回调,在block处理相关任务

          void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);
          
          • 参数1dispatch_source_t source:调度源

          • 参数2dispatch_block_t _Nullable handler:事件处理

        4. 取消调度源的监听

          void dispatch_cancel(void *object);
          
          • 参数void *object:调度源对象
        5. 取消监听调度源的事件回调

          void dispatch_source_set_cancel_handler(dispatch_source_t source,  dispatch_block_t _Nullable handler);
          
          • 参数1dispatch_source_t source:监听的调度源,该调度源的监听被取消后,回调这个方法。

          • 参数2dispatch_block_t _Nullable handler:取消监听调度源的回调,可以在该回调中处理相关任务

        6. 启动调度源void dispatch_resume(dispatch_object_t object);

          1. 刚创建好的Dispatch Source是处于暂停状态的,所以使用时需要用dispatch_resume函数将其启动。

          2. 参数dispatch_object_t object:调度源对象

        7. 吊起调度源void dispatch_suspend(dispatch_object_t object);

          1. 暂停对调度源的监听

          2. 参数dispatch_object_t object:调度源对象

      6. 使用定时器调度源实现一个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;
        }
        

    相关文章

      网友评论

          本文标题:GCD学习之函数

          本文链接:https://www.haomeiwen.com/subject/ubaesrtx.html