美文网首页
【原创】iOS 多线程之GCD 及GCD API的使用

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

作者: FeoniX_Fly | 来源:发表于2018-11-24 17:54 被阅读0次

    原创,转载请注明出处。

    抛砖引玉。最近在复习了《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

    相关文章

      网友评论

          本文标题:【原创】iOS 多线程之GCD 及GCD API的使用

          本文链接:https://www.haomeiwen.com/subject/ghbjqqtx.html