iOS GCD (Grand Central Dispatch)

1. GCD简介

百度百科给出的定义是:

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用

GCD是基于C语言的,他负责创建线程和调度需要执行的任务,由系统直接提供线程管理。GCD有两个核心的概念:队列任务

1.1 队列

队列就是一个用来存放任务的集合,负责管理开发者提交的任务。系统会负责管理这些队列,并放到多个线程上执行。无须开发者直接管理。

队列会维护和使用一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。GCD的队列严格遵守FIFO(先进先出)的工作原则。添加到队列的工作单元将始终按照队列的启动顺序启动。

根据执行任务的不同,队列分为并发队列串行队列

  • 串行队列(Serial Dispatch Queue)
    串行队列底层的线程池只有一个线程,一次只能运行一个任务,前一个任务执行完成才能继续执行下一个任务。

  • 并发队列(Concurrent Dispatch Queue)
    串行队列底层的线程池提供了多个线程,可以按照FIFO的顺序并发启动、执行多个任务

1.2 任务

任务就是我们提交给队列的工作单元,也就是你在线程中执行的代码块。执行任务的方式有两种:同步执行异步执行。两者的主要区别就是:队列是否需要等待其他任务执行结束以及是否具备开启新线程的能力。

  • 同步执行(sync)
    同步执行就是只会在当前线程执行,不具备开启新线程的能力。开发者同步提交任务,可以避免竞争条件和其他同步错误。但是需要注意的是,同步提交任务会阻塞当前调用的线程,直到相应的任务执行完成。同步提交任务定义的两个函数如下:
///queue:将任务提交到的队列  block:要执行的任务
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
///queue:将任务提交到的队列  context:向函数传入应用程序定义的上下文  work:传入的其他需要执行的函数
void dispatch_sync_f(dispatch_queue_t queue,  void *_Nullable context, dispatch_function_t work);
  • 异步执行(async)
    异步执行就是会在新的线程中执行任务,具备开启新线程的能力。我们提交任务到队列时,无法确定什么时候能够执行(无需等待)。异步执行任务的函数如下:
///queue:将任务提交到的队列  block:要执行的任务
void dispatch_async(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
///queue:将任务提交到的队列  context:向函数传入应用程序定义的上下文  work:传入的其他需要执行的函数
void dispatch_async_f(dispatch_queue_t queue,  void *_Nullable context, dispatch_function_t work);

注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关.

针对不同的队列类型,同步和异步会产生不同的执行结果,如下图:

全局并行队列 创建串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁
异步(async) 有开启新线程,并行执行任务 有开启新线程,串行执行任务 没有开启新线程,串行执行任务

总结:同步和异步决定了是否开启新线程,并发和串行决定了任务的执行方式

2. GCD的创建方法

GCD的使用分为两步

  • 创建一个队列(串行或者并行)
  • 将任务提交到队列中,系统会根据任务类型执行任务(同步或者是异步执行)

2.1 创建(获取)队列

在GCD中,我们既可以手动创建一个新的队列,也可以获取系统为我们提供的队列。

  • 手动创建队列
    我们可以使用dispatch_queue_create函数来手动创建一个队列
dispatch_queue_t dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);

该函数有两个参数,第一个参数表示队列的唯一标示,用于在debug的时候查看。可以为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列。
DISPATCH_QUEUE_SERIAL :串行队列
DISPATCH_QUEUE_CONCURRENT :并发队列

  • 获取系统提供的队列
    系统为我们提供了两种队列,一个串行队列和一个并发队列。如下:
    系统提供了一种特殊的串行队列:主队列(Main Dispatch Queue)。主队列中的任务都会放到主线程中去执行。获取方式如下:
  ///主队列获取方法
  dispatch_queue_t queue = dispatch_get_main_queue();

GCD默认提供了一个全局并发队列(Global Dispatch Queue)

/// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

该函数需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可

2.2 创建任务

在上面我们已经讲过,GCD的任务执行方式有两种,同步执行异步执行。对应的系统提供了同步执行的创建方法dispatch_sync,异步执行的创建方法dispatch_async

/// 同步执行任务创建方法
dispatch_sync(queue, ^{
    /// 这里放同步执行任务代码
});
/// 异步执行任务创建方法
dispatch_async(queue, ^{
    /// 这里放异步执行任务代码
});

综上所述,我们可以了解到,我们有两种队列的创建方式串行队列并发队列,两种任务的执行方式同步执行异步执行。这样我们就有了四种不同的组合:

