多线程-GCD

前言

GCD是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的。


GCD是一种轻量的基于block的线程模型,底层实现主要有Dispatch Queue和Dispatch Source
Dispatch Queue :管理block(操作)
Dispatch Source :处理事件
Dispatch Queue&Dispatch Source更多相关知识

GCD推出来以后,开发者可以不直接操纵线程,而是将所要执行的任务封装成一个unit丢给线程池去处理,线程池会有效管理线程的并发,控制线程的生死。因此,现在如果考虑到并发场景,基本上是围绕着GCD和NSOperationQueue来展开讨论。

任务和队列

这里就不得不提到两个概念:任务和队列

  • 任务:即操作,就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。调度队列执行任务有两种方式: 同步执行 和 异步执行.

同步派发(sync) 和 异步派发(async) 的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕!

【异步并不一定会开启多线程,当在主线程中派发任务到主队列后,会等待主线程空闲时才会调度该任务并没有开启新的线程;添加到其他线程时,会开启新的线程调度任务。】
如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。

  • 队列:调度队列是一个类似对象的结构体,它管理您提交给它的任务。所有的调度队列都是先进先出的数据结构。队列和线程的区别,他们之间并没有“拥有关系(ownership)”。队列用于存放任务。一共有两种队列, 串行队列 和 并行队列

放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。

放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。

GCD中不同队列中不同任务的执行情况如下表:

同步执行的任务 异步执行的任务
串行队列中 当前线程,一个一个执行 其他线程,一个一个执行
并行队列中 当前线程,一个一个执行 同时开很多线程,一起执行

队列的创建和执行
#开辟队列的方法:
dispatch_queue_t myQueue = dispatch_queue_create("MyQueue", NULL);

  参数1:标签,用于区分队列
  参数2:队列的类型,表示这个队列是串行队列还是并发队列NUll表示串行队列,
  DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。       
  DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。   

 #执行队列的方法
  异步执行
  dispatch_async(<#dispatch_queue_t queue#>, <#^(void)block#>)

   同步执行
  dispatch_sync(<#dispatch_queue_t queue#>, <#^(void)block#>)

几个特别的队列

  • 主队列(Main Queue):专门负责调度主线程(Main Thread)的任务,是一个串行队列,没有办法开辟新的线程。任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。

这里需要特别说一下:主队列和主线程的关系
(1)主队列是专门负责调度主线程的任务的。iOS编程中,需要在主线程(主线程,这个线程是其他线程的父线程)中进行操作时,我们经常会用到以下代码:
dispatch_async(dispatch_get_main_queue(), ^{
// dispatch_get_main_queue() 实际获取的是 主队列
});

而且在主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行。比如block内的任务是异步执行,主线程在将当前方法执行完毕之后,才会去继续执行主队列里的任务。
(2)主队列的任务一定在主线程中执行,主线程是可以执行主队列之外(dispatch_get_global_queue)其他队列的任务的。
另外关于主线程中更新UI操作也不是绝对安全的,详细请看这篇文章:主线程中也不绝对安全的 UI 操作

  • 全局队列:本质是一个并发队列,由系统提供,是所有应用程序共享的。方便编程,可以不用创建就直接使用。

     #获取全局队列的方法:
     dispatch_get_global_queue(long indentifier.unsigned long flags)
    第一个参数:线程优先级,默认写0就行,不要使用系统提供的枚举类型,因为ios7和ios8的枚举数值不一样,使用数字可以通用。
    第二个参数:标记参数,目前没有用,一般传入0.
    
    #使用全局队列多线程执行任务
     dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     #创建多个线程用于填充图片
     for (int i=0; i<count; ++i) {
         #异步执行队列任务
         dispatch_async(globalQueue, ^{
             [self loadImage:[NSNumber numberWithInt:i]];
         });
     }
    

队列组

队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。这是一个很实用的功能。

# 创建队列组
dispatch_group_t group = dispatch_group_create();
# 获取到全局队列
dispatch_queue_t queue =   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

# 多次使用队列组的方法执行任务, 目前API中只有异步方法
//1.执行3次循环
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 3; i++) {
        NSLog(@"group-01 - %@", [NSThread currentThread]);
    }
});

//2.主队列执行8次循环
dispatch_group_async(group, dispatch_get_main_queue(), ^{
    for (NSInteger i = 0; i < 8; i++) {
        NSLog(@"group-02 - %@", [NSThread currentThread]);
    }
});

//3.执行5次循环
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 5; i++) {
        NSLog(@"group-03 - %@", [NSThread currentThread]);
    }
});

#.都完成后会自动通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"完成 - %@", [NSThread currentThread]);
});

队列组其他实用方法
这也是使用GCD信号量实现多线程同步加锁的实现方式。

dispatch_group_enter

用于添加对应任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数加1,当未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞和dispatch_group_notify的block执行

void dispatch_group_enter(dispatch_group_t group);

dispatch_group_leave

用于减少任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数减1,dispatch_group_enter和dispatch_group_leave要匹配,不然系统会认为group任务没有执行完毕

void dispatch_group_leave(dispatch_group_t group);

dispatch_group_wait

等待组任务完成,会阻塞当前线程,当任务组执行完毕时,才会解除阻塞当前线程

long dispatch_group_wait(dispatch_group_t group, 
                     dispatch_time_t timeout);
***********************************
group   ——需要等待的任务组
timeout ——等待的超时时间(即等多久),单位为dispatch_time_t。
如果设置为DISPATCH_TIME_FOREVER,则会一直等待(阻塞当前线程),直到任务组执行完毕。

