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 ---><NSThread: 0x60400007c800>{number = 1, name = main}
    task1 ---><NSThread: 0x600000466700>{number = 3, name = (null)}
    barrier ---><NSThread: 0x604000467880>{number = 4, name = (null)}
    task4 ---><NSThread: 0x60400007c800>{number = 1, name = main}
    task3 ---><NSThread: 0x604000467880>{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<maxCount)
        {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//加锁
            if (saleNum<maxCount)
            {
                saleNum++;
                NSLog(@"已售出:%ld -> %@",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 ---><NSThread: 0x604000079dc0>{number = 1, name = main}
    (lldb) //卡死
    

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容