1. 同步执行 + 并发队列 
2. 同步执行 + 串行队列 
3. 异步执行 + 并发队列  
4. 异步执行 + 串行队列

实际上我们还有系统我我们提供的两种特殊队列:全局并发队列主队列。全局并发队列实际上我们可以把它理解成普通的并发队列来使用。但是主队列应为在主线程上执行,所以他有点特殊,这样我们又多了两种组合

5. 同步执行 + 主队列
6. 异步执行 + 主队列

3 GCD的使用

3.1 同步执行 + 并发队列

/**
 * 同步执行 + 并发队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 * 同步执行:不开启新线程
 */
- (void)syncConcurrent {

    NSLog(@"syncConcurrent--- %@ ---begin",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        /// 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        /// 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        /// 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncConcurrent--- %@ ---end",[NSThread currentThread]);
}
///执行结果
2018-11-08 10:37:50.949598+0800 iOS GCD[929:31034] syncConcurrent--- <NSThread: 0x604000066540>{number = 1, name = main} ---begin
2018-11-08 10:37:52.950297+0800 iOS GCD[929:31034] 1---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:37:54.951011+0800 iOS GCD[929:31034] 1---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:37:56.951856+0800 iOS GCD[929:31034] 2---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:37:58.952883+0800 iOS GCD[929:31034] 2---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:38:00.953523+0800 iOS GCD[929:31034] 3---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:38:02.953879+0800 iOS GCD[929:31034] 3---<NSThread: 0x604000066540>{number = 1, name = main}
2018-11-08 10:38:02.954061+0800 iOS GCD[929:31034] syncConcurrent--- <NSThread: 0x604000066540>{number = 1, name = main} ---end

从运行结果中我们可以看到:

  • 任务都是在当前线程(主线程)中执行,没有开启新线程。(同步执行不具备开启新线程的能力)
  • 任务在begin和end之间执行。(同步任务需要等待队列的任务执行结束)
  • 任务按顺序执行原因:虽然并发队里可以开启多个线程同时执行任务,但是创建的任务的同步任务。同步任务执行不具备开启新线程的能力,所以就不能并发执行。也就是只能串行执行。

3.2 同步执行 + 串行队列

/**
 * 同步执行 + 串行队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 * 同步执行:不开启新线程
 */
- (void)syncConcurrent {

    NSLog(@"syncConcurrent--- %@ ---begin",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        /// 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        /// 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        /// 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncConcurrent--- %@ ---end",[NSThread currentThread]);
}
///执行结果
2018-11-08 10:53:32.296206+0800 iOS GCD[963:37026] syncConcurrent--- <NSThread: 0x60400006bc40>{number = 1, name = main} ---begin
2018-11-08 10:53:34.296791+0800 iOS GCD[963:37026] 1---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:36.298250+0800 iOS GCD[963:37026] 1---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:38.299682+0800 iOS GCD[963:37026] 2---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:40.301198+0800 iOS GCD[963:37026] 2---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:42.302730+0800 iOS GCD[963:37026] 3---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:44.304199+0800 iOS GCD[963:37026] 3---<NSThread: 0x60400006bc40>{number = 1, name = main}
2018-11-08 10:53:44.304465+0800 iOS GCD[963:37026] syncConcurrent--- <NSThread: 0x60400006bc40>{number = 1, name = main} ---end

从运行结果中我们可以看到:

  • 任务都是在当前线程(主线程)中执行,没有开启新线程。(同步执行不具备开启新线程的能力)
  • 任务在begin和end之间执行。(同步任务需要等待队列的任务执行结束)
  • 任务按顺序执行原因:串行队列本身就是任务就是按照顺序执行,再加上同步执行,不具备开启新线程的能力,所以任务就会在主线程串行执行。

3.3 异步执行 + 并发队列

/**
 * 异步执行 + 并发队列
 * 特点:开启新线程,任务同时执行。
 */
- (void)syncConcurrent {

    NSLog(@"syncConcurrent--- %@ ---begin",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        /// 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncConcurrent--- %@ ---end",[NSThread currentThread]);
}
///执行结果
2018-11-08 10:57:50.012065+0800 iOS GCD[981:39765] syncConcurrent--- <NSThread: 0x60400007d5c0>{number = 1, name = main} ---begin
2018-11-08 10:57:50.012305+0800 iOS GCD[981:39765] syncConcurrent--- <NSThread: 0x60400007d5c0>{number = 1, name = main} ---end
2018-11-08 10:57:52.013674+0800 iOS GCD[981:39833] 2---<NSThread: 0x600000472d00>{number = 3, name = (null)}
2018-11-08 10:57:52.013674+0800 iOS GCD[981:39836] 1---<NSThread: 0x600000472d40>{number = 4, name = (null)}
2018-11-08 10:57:52.013674+0800 iOS GCD[981:39834] 3---<NSThread: 0x604000267dc0>{number = 5, name = (null)}
2018-11-08 10:57:54.019337+0800 iOS GCD[981:39833] 2---<NSThread: 0x600000472d00>{number = 3, name = (null)}
2018-11-08 10:57:54.019337+0800 iOS GCD[981:39836] 1---<NSThread: 0x600000472d40>{number = 4, name = (null)}
2018-11-08 10:57:54.019353+0800 iOS GCD[981:39834] 3---<NSThread: 0x604000267dc0>{number = 5, name = (null)}

从运行结果中我们可以看到:

  • 任务在新线程中执行,并且任务没有等待,是同时执行(异步执行具有开启新线程的能力,并发队列可以开启多个线程同时执行)。

3.4 异步执行 + 串行队列

/**
 * 异步执行 + 串行队列
 * 特点:开启新线程,任务按顺序执行。
 */
- (void)syncConcurrent {

    NSLog(@"syncConcurrent--- %@ ---begin",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        /// 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncConcurrent--- %@ ---end",[NSThread currentThread]);
}
///执行结果
2018-11-08 11:24:53.262898+0800 iOS GCD[646:11217] syncConcurrent--- <NSThread: 0x60000007bc80>{number = 1, name = main} ---begin
2018-11-08 11:24:53.263132+0800 iOS GCD[646:11217] syncConcurrent--- <NSThread: 0x60000007bc80>{number = 1, name = main} ---end
2018-11-08 11:24:55.266359+0800 iOS GCD[646:11395] 1---<NSThread: 0x600000460a00>{number = 3, name = (null)}
2018-11-08 11:24:57.270609+0800 iOS GCD[646:11395] 1---<NSThread: 0x600000460a00>{number = 3, name = (null)}
2018-11-08 11:24:59.275577+0800 iOS GCD[646:11395] 2---<NSThread: 0x600000460a00>{number = 3, name = (null)}
2018-11-08 11:25:01.281035+0800 iOS GCD[646:11395] 2---<NSThread: 0x600000460a00>{number = 3, name = (null)}
2018-11-08 11:25:03.282101+0800 iOS GCD[646:11395] 3---<NSThread: 0x600000460a00>{number = 3, name = (null)}
2018-11-08 11:25:05.287519+0800 iOS GCD[646:11395] 3---<NSThread: 0x600000460a00>{number = 3, name = (null)}

从运行结果中我们可以看到:

  • 开起了一条新线程,(异步执行允许开启新线程,串行允许开启一条线程)。
  • 所有任务是在打印的begin和end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(串行任务按顺序依次执行)。

3.5 同步执行 + 主队列

同步执行+主队列会造成线程卡死。最终到时程序崩溃。具体原因分析如下:

  • 同步执行不具备开启新线程的能力,主队列即串行队列。
  • 我们创建队列的代码在主线程执行,任务也在主线程执行。
  • 主线程首先创建执行我们创建GCD的代码,在执行到执行任务模块,等到任务执行完成才会继续执行下面的代码(任务依次执行)。
  • 我们的任务也要在主线程执行,目前主线程已经有创建GCD的任务还没有执行完成,所以它会等到创建GCD的代码执行才会开始执行任务。
  • 这样就造成了两个任务相互等待的情况。造成主线程卡死。

注意 :通过上面的分析,同步执行 + 主队列的情况下,我们在子线程执行创建GCD的任务,将任务提交到主队列,这时不会造成卡死情况。因为没有相互等待的情况发生。(创建GCD的任务被提交到了子线程)。

3.6 异步执行 + 主队列

/**
 * 异步执行 + 主队列
 * 特点:开启新线程,任务按顺序执行。
 */
- (void)syncConcurrent {

    NSLog(@"syncConcurrent--- %@ ---begin",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        /// 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        /// 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncConcurrent--- %@ ---end",[NSThread currentThread]);
}
///执行结果
2018-11-08 11:39:45.317169+0800 iOS GCD[689:16564] syncConcurrent--- <NSThread: 0x604000070040>{number = 1, name = main} ---begin
2018-11-08 11:39:45.317404+0800 iOS GCD[689:16564] syncConcurrent--- <NSThread: 0x604000070040>{number = 1, name = main} ---end
2018-11-08 11:39:47.322342+0800 iOS GCD[689:16564] 1---<NSThread: 0x604000070040>{number = 1, name = main}
2018-11-08 11:39:49.323635+0800 iOS GCD[689:16564] 1---<NSThread: 0x604000070040>{number = 1, name = main}
2018-11-08 11:39:51.325077+0800 iOS GCD[689:16564] 2---<NSThread: 0x604000070040>{number = 1, name = main}
2018-11-08 11:39:53.326577+0800 iOS GCD[689:16564] 2---<NSThread: 0x604000070040>{number = 1, name = main}
2018-11-08 11:39:55.328106+0800 iOS GCD[689:16564] 3---<NSThread: 0x604000070040>{number = 1, name = main}
2018-11-08 11:39:57.329529+0800 iOS GCD[689:16564] 3---<NSThread: 0x604000070040>{number = 1, name = main}

从运行结果中我们可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的begin和end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。

4 GCD线程之间的通信

在日常的开发中,我们将一些比较耗时的任务放在子线程中执行,在某些情况下,子线程任务执行完成之后需要再次回到主线程刷新UI。这时候就用到了线程的通信。

///线程间通信
- (void)communication {
    /// 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(queue, ^{
        /// 异步追加任务
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
        
        /// 回到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            /// 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        });
    });
}
///执行结果
2018-11-08 13:59:58.159160+0800 iOS GCD[902:52144] 1---<NSThread: 0x60400007b480>{number = 3, name = (null)}
2018-11-08 14:00:00.162415+0800 iOS GCD[902:52144] 1---<NSThread: 0x60400007b480>{number = 3, name = (null)}
2018-11-08 14:00:02.163904+0800 iOS GCD[902:52067] 2---<NSThread: 0x600000070900>{number = 1, name = main}

5 GCD 之 dispatch_once

dispatch_once函数是保证在应用程序执行中只执行一次处理的API。即使是在多线程环境下,该函数也可以保证线程安全。

- (void)dispatchOnce {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ///执行代码
    });
}

6 GCD 之 dispatch_apply

dispatch_apply函数是dispatch_sync函数和Dispatch Group关联的API。该函数按指定的次数将指定的block追加到指定的Dispath Queue中,并等待全部处理执行结束。

void dispatch_apply(size_t iterations, dispatch_queue_t queue, DISPATCH_NOESCAPE void (^block)(size_t));

第一参数为循环的次数,第二个参数为循环队列。如果为并发队列。则循环异步执行。如果是串行队列,则任务同步执行。以下为并发队列演示代码:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zd  ---   %@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
///执行结果
2018-11-08 17:38:10.029500+0800 iOS GCD[2056:148014] apply---begin
2018-11-08 17:38:10.029790+0800 iOS GCD[2056:148058] 1  ---   <NSThread: 0x6040004603c0>{number = 3, name = (null)}
2018-11-08 17:38:10.029788+0800 iOS GCD[2056:148014] 0  ---   <NSThread: 0x6000002616c0>{number = 1, name = main}
2018-11-08 17:38:10.029837+0800 iOS GCD[2056:148056] 3  ---   <NSThread: 0x60000046cb00>{number = 4, name = (null)}
2018-11-08 17:38:10.029855+0800 iOS GCD[2056:148055] 2  ---   <NSThread: 0x6040004604c0>{number = 5, name = (null)}
2018-11-08 17:38:10.029979+0800 iOS GCD[2056:148058] 4  ---   <NSThread: 0x6040004603c0>{number = 3, name = (null)}
2018-11-08 17:38:10.030072+0800 iOS GCD[2056:148014] 5  ---   <NSThread: 0x6000002616c0>{number = 1, name = main}
2018-11-08 17:38:10.030189+0800 iOS GCD[2056:148055] 7  ---   <NSThread: 0x6040004604c0>{number = 5, name = (null)}
2018-11-08 17:38:10.030189+0800 iOS GCD[2056:148056] 6  ---   <NSThread: 0x60000046cb00>{number = 4, name = (null)}
2018-11-08 17:38:10.030235+0800 iOS GCD[2056:148058] 8  ---   <NSThread: 0x6040004603c0>{number = 3, name = (null)}
2018-11-08 17:38:10.030328+0800 iOS GCD[2056:148014] 9  ---   <NSThread: 0x6000002616c0>{number = 1, name = main}
2018-11-08 17:38:10.031784+0800 iOS GCD[2056:148014] apply---end

7 GCD 之 dispatch_semaphore(信号量)

GCD中的Dispatch Semaphore是持有计数的信号。就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。在 Dispatch Semaphore 中,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。

其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。

举个例子:高速公路收费站有两个通过口,所以最多可以容纳两辆车同时通过,但是这时候来了三辆车过了,我们就可以控制先来的两辆车先过,第三辆等待,等前面有一辆车通过了第三辆车再走。

Dispatch Semaphore 提供了三个函数。

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

下面我们按照刚才举得这个例子为例,看看代码如何实现:

- (void)dispatchSemaphore {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        ///信号总量减一 (可以理解为当前任务占用一个收费口,还剩一个收费口)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"第1辆车 进入 收费站1号窗口");
        sleep(1);
        NSLog(@"第1辆车 驶出 收费站1号窗口");
        ///信号总量加一 (继续保持2个收费口数量)
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"第2辆车 进入 收费站1号窗口");
        sleep(1);
        NSLog(@"第2辆车 驶出 收费站1号窗口");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"第3辆车 进入 收费站1号窗口");
        sleep(1);
        NSLog(@"第3辆车 驶出 收费站1号窗口");
        dispatch_semaphore_signal(semaphore);
    });
}
///运行结果
2018-11-09 11:56:47.264024+0800 iOS GCD[1598:54884] 第2辆车 进入 收费站1号窗口
2018-11-09 11:56:47.264026+0800 iOS GCD[1598:54882] 第1辆车 进入 收费站1号窗口
2018-11-09 11:56:48.264409+0800 iOS GCD[1598:54884] 第2辆车 驶出 收费站1号窗口
2018-11-09 11:56:48.264409+0800 iOS GCD[1598:54882] 第1辆车 驶出 收费站1号窗口
2018-11-09 11:56:48.264616+0800 iOS GCD[1598:54885] 第3辆车 进入 收费站1号窗口
2018-11-09 11:56:49.265146+0800 iOS GCD[1598:54885] 第3辆车 驶出 收费站1号窗口

