阅读 717

iOS多线程GCD篇

首先,GCD的源码在这Grand Central Dispatch,如果想要深入的理解GCD的实现原理,最好还是下载一份源码慢慢的阅读一下。 本文不会对GCD的底层源码进行剖析,只会总结一下应用层面的东西。 本文会涉及到的内容:

  • 什么是GCD
  • GCD的基础实现
  • GCD与其他多线程实现方式相比的优劣
  • GCD常用API的释义、解析与应用
  • GCD的一些坑

一. 什么是GCD

GCD的全称是Grand Central Dispatch,是苹果公司开发的多核心处理器编写并发运行的程序。在Mac OS X10.6 和iOS4以及以上的系统可以使用。它是一套底层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。 除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。可以设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通讯)、进程、计时器、信号、用户生成事件。这些句柄通过GCD来并发执行。

苹果官方对GCD的解释中有这样一句话:

开发者要做的只是定义执行的任务并追加到适当的 Dispatch Queue 中。

所以在GCD中我们需要做的只是两件事:1:定义任务;2:把任务加到队列中。

  • GCD允许程序将任务切分成多个任务然后提交到一个队列中并发执行或者串行执行。
  • GCD是非常高效的多线程开发方式。并且它并不是Cocoa框架的一部分。

GCD编程的核心就是dispatch队列,dispatch block的执行最终都会放进某个队列中去进行,下边是GCD几种队列获取方式:

  1. 主线程队列(The main queue):提交至main queue的任务会在主线程中执行。
    • 可以调用dispatch_get_main_queue()来获得。
    • 因为main queue是与主线程相关的,所以这是一个串行队列。和UI相关的修改必须使用Main Queue。
  2. 全局并发队列(Global queue): 全局并发队列由整个进程共享,有高、中(默认)、低、后台四个优先级别。
    • 可以通过调用dispatch_get_global_queue函数来获取(可以设置优先级)
  3. 自定义队列(Custom queue):
    • 并行队列(Concurrent Dispatch Queue):全局队列是并发队列,并由整个进程共享。
      • 可以并发的执行多个任务,但是执行完成的顺序是随机的。
      • 进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队列。
    • 串行队列(Serial Dispatch Queue):也叫做用户队列(GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列)。
      • 同一时间只执行一个任务,通常用于同步访问特定的资源或数据。
      • 当你创建多个串行队列的时候,每个队列里的任务都是串行执行的
      • 每个串行队列与队列之间是并发执行的。
      • 有点像传统线程中的mutex。
  4. Group queue (队列组):将多线程进行分组,最大的好处是可获知所有线程的完成情况。
    • Group queue 可以通过调用dispatch_group_create()来获取,通过 dispatch_group_notify,可以直接监听组里所有线程完成情况。

所以,在使用GCD的时候我们只需要搞清楚我们的任务是要同步执行还是异步执行,是要串行还是并行,我们并不需要直接和线程打交道,只需要向创建好的队列中添加需要被执行的代码块就可以了。除了提供代码并发执行的能力,还提供了高度集成的事件控制系统。GCD自己在后端管理着一个可调度线程池,不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样就可以将开发者从线程管理的工作中解放出来,并且通过线程的集中管理,缓解了线程被大量创建的问题。 GCD的API很大程度是基于block,当然也可以脱离block来使用,比如使用函数指针和上下文指针等。实践证明配合block使用时,GCD非常的简单易用且能发挥最大的能力。

二. GCD的基础实现

GCD的基本概念就是dispatch queue。dispatch queue是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。也就是FIFO队列。dispatch queue可以是并发的或串行的。并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间只执行单一任务。 GCD是纯c语言的,但它被组建成面向对象的风格。GCD对象被称为dispatch object。Dispatch object像Cocoa对象一样是引用计数的。使用dispatch_release和dispatch_retain函数来操作dispatch object的引用计数来进行内存管理。 在iOS 6.0以前,我们必须手动管理GCD对象的内存,使用(dispatch_retain,dispatch_release),ARC并不会去管理它们。但是iOS6.0以后就不需要了,ARC已经能够管理GCD对象了,这时候,GCD对象就如同普通的OC对象一样,不应该使用dispatch_retain ordispatch_release。

