GCD使用·记录

一、开端

队列与任务创建

  • dispatch_queue_t 自定义GCD队列,区分串行队列与并行队列
  • dispatch_async(queue, block) 执行异步任务
  • dispatch_sync(queue, block) 执行同步任务

GCD常用方法

  • dispatch_barrier_async(queue, block) 分割执行异步任务块
  • dispatch_group_t 队列组,分组执行异步/同步任务
  • dispatch_semaphore_t 信号量,通常用来保证线程安全,或保持线程同步

GCD其他方法

  • dispatch_after(dispatch_time_t, queue, block) 指定时间之后执行队列中的任务
  • dispatch_once(&dispatch_once_t, block) 保证任务只被执行一次,同时也能保证线程安全
  • dispatch_apply(count, queue, block) 快速迭代队列任务,不论并行/串行队列,都是逐个遍历任务来操作,类似同步操作

二、详述

前面对于常用的GCD方法做了一个简要的展示,对于详细的使用情况,这里一一来展开说明。

概念

先说说基本的任务和队列:
任务 就是最基本的执行单元,在线程和队列中,任务执行被分为异步执行同步执行

  • 同步执行:
    任务被添加到指定队列后,按顺序执行完当前任务后才会继续执行其他任务,在此之前会等待任务执行结束。此外,只能在当前线程中执行任务,不具备开启新线程能力。

  • 异步任务:
    任务被添加到指定队列后,不会立即处理该任务,不做等待,继续执行后续任务。此外,可以在新线程中执行任务,具备开启线程的能力。

队列 相当于一个容器,用来存放和调度任务的,任务的同步、异步执行都是需要基于其所在的队列属性,队列的不同,任务所具备开启线程的能力也就不同;队列分为串行队列并行队列

  • 串行队列:
    每次只有一个任务被执行。所有在此队列中的任务,都是一个接一个的执行(基于同步、异步执行规则)。此外,在此队列中只会开启一个线程来执行其所有的任务。

  • 并行队列:
    同时可以执行多个任务,执行顺序由队列(系统)调度(基于同步、异步执行规则)。此外,在此队列中可以同时开启多个线程,同时处理多个任务,线程数量的上限基于系统限制。

1. 入口

  • 队列的建立

    //并行队列
    dispatch_queue_t queue =   dispatch_queue_create("queue.concurrent",DISPATCH_QUEUE_CONCURRENT);
    //串行队列
    dispatch_queue_t queue = dispatch_queue_create("queue.serial", DISPATCH_QUEUE_SERIAL);
    

    上面是最直接的队列创建方法,在GCD中有另外2种特殊的队列,不能手动创建,只能直接获取:

    主队列:

    dispatch_get_main_queue();
    

    全局队列:

     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
  • 创建任务

    1. 同步任务
    //queue为并行/串行队列,创建/获取方式参考上面队列部分
    dispatch_sync(queue, ^{
          NSLog(@"do sync task here");
    });
    
    1. 异步任务
    //queue为并行/串行队列,创建/获取方式参考上面队列部分
    dispatch_async(queue, ^{
          NSLog(@"do async task here");
    });
    
  • 任务与队列关系

    区别 并行队列 串行队列 主队列
    同步任务 不开启新线程,串行执行任务 不开启新线程,串行执行任务 主线程调用:触发死锁
    其他线程调用:与普通串行队列同步任务情况相同
    异步任务 开启新线程,并行执行任务 只开启一条新线程,串行执行任务 没有开启新线程,串行执行任务

代码此处不再赘述,网上有很多此类说明,具体可以参考这篇文章,相当全面:iOS多线程:『GCD』详尽总结

2. 扩展