8 GCD 之 dispatch_group (队列组)

GCD的队列组可以将多个任务组合成一组,用于监听这一组任务是否全部完成。知道关联的任务全部完成之后再发出通知以执行其他操作。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合 来实现
    dispatch_group_async。
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。

8.1 dispatch_group_notify

监听任务的执行情况,全部任务执行完成之后,追加到Group中,执行其他任务。

- (void)dispatchGroup {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    ///任务一
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"1-----%d-----",i);
            sleep(2);
        }
    });
    ///任务二
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"2-----%d-----",i);
            sleep(2);
        }
    });
    ///任务三
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"3-----%d-----",i);
            sleep(2);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"4-----%d-----",i);
            sleep(2);
        }
    });
}
///执行结果
2018-11-09 14:18:07.450983+0800 iOS GCD[1782:97016] 3-----0-----
2018-11-09 14:18:07.450983+0800 iOS GCD[1782:97017] 2-----0-----
2018-11-09 14:18:07.450983+0800 iOS GCD[1782:97018] 1-----0-----
2018-11-09 14:18:09.456447+0800 iOS GCD[1782:97016] 3-----1-----
2018-11-09 14:18:09.456452+0800 iOS GCD[1782:97018] 1-----1-----
2018-11-09 14:18:09.456447+0800 iOS GCD[1782:97017] 2-----1-----
2018-11-09 14:18:11.459356+0800 iOS GCD[1782:97017] 4-----0-----
2018-11-09 14:18:13.464384+0800 iOS GCD[1782:97017] 4-----1-----