FIFO:

FIFO队列称为dispatch queue,FIFO就是 First In First Out的简称,翻译过来就是先进先出。GCD保证先进来的任务先得到执行。

三. GCD与其他多线程实现方式相比的优劣

GCD的优势:

  1. GCD很优雅,轻巧。比专门创建消耗资源的线程更加的实用且快速。
  2. GCD自动根据系统的负载来增减线程的数量,这就增加了计算效率。
  3. GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。
  4. GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
  5. GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
  6. GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
  7. GCD 会自动利用更多的CPU
  8. 只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
  9. GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

GCD提供很多超越传统多线程编程的优势:

  1. GCD比之NSThread跟简单易用。由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文。
  2. 相对于NSOperation来说GCD更接近底层,而NSOperation则是更高级抽象,所以GCD在追求性能的底层操作来说,是速度最快的。
  3. GCD需要手动实现异步操作之间的事务性,顺序行,依赖关系等,而NSOperationQueue已经内建了这些支持
  4. 如果异步操作的过程需要更多的被交互和UI呈现出来,NSOperation会是一个更好的选择。
  5. 如果需要执行的任务相互的依赖关系较轻,并且需要高并发能力的话,GCD则更有优势。
  6. 在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);
  7. 我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;
  8. 在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;

总的来说,Operation queue 提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。

四. GCD常用API的释义、解析与应用

接下来就是介绍一下GCD常用的API、释义、解析以及应用

1. 创建和获取

GCD手动创建队列部分

// 手动创建队列API
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_SERIAL);
// 创建并行队列
dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_CONCURRENT);
复制代码

手动创建队列API有两个可变参数:

  1. const char ***label,为队列指定一个名称
  2. dispatch_queue_attr_t attr,关于这个参数官方API有直接的说明:
/*!
* @const DISPATCH_QUEUE_SERIAL
* @discussion A dispatch queue that invokes blocks serially in FIFO order.
*/
#define DISPATCH_QUEUE_SERIAL NULL
/*!
* @const DISPATCH_QUEUE_CONCURRENT
* @discussion A dispatch queue that may invoke blocks concurrently and supports
* barrier blocks submitted with the dispatch barrier API.
*/
#define DISPATCH_QUEUE_CONCURRENT \
      DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
      _dispatch_queue_attr_concurrent)
复制代码

ISPATCH_QUEUE_SERIAL 创建一个遵循FIFO协议的串行队列 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列

串行队列实例代码:

dispatch_queue_t currentQueue = dispatch_queue_create("com.serial.test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"串行队列异步事件:%tu ---",i);
        }
    });
    
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"串行队列同步事件:%tu +++",i);
        }
    });
复制代码

并行队列实例代码:

dispatch_queue_t currentQueue = dispatch_queue_create("com.parallel.test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"串行队列异步事件:%tu ---",i);
        }
    });
    
dispatch_sync(currentQueue, ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"串行队列同步事件:%tu +++",i);
        }
    });
复制代码

上边说了一种创建并发队列的方式,GCD还有另外一种获取并发队列的方式,这种方式是苹果已经为我们写好的,我们只需要传入参数就可以了:

dispatch_queue_t concurrent_queue =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
复制代码

下边是官方API:

/*!
 * @function dispatch_get_global_queue
 *
 * @abstract
 * Returns a well-known global concurrent queue of a given quality of service
 * class.
 *
 * @discussion
 * The well-known global concurrent queues may not be modified. Calls to
 * dispatch_suspend(), dispatch_resume(), dispatch_set_context(), etc., will
 * have no effect when used with queues returned by this function.
 *
 * @param identifier
 * A quality of service class defined in qos_class_t or a priority defined in
 * dispatch_queue_priority_t.
 *
 * It is recommended to use quality of service class values to identify the
 * well-known global concurrent queues:
 *  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
 *
 * The global concurrent queues may still be identified by their priority,
 * which map to the following QOS classes:
 *  - 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
 *
 * @param flags
 * Reserved for future use. Passing any value other than zero may result in
 * a NULL return value.
 *
 * @result
 * Returns the requested global queue or NULL if the requested global queue
 * does not exist.
 */
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags);
复制代码