GCD的基本用法之外,还有许多常用的方法,在本文的开头已经列举出来了,下面简述一下,作为记录参考。


  • dispatch_barrier_async
    GCD栅栏方法,用于分割上下两块任务操作,每块都可以包含多个异步任务操作。被分割的2块任务组可以看做为2个同步执行的任务组,只有当第一块任务全部执行完毕后,才会开始第二块任务执行。此方法用于并行异步任务处理中,同步任务处理没有意义。

    dispatch_queue_t queue = dispatch_queue_create("queue.serial", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
          NSLog(@"task1 --->%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
          NSLog(@"task2 --->%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
          NSLog(@"barrier --->%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
          NSLog(@"task3 --->%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
          NSLog(@"task4 --->%@",[NSThread currentThread]);
    });
    

    执行结果:

    task2 --->{number = 1, name = main}
    task1 --->{number = 3, name = (null)}
    barrier --->{number = 4, name = (null)}
    task4 --->{number = 1, name = main}
    task3 --->{number = 4, name = (null)}
    

  • dispatch_group
    GCD队列组,此方法常用于耗时任务组的等待操作,有点类似dispatch_barrier_async方法,等待一大块的任务执行完毕后才继续执行后续队列任务。

    • 2种调用方式:
      1. 通过dispatch_group_async将任务所在队列放到队列组中,接着通过dispatch_group_notify来回到指定的线程执行任务。
      2. 通过 dispatch_group_enterdispatch_group_leave组合来操作任务所在队列进入/离开队列组,接着使用 dispatch_group_notify 来回到指定线程执行任务。

    此处等待组队列任务执行完成的方法还有一种:dispatch_group_wait,与dispatch_group_notify有区别。dispatch_group_wait用于阻塞当前线程,等待指定group中的所有队列任务执行完成后,才会继续执行dispatch_group_wait后面的任务,其作用与dispatch_group_notify一致。

  • dispatch_group_async

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
      
    void(^groupBlock)(NSString *taskName) = ^(NSString* taskName)
    {
        for (int i = 0; i<2; i++)
        {
            [NSThread sleepForTimeInterval:2.];
            NSLog(@"%@ --> %@",taskName,[NSThread currentThread]);
        }
    };
      
    dispatch_group_async(group, queue, ^{
        groupBlock(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        groupBlock(@"task2");
    });
      
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        groupBlock(@"end group task");
    });
    
    /* 
    * 此方法效果与dispatch_group_notify一致,用于等待group中队列任务执行完毕后,继续执行其后的其他任务
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    groupBlock(@"end group task");
    */
    
  • dispatch_group_enterdispatch_group_leave

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
      
    void(^groupBlock)(NSString *taskName, BOOL groupTask) = ^(NSString* taskName, BOOL groupTask)
    {
        for (int i = 0; i<2; i++)
        {
            [NSThread sleepForTimeInterval:2.];
            NSLog(@"%@ --> %@",taskName,[NSThread currentThread]);
        }
        if (groupTask)
        {
            //如果为group任务,则离开group队列
            dispatch_group_leave(group);
        }
    };
      
    dispatch_group_enter(group);//进入group队列
    dispatch_group_async(group, queue, ^{
        groupBlock(@"task1",YES);
    });
      
    dispatch_group_enter(group);//进入group队列
    dispatch_group_async(group, queue, ^{
        groupBlock(@"task2",YES);
    });
    
    //等待上面的任务全部完成后,会继续往下执行,在此之前,此处阻塞了线程
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    groupBlock(@"end group task",NO);
    

    以上展示了2种执行组队列任务的方式,分别用了dispatch_group_notifydispatch_group_wait来等待组队列执行完毕。


  • dispatch_semaphore_t
    GCD信号量,通过操作信号量的增减,可以达到线程操作安全的目的。

    此方式提供了3个函数方法:

    • dispatch_semaphore_create 创建并初始化信号量
    • dispatch_semaphore_signal 发送一个信号,信号量增加1
    • dispatch_semaphore_wait 减少1个信号量,当信号量小于0时,将会阻塞所在线程,否则继续执行(注:为0时依旧继续执行)

    此方式常用于:

    • 保持线程同步,将异步任务转换为同步执行任务
    • 保证线程安全,为线程加锁

    线程同步

    __block int num = 0;
      
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_queue_t queue = dispatch_queue_create("queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
          num = 100;
          dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"num = %d",num);
    

    输出结果:

    num = 100
    

    可以看出,原本异步执行的任务,却在主线程输出任务之前执行了,说明在输出num之前,线程处于阻塞状态。

    此处如果将创建时的信号量改为1,则无法达到同步线程目的,异步执行的任务依旧在主线程输出num值之后执行。

    线程安全

    __block NSInteger saleNum = 0;
    NSInteger maxCount = 100;
      
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_queue_t sale_queue1 = dispatch_queue_create("sale1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t sale_queue2 = dispatch_queue_create("sale2", DISPATCH_QUEUE_CONCURRENT);
      
    void(^saleBlock)(void) = ^()
    {
        while (saleNum %@",saleNum,[NSThread currentThread]);
                [NSThread sleepForTimeInterval:0.2];
            }
            else
            {
                NSLog(@"已售完 -> %@",[NSThread currentThread]);
                dispatch_semaphore_signal(semaphore);//解锁
                break;
            }
              
            dispatch_semaphore_signal(semaphore);//解锁
        }
    };
      
    //售货员1
    dispatch_async(sale_queue1, ^{
        saleBlock();
    });
      
    //售货员2
    dispatch_async(sale_queue2, ^{
        saleBlock();
    });
    

    输出结果太长,不在此处展示,最后得到的输出顺序是按照常规递增方式来展现的,即 1,2,3,....,100,已售完。其只使用了1个信号的增量,来控制库存加法的异步任务,在同一时间只能由一个线程执行,这样就保证了该库存数据的准确性。

    这里的主要思路是:总信号量为1,在进入执行加法任务前,先通过dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) 方法减少一个信号量,使总信号量为0,保证当前线程无阻塞可以继续执行,如果在此同时另外一条线程插入进来开始访问此任务,那么信号量将继续减少(因为第二条线程也会走一次dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)方法),变为负数,则该线程阻塞,这时,只能先等待第一条线程执行完此加法任务后,通过dispatch_semaphore_signal(semaphore)方法增加一个信号量,解锁第二条线程阻塞的情况,同时第二条线程将继续执行加法,如此循环下去。


  • 其他方法
    • dispatch_after
      GCD延时执行方法,可以指定多久后执行某个任务,执行此方法后,在指定时间之后才会将任务追加到队列中,并不是到指定时间后才开始执行任务,所以指定的执行时间并不是绝对准确的。

      NSLog(@"task0 --> %@", [NSThread currentThread]);
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
          NSLog(@"task1 --> %@", [NSThread currentThread]);
      });
      

      打印结果很明显能看到在task0之后,延迟了一段时间才执行了task1

    • dispatch_once
      GCD只会也只能执行一次该任务的方法,常用语单例创建中,在整个程序运行过程中只会执行一次。

      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
          //此处执行单例创建方法
          NSLog(@"此段代码只会执行一次!");
      });
      

      此处结果不是很容易能看出来,如果放到一个类的创建方法中,多次执行就能很容易看到实际执行的次数只有一次。

    • dispatch_apply
      GCD中快速迭代方法,有点类似dispatch_group_wait,会等待dispatch_apply中的全部任务执行完毕。

      dispatch_queue_t queue = dispatch_queue_create("queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
      dispatch_apply(5, queue, ^(size_t i) {
          NSLog(@"current index:%zd --> %@",i,[NSThread currentThread]);
      });
      NSLog(@"end task %@",[NSThread currentThread]);//最后才会输出此处代码
      

      输出结果为正常的串行/并行队列中同步/异步任务调用顺序,只不过需要等待 dispatch_apply 执行完毕后才会执行后续任务。

三、延伸思考

  • 在串行、并行队列中,同步、异步执行任务时,如果涉及到嵌套操作,那么其执行的顺序以及开启的线程状态与数量都有什么样的结果呢?

  • 死锁触发有几种情况?

    1. 主队列中执行同步任务:
    dispatch_sync(dispatch_get_main_queue(), ^{
          NSLog(@"do something here");
    });
    

    虽然此任务是新开的一个同步任务,处于主队列中,但是实际上是嵌套在另一个主队列同步任务中(当前正在执行的任务中),当调用dispatch_sync方法时,会将此block加入到主队列尾部,等待主队列中的任务(当前正在执行的任务)执行完毕返回后,才会继续执行block中的任务。

    根据规则,串行队列同步执行任务会阻塞当前线程,直到该任务执行完毕,此处当前线程为主线程,那么调用block中的任务时主线程会被阻塞,意味着主队列中的当前任务不能继续执行,而block中的任务必须等待主队列中的当前任务执行完毕才能继续执行,进而形成了一个相互等待状态,线程就发生了死锁。

    1. 同一个串行队列中嵌套执行同步任务
    dispatch_queue_t queue = dispatch_queue_create("queue.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
          NSLog(@"task1 --->%@",[NSThread currentThread]);
          dispatch_sync(queue, ^{
              NSLog(@"task2 --->%@",[NSThread currentThread]);
          });
    });
    

    此操作将会卡在task2输出之前:

    task1 --->{number = 1, name = main}
    (lldb) //卡死
    

    此处所处情况与主队列中执行同步任务情况相同,只不过更加具体和明显。

    此处task1没有被卡死,是因为队列queue中没有其他任务正在执行,那么task1任务加入到队列queue后直接被执行;当执行到第二个dispatch_sync方法时,会将task2任务追加到队列queue尾部,此时task1任务实际上并没有执行完毕,但是因为调用了task2任务,那么此处task1任务所在线程将会阻塞等待task2任务执行完毕,但是由于task1任务并未执行返回结果,导致task2任务在此处同样处于等待状态。如此一来,2个任务相互等待对方执行完毕,直接导致死锁。

    以上需要注意的是,所有触发死锁的同步任务都处于同一个串行队列中,异步任务在添加任务后不会等待任执行完毕,而是继续往下执行,所以无法触相互等待状态就不会发生死锁状态。

你可能感兴趣的:(GCD使用·记录)