从执行结果可以看出,在所有任务都执行完成之后,才会执行dispatch_group_notify中的代码。

8.2 dispatch_group_wait

暂停(阻塞)当前线程,等待执行的Group都完成之后,才会继续执行。

- (void)dispatchGroup {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    ///任务一
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"1-----%d-----",i);
            sleep(2);
        }
    });
    ///任务二
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"2-----%d-----",i);
            sleep(2);
        }
    });
    ///任务三
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"3-----%d-----",i);
            sleep(2);
        }
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"4------------");
}
///执行结果
2018-11-09 14:23:25.062623+0800 iOS GCD[1802:99515] 2-----0-----
2018-11-09 14:23:25.062624+0800 iOS GCD[1802:99516] 3-----0-----
2018-11-09 14:23:25.062624+0800 iOS GCD[1802:99541] 1-----0-----
2018-11-09 14:23:27.065677+0800 iOS GCD[1802:99541] 1-----1-----
2018-11-09 14:23:27.065681+0800 iOS GCD[1802:99515] 2-----1-----
2018-11-09 14:23:27.065677+0800 iOS GCD[1802:99516] 3-----1-----
2018-11-09 14:23:29.066620+0800 iOS GCD[1802:99443] 4------------

从执行结果可以看出,dispatch_group_wait会阻塞当前线程。在group任务都执行完成之后,才继续往下执行。

