iOS多线程 之 GCD详解

GCD全称Grand Central Dispatch是苹果提供的一个多核编程的解决方案,在真正意义上实现了并行操作,而不是并发。

GCD使用线程池模型来执行用户提交的任务,所以它比较节约资源,不需要为每个任务都重新创建一个新的线程,GCD不需要自行编写并行代码,而是自动进行多核的并行计算,自动管理线程的生命周期,如:使用线程池管理线程的创建和销毁,线程的调度,任务的调度等,用户只需要编写任务代码并提交即可。

GCD中有两个比较重要的概念:任务队列

GCD的任务:

任务顾名思义就是我们需要执行的代码块,可以是一个方法也可以是一个block,就是我们需要线程为我们完成的工作,编写完成的任务只需提交给GCD的队列,即可自动帮我们完成任务的调度,以及线程的调度,可以很方便的以多线程的方式执行。

GCD的队列:

队列用于管理用户提交的任务,GCD的队列有两种形式,串行队列和并发队列:

  • 串行队列: GCD底层只维护一个线程,任务只能串行依次执行。
    对于串行队列来说,GCD每次从串行队列的队首取一个任务交给唯一的一个线程来处理,直到前一个任务完成后,才继续从队列中取下一个任务来执行,因此,串行队列中的任务执行严格按照提交顺序,并且后一个任务必须等前一个任务执行完成后才可以执行。

  • 并发队列: GCD底层使用线程池维护多个线程,任务可并发执行。
    对于并发队列来说,GCD每次从并发队列的队首取一个任务,并将这个任务按照任务调度分发给多个线程中的某一个线程,此时不需要等待其完成,如果队列中还有其他任务继续从队列中取出并分发给某一个线程来执行,由于底层由线程池管理多个线程,每个任务的时间复杂度不同再加上线程调度的影响,后提交的任务可能先执行完成。但对于单个线程来说,只能按顺序执行,比如某个线程被安排了多个任务,那这个线程就只能按提交顺序依次执行任务。

不论是串行队列还是并发队列都使用FIFO 先进先出的方式来管理用户提交的任务。

所以,我们在使用GCD时也就很简单了,只需要创建或获取系统队列、编写任务并提交任务到队列即可。首先举一个下载图片的栗子,这个栗子和第一篇讲解NSThread的栗子一样,但是使用GCD来实现:

//获取一个优先级默认的全局并发队列,并以异步的方式提交任务执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //下载图片
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
        //获取主队列,在主线程中更新UI,并以异步方式提交任务
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        }); 
    });

上面的栗子非常简单,不需要我们手动创建线程即可实现多线程的并发编程,如果现在还看不懂没关系,学完本章内容你一定会懂。

GCD创建队列和获取队列的API:
//获取当前执行该方法的队列,被废弃了,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);

/*
获取主队列,即与主线程相关联的队列
如果需要提交任务到主线程使用该方法获取主线程的主队列即可
主队列是串行队列因为只维护主线程一个线程
*/
dispatch_queue_t dispatch_get_main_queue(void);