可以看到文档对两个参数都做了解释,其中第一个参数是标识队列的优先级,一共有四种:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
复制代码

第二个参数是苹果留作以后备用的,一般默认就填0就OK了。

获取主队列

dispatch_queue_t queue = dispatch_get_main_queue();
复制代码

2. 同步和异步

下边是系统提供的同步和异步API:

dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block);
dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block);
复制代码

两个参数都相同,第一个是需要执行的队列,第二个是block回调。

同步函数就是等待任务的执行,等任务执行完成后再返回。所以同步就意味着在当前线程中执行任务,并不会开启新的线程,不管是串行队列还是并发队列。

dispatch_sync(需要执行的线程名称, ^{ 
      NSLog(@"同步线程");
});
复制代码

而异步函数就不会等待任务的执行完成,它会立即返回。所以异步也就意味着会开启一个新的线程,所以并不会阻塞当前的线程。

dispatch_async(需要执行的线程名称, ^{ 
  NSLog(@"异步线程");
});
复制代码

3. 队列组

首先我们看一下队列组的基础使用,假设有三个任务,第一个和第二个任务完成之后执行第三个任务:

// 获取全局并发队列
dispatch_queue_t currentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主线程
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 创建队列组
dispatch_group_t groupQueue = dispatch_group_create();
// 将第一个任务加入队列组
dispatch_group_async(groupQueue, currentGlobalQueue, ^{
    NSLog(@"并行任务1");
});
// 将第二个任务加入队列组
dispatch_group_async(groupQueue, currentGlobalQueue, ^{
    NSLog(@"并行任务2");
});
// 将需要最后执行的任务加入队列组
dispatch_group_notify(groupQueue, mainQueue, ^{
    NSLog(@"groupQueue中的任务 都执行完成,回到主线程更新UI");
});
复制代码

队列组部分API释义如下 创建队列组:

dispatch_group_create();
复制代码

将任务加入队列组:

dispatch_group_enter(dispatch_group_t  _Nonnull group);
复制代码

参与为需要加入的队列组

标识加入队列组的任务已经完成:

dispatch_group_leave(dispatch_group_t  _Nonnull group);
复制代码

需要注意的是,enter和leave必须配对。

队列里边所有任务执行完毕之后:

dispatch_group_notify(dispatch_group_t  _Nonnull group, dispatch_queue_t  _Nonnull queue, ^{
        NSLog(@"队列里边所有的任务都执行完毕之后需要执行的事件写到这");
});
复制代码

3. 其他dispatch方法

  • 阻塞当前线程dispatch_group_wait: dispatch_group_wait ,它会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时发生。
dispatch_group_wait(dispatch_group_t  _Nonnull group, dispatch_time_t timeout);
复制代码

示例代码:

dispatch_group_t groupQueue = dispatch_group_create();
dispatch_queue_t conCurrentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"开始异步任务");
dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{
      NSLog(@"等待三秒");
dispatch_group_wait(groupQueue, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
      NSLog(@"执行并行任务");
});
复制代码
  • dispatch_after延时添加到队列 dispatch_after函数并不会在指定的时间后立即执行任务,时间到后,它只将任务加入到队列上。因此这段代码的作用和你在3秒钟后用dispatch_async函数将block加到主线程上面的作用是一样的。主线程队列是在RunLoop上执行的,因此,假如RunLoop每1/60秒执行一次任务,那么上面添加的block会在3秒~3+1/60秒后被执行。如果主线程队列上加了很多任务,或者主线程延迟了,时间可能更晚。所以,将dispatch_after作为一个精确的定时器使用是有问题的。如果你只是想粗略的延迟一下任务,这个函数还是很有用的。 函数的第二个参数指定了一个dispatch队列,用于添加任务。第三个参数,是一个block,即要执行的任务。第一参数指定了延迟的时间,是一个dispatch_time_t类型的参数。这个值可以用dispatch_time函数或dispatch_walltime函数来创建。 dispatch_time的第一个参数是指定的起始时间,第二个参数是以纳秒为单位的一个时间间隔。这个函数以起始时间和时间间隔来创建一个新的时间。如例中所示,通常以DISPATCH_TIME_NOW来作为第一个参数的值,它代表了当前的时间。在下面这段代码中,你可以得到一个dispatch_time_t类型的表示1秒钟之后的时间的变量。 第二个参数中,NSEC_PER_SEC和数字的乘积会得到一个以纳秒为单位的时间间隔值。如果使用NSEC_PER_MSEC,就会得到一个以毫米为单位的时间间隔值。