8.3 dispatch_group_enter、dispatch_group_leave

一般在网络请求中,会使用此组合方法控制网络请求顺序。

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。
  • 当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
- (void)dispatchGroup {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    ///任务一
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"1-----%d-----",i);
            sleep(2);
        }
        dispatch_group_leave(group);
    });
    ///任务二
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"2-----%d-----",i);
            sleep(2);
        }
        dispatch_group_leave(group);
    });
    ///任务三
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"3-----%d-----",i);
            sleep(2);
        }
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, queue, ^{
        NSLog(@"4-----------");
    });
}
///执行结果
2018-11-09 14:34:03.145127+0800 iOS GCD[1860:105502] 3-----0-----
2018-11-09 14:34:03.145127+0800 iOS GCD[1860:105501] 1-----0-----
2018-11-09 14:34:03.145127+0800 iOS GCD[1860:105504] 2-----0-----
2018-11-09 14:34:05.148997+0800 iOS GCD[1860:105501] 1-----1-----
2018-11-09 14:34:05.148997+0800 iOS GCD[1860:105502] 3-----1-----
2018-11-09 14:34:05.148997+0800 iOS GCD[1860:105504] 2-----1-----
2018-11-09 14:34:07.151059+0800 iOS GCD[1860:105501] 4-----------