信号量

当我们在处理一系列线程的时候,当数量达到一定量,在以前我们可能会选择使用NSOperationQueue来处理并发控制,但如何在GCD中快速的控制并发呢?答案就是
dispatch_semaphore.

信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。

在GCD中有三个函数是semaphore的操作,分别是:

dispatch_semaphore_create   创建一个semaphore
dispatch_semaphore_signal   发送一个信号
dispatch_semaphore_wait    等待信号
 
第一个函数有一个整形的参数,我们可以理解为信号的总量;
dispatch_semaphore_signal是发送一个信号,自然会让信号总量+1,
dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,

特别说明下:信号总量为0时dispatch_semaphore_wait会阻塞当前的线程(主线程、其他线程),被阻塞的线程中无法执行任何代码。
根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。

使用GCD的信号量实现并发的控制

创建了一个初使值为10的semaphore,每一次for循环都会创建一个新的线程,线程结束的时候会发送一个信号,线程创建之前会信号等待,所以当同时创建了10个线程之后,for循环就会阻塞,等待有线程结束之后会增加一个信号才继续执行,如此就形成了对并发的控制,如上就是一个并发数为10的一个线程队列。

GCD执行任务的其他一些常用方法

#重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。 
dispatch_apply():
#单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。 
dispatch_once():
#延迟一定的时间后执行。 
dispatch_time():
#使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等待此方法中任务执行后才能执行。(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用dispatch_async()添加其他图片加载任务)
dispatch_barrier_async(): 
#实现对任务分组管理,如果一组任务全部完成可以通过
dispatch_group_async(): 
#方法获得完成通知(需要定义dispatch_group_t作为分组标识)。       
dispatch_group_notify()

一个栗子:多个并发网络请求完成后执行下一步

  • 1.使用GCD的dispatch_group_t
    -(void)Btn2{
    
      NSString *str = @"http://www.jianshu.com/p/6930f335adba";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_group_t downloadGroup = dispatch_group_create();
    for (int i=0; i<10; i++) {
      dispatch_group_enter(downloadGroup);
      NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
          NSLog(@"%d---%d",i,i);
          dispatch_group_leave(downloadGroup);
      }];
      [task resume];
     }
    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
            NSLog(@"end");
      });
     }
    

    创建一个dispatch_group_t, 每次网络请求前先dispatch_group_enter,请求回调后再dispatch_group_leave,对于enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。当所有enter的block都leave后,会执行dispatch_group_notify的block。

  • 2.使用GCD的信号量dispatch_semaphore_t
    -(void)Btn3{
      NSString *str = @"http://www.jianshu.com/p/6930f335adba";
      NSURL *url = [NSURL URLWithString:str];
      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      NSURLSession *session = [NSURLSession sharedSession];
    
      dispatch_semaphore_t sem = dispatch_semaphore_create(0);
      for (int i=0; i<10; i++) {
      NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
          NSLog(@"%@",[NSThread currentThread])
          NSLog(@"%d---%d",i,i);
          count++;
          if (count==10) {
              dispatch_semaphore_signal(sem);
              count = 0;
            } 
        }];
        [task resume];
     }
      dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
      dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"end");
      });
    }
    

    开始信号量为0,等待,等10个网络请求都完成了,dispatch_semaphore_signal(semaphore)为计数+1,然后计数-1返回,程序继续执行。

    这里特别说一下,例子中的NSURLSessionDataTask 的block 回调中不是主线程,而是多线程环境。此时的主线程已经被阻塞了,是不会执行任何代码的,只有在子线程中把信号量加1,才能结束主线程的阻塞。

    10个网络请求顺序回调。

    - (void)toCrashing
    {
        NSString *str = @"http://www.jianshu.com/p/6930f335adba";
      NSURL *url = [NSURL URLWithString:str];
      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      NSURLSession *session = [NSURLSession sharedSession];
      
      NSConditionLock  *lock = [[NSConditionLock alloc] initWithCondition:0];
      dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
      for (int i=0; i<10; i++) {
          NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
              dispatch_async(queue, ^{
                  [lock lockWhenCondition:i];
                  NSLog(@"%d---%d",i,i);
                  [lock unlockWithCondition:i+1];
              });
          }];
          [task resume];
      }
     }
    

    使用NSConditionLock 可以解决。

小结

GCD的知识很多,本文就不一一罗列了,后续如有新的收获,会持续更新的。


本文参考文章:
iOS编程中throttle那些事
关于iOS多线程,你看我就够了
GCD入门(二): 多核心的性能

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

推荐阅读更多精彩内容

  • 最近颇花了一番功夫把多线程GCD人的一些用法总结出来,一来帮自己巩固一下知识、二来希望能帮到对这一块还迷茫...
    人活一世阅读 271评论 1 1
  • 目录:iOS多线程(一)--pthread、NSThreadiOS多线程(二)--GCD详解iOS多线程(三)--...
    Claire_wu阅读 1,051评论 0 6
  • 目录 一、基本概念1.多线程2.串行和并行, 并发3.队列与任务4.同步与异步5.线程状态6.多线程方案 二、GC...
    BohrIsLay阅读 1,548评论 5 12
  • 参考:GCD源码深入理解 GCDiOS多线程--彻底学会多线程之『GCD』关于iOS多线程,我说,你听,没准你就懂...
    啊哈呵阅读 2,789评论 2 2
  • 你总是碌碌无为,不求上进。不去寻找让自己发光发亮的可能,就只是一昧的抱怨命运的不公,那么你本来就不值得成功。
    喵了个呜的咪阅读 211评论 0 0