【原创】iOS 多线程之GCD 及GCD API的使用

原创,转载请注明出处。

抛砖引玉。最近在复习了《Obj-C高级编程》这本书后,一方面记录一下知识点,另一方便加了一些自己的理解。结合一些经典的例子以及实际使用场景加深理解,权当学习交流之用。

需要了解的基本概念

1.同步执行:阻塞当前线程。
2.异步执行:不阻塞当前线程。
3.串行队列:按照FIFO原则出列,一个一个的执行。
4.并行队列: 一起执行。
后续内容会再做解释。

基础API

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

这是gcd 中最常见的两个API,其中:

参数部分
  1. 第一个参数 dispatch_queue_t代表放在哪个队列执行,系统提供了几种队列:
  • dispatch_get_global_queue(long identifier, unsigned long flags)全局并行队列。第一个参数表示优先级,最后一个参数写0就可以了。系统提供了四种优先级:(优先级由高到低)
 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND

一般dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)这么写就行了。

  • dispatch_get_main_queue()主队列,也就是主线程的执行队列。此为串行队列。按照FIFO原则执行。
  • 同样我们也可以自定义队列:dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
    第一个参数为队列的名称,《Obj-C高级编程》作者推荐使用应用ID这种逆序全程域名的命名方式:"com.example.gcd.MyConcurrentDispatchQueue",该名称会出现在程序崩溃的crashlog中。便于排查问题。
    第二个参数代表队列类型,NULLDISPATCH_QUEUE_SERIAL创建串行队列。DISPATCH_QUEUE_CONCURRENT创建并行队列。
  1. 第二个参数dispatch_block_t 是一个block,我们把需要使用GCD执行的任务放在这个block里。
函数部分

dispatch_sync:代表同步执行,对应基础概念里的同步执行。
这个方法会阻塞当前线程,将第二个参数block里的任务追加到第一个参数指定的队列queue里执行。直到block里的任务执行完毕,程序才继续往下运行。
假如当前线程的执行队列和第一个参数里的queue是同一个队列,且都是串行队列,那么就会造成死锁。(见代码1.1.1,1.1.2)

dispatch_async:代表异步执行。不阻塞当前线程,即使用多个线程同时执行多个处理。其中异步执行一个并行队列,XNU内核会基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定要不要开辟新的线程,以及开辟多少个线程来处理。(代码1.2)

代码 1.1.1 死锁

当前线程为主线程,当前队列为主队列。queue内参数也为主队列,同时他们都是串行队列。那么按照刚刚我们总结的简单理论,发生死锁。

     NSLog(@"程序开始运行");
    //主线程阻塞,开始执行block里的任务
    dispatch_sync(dispatch_get_main_queue(), ^{
        //task2
        NSLog(@"此句不执行,加到主队列中执行,排在task3后面,按照FIFO原则需要等待 task3执行完毕才能执行");
    });
    // task2 等待task3, task3 等待 task2 .死锁
    NSLog(@"此句不执行,主线程主队列死锁");

我们将dispatch_get_main_queue 替换为dispatch_get_global_queue使得二者不为同一个串行队列,则不会发生死锁。同学们可以自行试验。

代码 1.1.2 死锁 。

当前执行队列和dispatch_syncqueue参数都为同一个同步队列。发生死锁。

    NSLog(@"task1");
    dispatch_queue_t otherQueue = dispatch_queue_create("com.test.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(otherQueue, ^{
        NSLog(@"task2");
        //发生死锁,当前执行队列和dispatch_sync 执行队列相同且都是同步队列
        dispatch_sync(otherQueue, ^{
            NSLog(@"task4");
            //task4 排在otherQueue执行任务task5之后,需要task5执行完毕才可以执行。
        });
        //同步执行,需要task4 执行完毕才可以执行task5。
        //task4 等待task5, task5 等待task4,发生死锁。
        NSLog(@"task5");
    });
    //打印 task1 task2 task3(task2,task3顺序不定)
    NSLog(@"task3");

同样我们可以将dispatch_sync 里的otherQueue替换为任意一个非相同队列,则不会发生死锁。这里也不再赘述。

代码 1.2 是否开启新线程,以及开辟多少个。
    dispatch_queue_t queue1 = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("com.test.gcd.concurrentQueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue1, ^{
        NSLog(@"thread%@",[NSThread currentThread]);
        dispatch_async(queue2, ^{
            NSLog(@"thread%@",[NSThread currentThread]);
        });
    });
    //有时打印
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //有时打印
    //thread<NSThread: 0x6000037a1f40>{number = 4, name = (null)}
    //thread<NSThread: 0x600003751e40>{number = 6, name = (null)}

以上试验也可以验证了这一理论,有时只需要开辟一个线程即可处理。有时需要开辟两个新线程。

iOS和OS X的核心--XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程。XNU内核仅使用Concurrent Dispatch Queue便可完美地管理并行执行多个处理的线程。

dispatch_set_target_queue

dispatch_set_target_queue(dispatch_object_t object,dispatch_queue_t _Nullable queue);
此API,可以改变执行队列优先级以及队列类型。

  • Concurrent Dispatch Queue 改 Serial Dispatch Queue :
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_set_target_queue(concurrentQueue, serialQueue);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task1 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task2 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task3 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task4 thread:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"task5 thread:%@",[NSThread currentThread]);
    });

