GCD学习之函数

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 = {number = 7, name = (null)}
      2022-04-06 10:52:57.155722+0800 suanfaProject[2701:106482] >>>>>i = 1, thread = {number = 8, name = (null)}
      //执行时间为10:52:587,还是前一秒两条线程来执行任务
      2022-04-06 10:52:58.160813+0800 suanfaProject[2701:106482] >>>>>i = 2, thread = {number = 8, name = (null)}
      2022-04-06 10:52:58.160819+0800 suanfaProject[2701:106484] >>>>>i = 3, thread = {number = 7, name = (null)}
      //执行时间为10:52:59,是前一秒两条线程中的其中一天执行的任务
      2022-04-06 10:52:59.163395+0800 suanfaProject[2701:106482] >>>>>i = 4, thread = {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 {number = 6, name = (null)}
        2022-04-06 17:18:01.161346+0800 ~~1~~, thread is {number = 7, name = (null)}
        2022-04-06 17:18:01.161347+0800 ~~3~~thread is {number = 8, name = (null)}
        2022-04-06 17:18:03.166743+0800 ~~4~~thread is {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 {number = 6, name = (null)}
      2022-04-06 17:24:58.589348+0800 ~~3~~,thread is {number = 4, name = (null)}
      2022-04-06 17:24:58.589352+0800 ~~2~~,thread is {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{number = 5, name = (null)}
    2022-04-06 17:30:00.119073+0800 ~~1~~value is 1,thread is{number = 5, name = (null)}
    2022-04-06 17:30:01.124596+0800 ~~1~~value is 2,thread is{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: {number = 3, name = (null)}
       2022-04-07 09:54:56.430452+0800 suanfaProject[2626:86392] >>>>iteration = 2, current thread: {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: {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学习之函数)