从执行结果可以看出,在所有任务都执行完成之后,才会执行dispatch_group_notify中的代码。

9 GCD 之 dispatch_after (延时任务)

dispatch_after一般用于执行延时任务,但是需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

- (void)dispatchAfter{
    NSLog(@"----------begin----%@",[NSThread currentThread]);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"----------延时任务--%@",[NSThread currentThread]);
    });
}
///执行结果
2018-11-09 14:42:19.681291+0800 iOS GCD[1915:110037] ----------begin----<NSThread: 0x604000261e40>{number = 1, name = main}
2018-11-09 14:42:22.975073+0800 iOS GCD[1915:110037] ----------延时任务--<NSThread: 0x604000261e40>{number = 1, name = main}

10 GCD 之 dispatch_barrier_async 栅栏方法

在某些特殊情景下,我们需要执行两组操作,需要在第一组操作完成之后再执行第二组操作,在此类需求下,我们可以虽然dispatch_groupdispatch_set_target_queue可以解决,但是实现过于复杂。这时我们可以使用dispatch_barrier_async方法。此函数更加的聪明。

他就像一个栅栏一样,将这两组任务分离开来。dispatch_barrier_async函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

注意
1.该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用(同dispatch_get_global_queue函数无效)
2.如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用。

dispatch_barrier_async.png

代码演示如下:

- (void)dispatch_barrier_sync {
    dispatch_queue_t queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"-----start-----");
    ///任务一
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            sleep(2);
            NSLog(@"1-----%d-----%@",i,[NSThread currentThread]);
        }
    });
    ///任务二
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            sleep(2);
            NSLog(@"2-----%d-----%@",i,[NSThread currentThread]);
        }
    });
    dispatch_barrier_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            sleep(2);
            NSLog(@"dispatch_barrier_sync-----%d-----%@",i,[NSThread currentThread]);
        }
    });
    NSLog(@"-----barrier-----");
    ///任务三
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            sleep(2);
            NSLog(@"3-----%d-----%@",i,[NSThread currentThread]);
        }
    });
    ///任务四
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            sleep(2);
            NSLog(@"4-----%d-----%@",i,[NSThread currentThread]);
        }
    });
    NSLog(@"-----end-----");
}
///执行结果
2018-11-09 16:02:31.880398+0800 iOS GCD[2436:156316] -----start-----
2018-11-09 16:02:31.880669+0800 iOS GCD[2436:156316] -----barrier-----
2018-11-09 16:02:31.880965+0800 iOS GCD[2436:156316] -----end-----
2018-11-09 16:02:33.883236+0800 iOS GCD[2436:156454] 1-----0-----<NSThread: 0x600000273f40>{number = 4, name = (null)}
2018-11-09 16:02:33.883236+0800 iOS GCD[2436:156452] 2-----0-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:35.888045+0800 iOS GCD[2436:156452] 2-----1-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:35.888045+0800 iOS GCD[2436:156454] 1-----1-----<NSThread: 0x600000273f40>{number = 4, name = (null)}
2018-11-09 16:02:37.893215+0800 iOS GCD[2436:156452] dispatch_barrier_sync-----0-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:39.895096+0800 iOS GCD[2436:156452] dispatch_barrier_sync-----1-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:41.896703+0800 iOS GCD[2436:156454] 4-----0-----<NSThread: 0x600000273f40>{number = 4, name = (null)}
2018-11-09 16:02:41.896703+0800 iOS GCD[2436:156452] 3-----0-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:43.899571+0800 iOS GCD[2436:156452] 3-----1-----<NSThread: 0x600000273e80>{number = 3, name = (null)}
2018-11-09 16:02:43.899571+0800 iOS GCD[2436:156454] 4-----1-----<NSThread: 0x600000273f40>{number = 4, name = (null)}

