【原创】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{number = 4, name = (null)}
    //thread{number = 4, name = (null)}
    //有时打印
    //thread{number = 4, name = (null)}
    //thread{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的使用)