dispatch_time_t delayTime3 = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
dispatch_time_t delayTime2 = dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"准备添加执行任务");
dispatch_after(delayTime3, mainQueue, ^{
      NSLog(@"3秒之后添加到队列");
});
dispatch_after(delayTime2, mainQueue, ^{
      NSLog(@"2秒之后添加到队列");
});
复制代码
  • dispatch_apply多次执行 dispatch_apply是同步执行的函数,不会立刻返回,在执行完block中的任务后才会返回。功能是把一项任务提交到队列中多次执行,队列可以是串行也可以是并行。 为了不阻塞主线程,dispatch_apply正确使用方法是把dispatch_apply放在异步队列中调用,然后执行完成后通知主线程
dispatch_apply(size_t iterations, dispatch_queue_t  _Nonnull queue, ^(size_t)block)
复制代码
  • 第一个参数是需要执行的次数
  • 第二个参数是准备提交的队列
  • 第三个参数是block回调,里边编写需要被重复执行的代码

示例代码:

dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"准备添加执行任务");
dispatch_async(globalQueue, ^{
      dispatch_queue_t applyQueue = dispatch_get_global_queue(0, 0);
      dispatch_apply(3, applyQueue, ^(size_t currentCount) {
            NSLog(@"%tu",currentCount);
      });
      NSLog(@"dispatch_apply 执行完成");
});
复制代码
  • dispatch_once 执行一次

    在app运行期间只执行block中的代码只执行一次,常用于单例。 示例代码:

    for (int i = 0; i < 10; i ++) {
        NSLog(@"%tu",i);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"执行一次");
        });
        sleep(1);
    }
    复制代码
  • ** dispatch_barrier_async 栅栏**

    在并行队列中添加多个任务,首先执行dispatch_barrier_async之前添加到队列里边的任务,之后执行dispatch_barrier_async里的任务,之后再执行dispatch_barrier_async之后添加到队列里的任务。必须注意它只对dispatch_queue_create(label, attr)接口创建的并发队列有作用,如果是Global Queue(dispatch_get_global_queue),这个barrier就不起作用。

示例代码:

    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.multithread.tempApp", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第一个添加到队列任务");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第二个添加到队列的任务");
    });
    dispatch_barrier_async(conCurrentQueue, ^{
        NSLog(@"dispatch barrier 阻塞任务");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"第三个添加到队列的任务,在barrier之后");
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"最后一个添加到队列的任务,在barrier之后");
    });
复制代码
  • dispatch_set_target_queue 设置优先级
dispatch_set_target_queue(dispatch_object_t  _Nonnull object, dispatch_queue_t  _Nullable queue)
复制代码

dispatch_set_target_queue函数用于设置一个”目标”队列。这个函数主要用来为新创建的队列设置优先级。当用dispatch_queue_create函数创建一个队列后,无论创建的是并行队列还是串行队列,队列的优先级都和全局dispatch队列的默认优先级一样。创建队列后,你可以用这个函数来修改队列的优先级。下面的代码演示了如何给一个串行队列设置background优先级。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.multithread.tempApp", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
复制代码

在上面代码中,dispatch_set_target_queue函数的第一个参数是要设置优先级的队列,第二个参数是一个全局的dispatch队列,它会被作为目标队列。这段代码会使队列拥有和目标队列一样的优先级(稍后会解释这里的机制)。如果你将主线程队列或者全局队列传递给dispatch_set_target_queue函数的第一参数,结果不确定,最后不要这么干。使用dispatch_set_target_queue函数不仅能够设置优先级,也能创建队列的层次体系。如图7-8所示,一个串行队列被设置成了多个串行队列的目标队列,在目标队列上,一次只会有一个队列被执行。

  • dispatch_semaphore_signal 信号量