注释掉 dispatch_set_target_queue这行,会无序打印task1-5。
加上后实际上执行队列由并行变成了串行执行,task1-5按顺序打印。

  • 改变队列优先级
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    dispatch_set_target_queue(concurrentQueue, globalQueue);

dispatch_queue_create 函数生成的Dispatch Queue 不管是Serial Queue 还是 Concurrent Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue 的执行优先级要使用dispatch_set_target_queue函数。

dispatch_after

dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
参数:
1.时间(指定时间追加处理到Dispatch Queue)。
2.队列。
3.任务。

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull *NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"3秒后追加到主线程队列里执行");
    });

因为主线程主队列,在主线程RunLoop中执行,所以在每隔1/60秒执行的RunLoop中,任务最快在3秒后执行,最慢在3+1/60秒后执行。如果主队列有大量处理,那么这个时间会更长。

Dispatch Group

在多个并行执行的任务全部执行完毕后,想要追加一个结束处理。这种场景往往比较常见。虽然可以通过别的方式实现,但逻辑会变的复杂,代码也不雅观。这时候Dispatch Group就发挥作用了。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"task Done");
    });
    //打印 (task1-3 无序)
    //task1
    //task2
    //task3
    //task Done

一个简单的demo,在任务1-3完成后,执行task Done

除了使用dispatch_group_notifyAPI 处理group任务结束外,还可以使用dispatch_group_wait函数。同样的例子:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
    });
    
    //也可以使用dispatch_group_wait 函数
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"task Done");

DISPATCH_TIME_FOREVER代表永久等待。

我们也可以指定等待的时间,下例等待1秒,超过1秒不再等待。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"task1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"task3");
        for (int i = 0; i < 10000000; i++){
            @autoreleasepool{
                NSString* string = @"ab c";
                //生成autorelease对象
                NSArray* array = [string componentsSeparatedByString:string];
            }
        }
    });
    
    //DISPATCH_TIME_FOREVER 永久等待,同样我们可以设置等待的时间
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    if (result == 0) {
        // 属于Dispatch Group 的全部处理执行结束
        NSLog(@"task Done");
    } else {
        // 属于Dispatch Group 的某个处理还在执行中
        NSLog(@"task Doing");
    }

通过dispatch_group_wait返回值可以判断,是group任务在设置的超时时间内完成,还是超时未完成。 result ==0 代表全部处理完成,非0代表执行超时了。

但假如Dispatch Queue 里的任务是一个个网络请求的话,由于网络请求是异步执行,那么实际达不到我们想要的在所有请求完毕后执行某段代码的目的。那么这时就可以借助信号量Dispatch Semaphore来完成了。

Dispatch Semaphore

  • dispatch_semaphore_create(long value) 创建一个信号量。
  • dispatch_semaphore_signal(dispatch_semaphore_t dsema) 信号量加1
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 等待信号大0时执行,并对信号量进行减一操作。
1.上述在Dispatch Queue里并行执行多个网络请求的情况,想要在所有请求都完成的情况执行某段代码就可以使用Dispatch Semaphore了。
- (void)requestDemo{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __weak typeof(self) weak_self = self;
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求任务A");
        [weak_self requestA];
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求任务B");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求任务C");
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"所有请求完成");
    });
}

- (void)requestA{
    //    用于GCD Group 以及 NSOperationQueue中设置依赖关系的任务,因为网络请求异步执行,
    //    不会阻塞当前线程,达不到按序执行的效果。
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    //    [异步请求:{
    //        成功: dispatch_semaphore_signal(sema);
    //        失败: dispatch_semaphore_signal(sema);
    //    }];
    //一直等待到信号量大于0才执行,并减1
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

异步请求方法前创建为0的信号量,请求结束后信号量+1,dispatch_semaphore_wait会等到信号量大于才继续运行。整个请求模块会在dispatch_semaphore_wait可以继续运行才标记为block任务结束。

2.控制异步执行Dispatch Concurrent Queue最大并发数。

众所周知NSOperationQueue便于管理多线程,可以设置maxConcurrentOperationCount来控制多线程执行的最大并发数。那么GCD要如何控制最大并发呢?这时Dispatch Semaphore又发挥作用了。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //初始信号量1 ,这里1可以为n
    dispatch_semaphore_t sema = dispatch_semaphore_create(1);
    for (NSInteger i = 0; i < 10; i++) {
        //大于0执行,并减1
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            NSLog(@"%ld",i);
            //任务完成,信号量加1
            dispatch_semaphore_signal(sema);
        });
    }
    //按顺序打印0-9

通过设置dispatch_semaphore_create (1)设置最大并发1,那么实际上就把并发队列设置成了一个串行队列。dispatch_semaphore_create (n)则最大并发为n,如果n设置的很大,实际上达不到n。因为苹果内核决定了此次GCD执行的并发队列所需要的线程数。

未完待续。。。
后续补充:

dispatch_barrier_async
dispatch_apply
dispatch_once

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

推荐阅读更多精彩内容