从结果可以看到,dispatch_barrier_async将任务切分成了两组,前面的任务执行完成之后,执行dispatch_barrier_async追加的任务。dispatch_barrier_async追加的任务完成之后,执行后面的任务。

我们将dispatch_barrier_async切换为dispatch_barrier_sync时,执行结果如下:

2018-11-09 16:06:27.950577+0800 iOS GCD[2476:159238] -----start-----<NSThread: 0x6000000693c0>{number = 1, name = main}
2018-11-09 16:06:29.952112+0800 iOS GCD[2476:159330] 1-----0-----<NSThread: 0x60400027cec0>{number = 4, name = (null)}
2018-11-09 16:06:29.952115+0800 iOS GCD[2476:159332] 2-----0-----<NSThread: 0x600000262380>{number = 3, name = (null)}
2018-11-09 16:06:31.953935+0800 iOS GCD[2476:159330] 1-----1-----<NSThread: 0x60400027cec0>{number = 4, name = (null)}
2018-11-09 16:06:31.953935+0800 iOS GCD[2476:159332] 2-----1-----<NSThread: 0x600000262380>{number = 3, name = (null)}
2018-11-09 16:06:33.955469+0800 iOS GCD[2476:159238] dispatch_barrier_sync-----0-----<NSThread: 0x6000000693c0>{number = 1, name = main}
2018-11-09 16:06:35.955883+0800 iOS GCD[2476:159238] dispatch_barrier_sync-----1-----<NSThread: 0x6000000693c0>{number = 1, name = main}
2018-11-09 16:06:35.956068+0800 iOS GCD[2476:159238] -----barrier-----<NSThread: 0x6000000693c0>{number = 1, name = main}
2018-11-09 16:06:35.956225+0800 iOS GCD[2476:159238] -----end-----<NSThread: 0x6000000693c0>{number = 1, name = main}
2018-11-09 16:06:37.959516+0800 iOS GCD[2476:159332] 4-----0-----<NSThread: 0x600000262380>{number = 3, name = (null)}
2018-11-09 16:06:37.959516+0800 iOS GCD[2476:159331] 3-----0-----<NSThread: 0x60400027aa40>{number = 5, name = (null)}
2018-11-09 16:06:39.961185+0800 iOS GCD[2476:159331] 3-----1-----<NSThread: 0x60400027aa40>{number = 5, name = (null)}
2018-11-09 16:06:39.961185+0800 iOS GCD[2476:159332] 4-----1-----<NSThread: 0x600000262380>{number = 3, name = (null)}

通过分析可以看出:

  • dispatch_barrier_sync需要等待执行完栅栏前面的任务之后,才会追加栅栏之后的任务。
  • dispatch_barrier_async无需等待栅栏执行完,会将任务都追加到队列里面,只是暂时不执行而已。

11 GCD 之 dispatch_set_target_queue

dispatch_set_target_queue主要有两个主要功能

  1. 使用dispatch_set_target_queue更改Dispatch Queue的执行优先级
    dispatch_queue_create函数生成的DisPatch Queue不管是Serial DisPatch Queue还是Concurrent Dispatch Queue,执行的优先级都与默认优先级的Global Dispatch queue相同,如果需要变更生成的Dispatch Queue的执行优先级则需要使用dispatch_set_target_queue函数