/*
获取一个全局的并发队列
identifier指定该队列的优先级可选值有:
    DISPATCH_QUEUE_PRIORITY_HIGH 2
    DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    DISPATCH_QUEUE_PRIORITY_LOW (-2)
    DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到传0
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);

/*
创建一个队列
label 队列的名称
attr 队列的属性可选值有:
    DISPATCH_QUEUE_SERIAL 创建一个串行队列
    DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
通过这种方式可以自己维护一个队列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

GCD的执行方式:

  • 同步执行:阻塞当前线程,直到任务执行完成后,当前线程才可继续执行。
  • 异步执行:不阻塞当前线程,可能使用其他线程来执行任务,不需要等待任务完成,当前线程即可立即继续执行。
API:
/*
以异步方式执行任务,不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

/*
同上
context 是一个void*的指针,作为work的第一个形参
work 是一个函数指针,指向返回值为void 形参为void*的函数,且形参不能为NULL,也就是说context一定要传
使用起来不方便,一般不怎么用,需要使用C函数,也可以使用OC方法通过传递IMP来执行但是会有编译警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式执行任务,阻塞当前线程,必须等待任务完成当前线程才可继续执行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,并重复执行iterations次
iterations 迭代执行次数
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值为void形参为iterations迭代次数
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue,  DISPATCH_NOESCAPE void (^block)(size_t));

//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));

/*
以异步方式提交任务,在when时间点提交任务
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以异步方式提交任务,会阻塞queue队列,但不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
需要说明的是,即时使用并发队列,该队列也会被阻塞,前一个任务执行完成才能执行下一个任务
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,会阻塞queue队列,也会阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
同样的,即时是并发队列该队列也会被阻塞,需要等待前一个任务完成,同时线程也会阻塞
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
底层线程池控制block任务在整个应用的生命周期内只执行一次
predicate 实际为long类型,用于判断是否执行过
block block形式的任务,该block返回值、形参都为void
该方法常用于实现单例类,以及结合RunLoop创建一个常驻内存的线程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

猛的一看常用的方法就有十二种呢,但是可以发现每一类方法都提供了block任务function任务两种形式,所以常用的也就七种,只是对应了block版本和函数版本。接下来先介绍最常用的同步执行和异步执行,其他的方法后文会讲。

GCD队列和执行方式的组合:

  • 异步并发队列:

    //手动创建了一个并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以获取全局的并发队列,效果一样
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //开启异步任务,并加入并发队列中
    dispatch_async(concurrentQueue, ^{
      NSLog(@"Task1 %@", [NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
      NSLog(@"Task2 %@", [NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
      NSLog(@"Task3 %@", [NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
      NSLog(@"Task4 %@", [NSThread currentThread]);
    });  
    

    打印结果:

    2020-03-17 11:21:14.225080+0800 多线程[2918:102124] Task2 <NSThread: 0x600003a25400>{number = 9, name = (null)}
    2020-03-17 11:21:14.225108+0800 多线程[2918:103405] Task1 <NSThread: 0x600003a254c0>{number = 11, name = (null)}
    2020-03-17 11:21:14.225367+0800 多线程[2918:103431] Task4 <NSThread: 0x600003a20f80>{number = 13, name = (null)}
    2020-03-17 11:21:14.225491+0800 多线程[2918:103407] Task3 <NSThread: 0x600003a25500>{number = 12, name = (null)}
    

    可以看出开辟多个线程,每个任务都是在不同的线程中同时执行。

  • 异步串行队列:

    //手动创建了一个串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
    //开启异步任务,并加入串行队列中
    dispatch_async(serialQueue, ^{
      NSLog(@"Task1 %@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
      NSLog(@"Task2 %@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
      NSLog(@"Task3 %@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
      NSLog(@"Task4 %@", [NSThread currentThread]);
    });  
    

    打印结果:

    2020-03-17 11:27:59.970147+0800 多线程[3004:107449] Task1 <NSThread: 0x600002394780>{number = 7, name = (null)}
    2020-03-17 11:27:59.970364+0800 多线程[3004:107449] Task2 <NSThread: 0x600002394780>{number = 7, name = (null)}
    2020-03-17 11:27:59.970544+0800 多线程[3004:107449] Task3 <NSThread: 0x600002394780>{number = 7, name = (null)}
    2020-03-17 11:27:59.970682+0800 多线程[3004:107449] Task4 <NSThread: 0x600002394780>{number = 7, name = (null)}
    

    可以看出开辟出一个新的线程,并且所有任务都在这个线程中按照顺序执行。

  • 同步并发队列:

    //手动创建了一个并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以获取全局的并发队列,效果一样
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //开启同步任务,并加入并发队列中
    dispatch_sync(concurrentQueue, ^{
      NSLog(@"Task1 %@", [NSThread currentThread]);
    });
    dispatch_sync(concurrentQueue, ^{
      NSLog(@"Task2 %@", [NSThread currentThread]);
    });
    dispatch_sync(concurrentQueue, ^{
      NSLog(@"Task3 %@", [NSThread currentThread]);
    });
    dispatch_sync(concurrentQueue, ^{
      NSLog(@"Task4 %@", [NSThread currentThread]);
    });  
    

    打印结果:

    2020-03-17 11:32:51.963617+0800 多线程[3081:111266] Task1 <NSThread: 0x600001204c00>{number = 1, name = main}
    2020-03-17 11:32:51.963813+0800 多线程[3081:111266] Task2 <NSThread: 0x600001204c00>{number = 1, name = main}
    2020-03-17 11:32:51.963920+0800 多线程[3081:111266] Task3 <NSThread: 0x600001204c00>{number = 1, name = main}
    2020-03-17 11:32:51.964038+0800 多线程[3081:111266] Task4 <NSThread: 0x600001204c00>{number = 1, name = main}
    

    可以看出没有开辟新的线程,每个任务都是在当前线程中按照顺序执行。

  • 同步串行队列:

    //手动创建了一个串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
    //开启同步任务,并加入串行队列中
    dispatch_sync(serialQueue, ^{
      NSLog(@"Task1 %@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
      NSLog(@"Task2 %@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
      NSLog(@"Task3 %@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
      NSLog(@"Task4 %@", [NSThread currentThread]);
    });  
    

    打印结果:

    2020-03-17 11:39:27.880465+0800 多线程[3162:115800] Task1 <NSThread: 0x6000020b25c0>{number = 1, name = main}
    2020-03-17 11:39:27.880655+0800 多线程[3162:115800] Task2 <NSThread: 0x6000020b25c0>{number = 1, name = main}
    2020-03-17 11:39:27.880791+0800 多线程[3162:115800] Task3 <NSThread: 0x6000020b25c0>{number = 1, name = main}
    2020-03-17 11:39:27.880908+0800 多线程[3162:115800] Task4 <NSThread: 0x6000020b25c0>{number = 1, name = main}
    

    可以看出没有开辟新的线程,每个任务都是在当前线程中按照顺序执行。

GCD队列和执行方式的组合总结:
type Serial串行队列 Concurrent并发队列
async异步执行 不阻塞当前线程,使用其他线程串行执行任务,只有一个线程按顺序执行任务 不阻塞当前线程,并发执行任务,使用多个线程同时执行任务
sync同步执行 阻塞当前线程,使用同一线程串行执行任务,只有一个线程用于执行任务 阻塞当前线程,可能使用同一线程串行执行任务

所以,针对异步执行/同步执行和串行队列/并发队列,只需要掌握其关键就可以了,同步/异步的区别在于是否阻塞线程,串行/并发队列的区别在于有多少个线程参与执行任务。即时存在嵌套结构也能够很好理解了。举一个嵌套结构的例子:

dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);

dispatch_sync(serialQueue, ^{
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
        
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
        
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
        
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Complete.");
    }
});    

外层dispatch不论使用串行队列还是并发队列,由于只有一个任务,只会有一个线程来执行这个block块的内容,而同步和异步的区别就在于是否会阻塞当前线程,接下来看block块的内容,采用了三个异步提交到并发队列,所以并发队列里就有了三个不同的任务,就可以真正执行并发,由于都是异步提交没有阻塞当前线程,所以输出Complete的代码也会掺杂在Task1-3中乱序输出。

GCD其他方法的使用:

  • dispatch_apply:表示重复执行任务iterations次。同步执行,会阻塞当前线程。

    dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
      NSLog(@"Task %@ %ld", [NSThread currentThread], t);
    });
    

    注意:如果该方法是在主线程中执行,则不能传入当前串行队列。因为dispatch_apply方法是是同步执行,又把该方法放入当前串行队列中去执行,但是当前串行队列处于阻塞中,等待dispatch_apply方法执行完成,所以会产生死锁。

  • dispatch_after:等待when时间后,执行任务。异步执行,不会造成线程阻塞。

    NSLog(@"Before");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSLog(@"In %@", [NSThread currentThread]);
    });
    NSLog(@"After");
    

    打印结果:

    2020-03-17 14:58:27.403048+0800 多线程[4687:212802] Before
    2020-03-17 14:58:27.403355+0800 多线程[4687:212802] After
    2020-03-17 14:58:32.403767+0800 多线程[4687:212986] In <NSThread: 0x6000007a6040>{number = 7, name = (null)}
    

    可以看出这个5s并不是精确的5s,因为该方法是在when时间点到达的时候去提交任务到队列,所以是延迟提交,而不是延迟执行,队列什么时候安排线程去执行是未知的,所以不要用这个方法去实现定时器这样的功能。

  • dispatch_barrier_(a)sync:栅栏函数,用于阻塞当前队列,等待当前队列任务执行完成后,在执行dispatch_barrier_(a)sync的任务。

    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
      
    dispatch_async(concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
          NSLog(@"Task0 %@ %d", [NSThread currentThread], i);
      }
    });
      
    dispatch_async(concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
          NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
      }
    });
      
    dispatch_barrier_async(concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
          NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
      }
    });
      
    dispatch_async(concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
          NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
      }
    });
      
    dispatch_async(concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
          NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
      }
    });
    

    上面的输出是按照Task0 Task1并发执行,Task2等待Task0 Task1执行完成后单独执行, 最后Task3 Task4等待Task2执行完成后开始并发执行 。

    dispatch_barrier_async方法常与并发队列共用,前一段任务使用dispatch_async异步并发执行,然后插入一个dispatch_barrier_async执行一个中间任务,这个中间任务必须要等待前面的并发任务执行完成后才能开始执行,接着这个中间任务完成后,继续异步并发执行接下来的任务。

  • dispatch_once:该方法能够保证在应用的生命周期内只执行一次提交的任务,所以常用于单例类的创建。

    @interface MyUtil: NSObject <NSCopying>
    
    + (instancetype)sharedUtil;
    
    @end
    
    @implementation MyUtil
    
    static MyUtil *staticMyUtil = nil;
    
    + (instancetype)sharedUtil
    {
        //保证初始化创建只执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            staticMyUtil = [[MyUtil alloc] init];
        });
        return staticMyUtil;
    }
    
    //防止通过alloc或new直接创建对象
    + (instancetype)allocWithZone:(struct _NSZone *)zone
    {
        //保证alloc函数只执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            staticMyUtil = [super allocWithZone:zone];
        });
        return staticMyUtil;
    }
    
    //实现NSCopying协议的方法,防止通过copy获取副本对象
    - (instancetype)copyWithZone:(NSZone *)zone
    {
      return staticMyUtil;
    }
    
    @end
    

    dispatch_once函数需要传入一个long类型的predicate,这个值必须是独一无二的,使用静态变量的地址最合适不过了,MyUtil实现了NSCopying协议的copyWithZone:方法,防止通过copy方法获取副本对象。

    当使用alloc&&init方法初始化时,先调用allocWithZone:方法来分配存储空间,如果再次使用sharedUtil方法来获取的话,由于没有执行过,会执行到dispatch_once内部block,此时会再去执行allocWithZone:方法,但该方法内部dispatch_once已经执行过了会直接返回staticMyUtil,反过来调用是一样的道理,通过这样的方式就可以实现真正的单例了。

  • dispatch_ group_ t:是一个比较实用的方法,通过构造一个组的形式,将各个同步或异步提交任务都加入到同一个组中,当所有任务都完成后会收到通知,用于进一步处理,通过这样的方式就可以实现多线程下载,当下载完成后就可以通知用户了。

    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
      
    dispatch_group_t group = dispatch_group_create();
      
    dispatch_group_async(group, concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
      {
        NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
      }
    });
      
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
      for (int i = 0; i < 500; i++)
        {
          NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
      
    dispatch_group_async(group, concurrentQueue, ^{
      for (int i = 0; i < 500; i++)
        {
          NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
      
    dispatch_group_notify(group, concurrentQueue, ^{
      NSLog(@"All Task Complete");
    });
    

    像一个组中添加了三个异步的任务,最终三个任务完成后可以收到通知执行回调的block,上面的输出为,All Task Complete在前面三个输出都结束后才会输出。

防止GCD产生死锁:

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"Before");
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"In");
    });
    
    NSLog(@"After");
}

上述代码就会产生死锁,分析下原因,首先,viewWillAppear:方法是在主线程中执行的,接着调用dispatch_sync方法,该方法会阻塞当前线程,也就是会阻塞主线程,主线程被阻塞是为了等待任务的完成,然后该代码将任务添加到了主队列,主队列会将任务交给主线程执行,但此时主线程阻塞了,任务添加进了主线程得不到运行,而主线程在等待任务的执行,因此就造成了死锁

这个栗子一般人写不出来这样的代码,仅仅是为了讲解什么情况下会造成死锁,即,线程被阻塞需要等到任务执行完成,而任务由于线程阻塞得不到执行。前文举的几个串行队列的栗子很多是不能使用主队列的,原因也正在此。

dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
    dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
        NSLog(@"===== %@ %ld", [NSThread currentThread], t);
    });
 }); 

上述栗子也会造成死锁,因为,dispatch_apply同样会阻塞当前线程,它需要等待内部的dispatch_apply执行完成,内部的需要等待外部的线程来执行它,产生了死锁。

结论:使用sync同步函数当前串行队列中添加任务,会卡主当前的串行队列(产生死锁)。

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

推荐阅读更多精彩内容