// dispatch_semaphore_signal
    NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    // 设置缓存策略为每次都从网络加载 超时时间30秒
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 处理完成之后,发送信号量
        NSLog(@"正在处理...");
        dispatch_semaphore_signal(semaphore);
    }] resume];
    // 等待网络处理完成
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"处理完成!");
复制代码

需要注意的是:在上面的举例中dispatch_semaphore_signal的调用必须是在另一个线程调用,因为当前线程已经dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主线程调用

  • dispatch_suspend(暂停)和dispatch_resume(继续) 我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止

五. GCD的一些坑

常用的API和用法都说过了,接下来说一下GCD里边常见的坑。

  1. 在主队列的主线程里边调用主线程执行同步任务,会产生死锁:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue,^{
    NSLog(@"MainQueue");            
});
复制代码

执行这段代码会发现程序崩溃,block里边的MainQueue不会打印出来。 反之用异步API去执行这段代码就没问题

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue,^{
  NSLog("MainQueue");            
});
复制代码

程序正常运行,block中的代码正常运行 这段代码崩溃的原因是:主线程的获取和执行都是在主队列的主线程里边执行的,然后碰到了需要同步执行的代码,主线程就会阻塞到同步代码的地方,等待同步代码执行完毕之后继续执行。可是由于FIFO协议的存在,串行队列先进先出,由于主队列主线程的事件是先进去的,所以同步代码会等待主队列主线程代码执行完毕才会继续执行。这样就产生了死锁,程序崩溃。 如果将这段代码放到其他异步队列然后由主线程执行就不会崩溃了,代码如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        dispatch_sync(mainQueue,^{
            NSLog(@"MainQueue");
        });
    });
复制代码

当然必须是异步队列,如果是同步队列的话还是会产生崩溃,原因和刚刚一样。

  1. 在串行队列的同步任务里边再执行一个同步任务,会发生死锁:
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serial_queue, ^{
        NSLog(@"串行1--同步1");
        dispatch_sync(serial_queue, ^{
            NSLog(@"串行1--同步2");
        });
    });
复制代码

程序崩溃,NSLog(@"串行1--同步1");会被执行,NSLog(@"串行1--同步2");不会被执行。崩溃原因和第一条是相同的,严格来说第一条只是第二条的一种特殊情况。

  1. 在串行队列的异步任务中再嵌套执行同步任务,也会发生死锁:
dispatch_queue_t serial_queue = dispatch_queue_create("com.haley.com", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serial_queue, ^{
        NSLog(@"串行1--异步1");
        dispatch_sync(serial_queue, ^{
            NSLog(@"串行1----同步1");
        });
        [NSThread sleepForTimeInterval:2.0];
    });
复制代码

NSLog(@"串行1--异步1");会执行,NSLog(@"串行1----同步1");不会执行。同样的,由于串行队列一次只能执行一个任务,任务结束后,才能执行下一个任务。所以异步任务的结束需要等里面同步任务结束,而里面同步任务的开始需要等外面异步任务结束,所以就相互等待,发生死锁了。 3. dispatch_apply导致的死锁

dispatch_queue_t queue = dispatch_queue_create("com.multithread.tempApp", DISPATCH_QUEUE_SERIAL);
dispatch_apply(3, queue, ^(size_t fitstApplyCount) {
    NSLog(@"first apply loop count: %tu", fitstApplyCount);
    //再来一个dispatch_apply!死锁!
    dispatch_apply(3, queue, ^(size_t secondApplyCount) {
        NSLog(@"second apply loop count %tu", secondApplyCount);
    });
});
复制代码

一些需要知道的知识:

  1. 主线程串行队列由系统默认生成的,所以无法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。
  2. 全局并发队列由系统默认生成的,所以无法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。

关于GCD的知识先写到这,其实还有很多东西没写,如果有时间再续写第二篇,不过在这可以想一下下一篇都会涉及到什么内容:

  1. GCD中不为人所知但是很有用的API
  2. GCD的线程安全
  3. 常用API实现原理与源码解析

以上,就是GCD的调研结果,并没有太底层的东西,大部分都是应用层面的,希望以后有时间把缺失的东西补上。





有志者、事竟成,破釜沉舟,百二秦关终属楚;

苦心人、天不负,卧薪尝胆,三千越甲可吞吴.

关注下面的标签,发现更多相似文章
评论