- (void)testTeagerQueue1 {
     dispatch_queue_t serialQueue = dispatch_queue_create("com.oukavip.www",NULL);
     dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
     dispatch_set_target_queue(serialQueue, globalQueue);
     // 第一个参数为要设置优先级的queue,第二个参数是参照物,既将第一个queue的优先级和第二个queue的优先级设置一样。
 }

注意:不要将系统系统的Main Dispatch Queue和Global Dispatch Queue修改优先级,因为有可能出现不可控状况。

  1. 使用dispatch_set_target_queue修改用户队列的目标队列,使多个serial queue在目标queue上一次只有一个执行

注意:我们需要阐述一下生成多个Serial DisPatch Queue时的注意事项
Serial DisPatch Queue是一个串行队列,只能同时执行1个追加处理(即任务),当用Dispatch_queue_create函数生成多个Serial DisPatch Queue时,每个Serial DisPatch Queue均获得一个线程,即多个Serial DisPatch Queue可并发执行,同时处理添加到各个Serial DisPatch Queue中的任务,但要注意如果过多地使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能,所以我们只在为了避免多个线程更新相同资源导致数据竞争时,使用Serial DisPatch Queue

第一种情况:使用dispatch_set_target_queue(Dispatch Queue1, Dispatch Queue2)实现队列的动态调度管理

  - (void)testTargetQueue2 {
      //创建一个串行队列queue1
      dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
      //创建一个串行队列queue2
      dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
      
      //使用dispatch_set_target_queue()实现队列的动态调度管理
      dispatch_set_target_queue(queue1, queue2);
      
 /*
     <*>dispatch_set_target_queue(Dispatch Queue1, Dispatch Queue2);
     那么dispatchA上还未运行的block会在dispatchB上运行。这时如果暂停dispatchA运行:
     
     <*>dispatch_suspend(dispatchA);
     这时则只会暂停dispatchA上原来的block的执行,dispatchB的block则不受影响。而如果暂停dispatchB的运行,则会暂停dispatchA的运行。
     
     这里只简单举个例子,说明dispatch队列运行的灵活性,在实际应用中你会逐步发掘出它的潜力。
     
     dispatch队列不支持cancel(取消),没有实现dispatch_cancel()函数,不像NSOperationQueue,不得不说这是个小小的缺憾
      
 */
     dispatch_async(queue1, ^{
         for (NSInteger i = 0; i < 10; i++) {
             NSLog(@"queue1:%@, %ld", [NSThread currentThread], i);
             [NSThread sleepForTimeInterval:0.5];
             if (i == 5) {
                 dispatch_suspend(queue2);
             }
         }
     });
     
     dispatch_async(queue1, ^{
         for (NSInteger i = 0; i < 100; i++) {
             NSLog(@"queue1:%@, %ld", [NSThread currentThread], i);
         }
         
     });
     
     dispatch_async(queue2, ^{
         for (NSInteger i = 0; i < 100; i++) {
             NSLog(@"queue2:%@, %ld", [NSThread currentThread], i);
         }
     });
     
 }

第二种情况:使用dispatch_set_target_queue将多个串行的queue指定到了同一目标,那么着多个串行queue在目标queue上就是同步执行的,不再是并行执行。

- (void)testTargetQueue {
    //1.创建目标队列
    dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);
    
    //2.创建3个串行队列
    dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
    
    //3.将3个串行队列分别添加到目标队列
    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);
    dispatch_set_target_queue(queue3, targetQueue);
    
    
    dispatch_async(queue1, ^{
        NSLog(@"1 in");
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"1 out");
    });
    
    dispatch_async(queue2, ^{
        NSLog(@"2 in");
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"2 out");
    });
    dispatch_async(queue3, ^{
        NSLog(@"3 in");
        [NSThread sleepForTimeInterval:1.f];
        NSLog(@"3 out");
    });
}

12 dispatch_suspend / dispatch_resume

在我们追加大量任务到dispatch_Queue中时,有时希望不执行已经追加的任务。在这种情况下可以使用挂起函数dispatch_suspend,当可以执行时在恢复即可dispatch_resume

  • dispatch_suspend(queue):挂起指定的Queue
  • dispatch_resume(queue):恢复指定的Queue

这两个函数对已经执行的任务没有影响。挂起后,追加到dispatch_Queue中但尚未处理的任务在此之后停止执行。而恢复则使得这些任务继续执行。

本文参考

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

推荐阅读更多精彩内容