iOS 中实现多线程的四种方案

一: 多线程的基本概念
1.同步与异步的概念
1.1 同步 必须等待当前语句执行完毕,才可以执行下一个语句。
1.2异步 不用等待当前语句执行完毕,就可以执行下一个语句。

2.进程与线程的概念
2.1进程
(1)进程的概念:系统中正在运行的应用程序。
(2)进程的特点:每个进程都运行在其专用且受保护的内存空间,不同的进程之间相互独立,互不干扰。
2.2线程
(1)线程的概念: 一个进程要想执行任务,必须得有线程 (每一个进程至少要有一条线程) 线程是进程的基本执行单元,一个进程的所有任务都是在线程中执行的。
(2)线程的特点:
一条线程在执行任务的时候是串行(按顺序执行)的。如果要让一条线程执行多个任务,那么只能一个一个地按顺序执行这些任务。也就是说,在同一时间,一条线程只能执行一个任务

3.多线程的概念和原理
3.1多线程的概念: 1个进程可以开启多条线程,多条线程可以并发(同时)执行不同的任务。
3.2多线程的原理: 同一时间,CPU只能处理一条线程,即只有一条线程在工作多线程同时执行,其实是CPU快速地在多条线程之间进行切换。如果CPU调度线程的速度足够快,就会造成多线程并发执行的”假象” 如图:

多线程的原理.png

4.多线程的优缺点
4.1优点
1> 能适当提高程序的执行效率
2> 能适当提高资源的利用率(CPU、内存利用率)
4.2缺点
1> 开启线程需要占用一定的内存空间(默认情况下,每一条线程都占用512KB),如果开启大量的线程,会占用大量的内存空间,从而降低程序的性能。
2> 线程越多,CPU在调度线程上的开销就越大。
3> 线程越多,程序设计就会更复杂:比如 线程间通讯、多线程的数据共享等。
4.3总结
实际上,使用多线程,由于会开线程,必然就会消耗性能,但是却可以提高用户体验。所以,综合考虑,在保证良好的用户体验的前提下,可以适当地开线程,一般开3-6条。

二:iOS 中实现多线程的方案有以下四种:pthread,NSThread,GCD,NSOperation,其各自的优缺点如下图:

四种多线程方案的对比.png

{1}: pthread
pthread是一种纯 C 语言提供的多线程方案,使用 pthread 非常麻烦,基本已经没有人使用了,这里只做简单的介绍

//创建线程的方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//创建子线程(这个在后面需要用到)
    pthread_t pthread; //线程编号
/*
pthread_create(pthread_t *restrict, const pthread_attr_t *restrict, void *(*)(void *), void *restrict)
参数的含义:
    1: &pthread:线程编号的地址。这个需要在之前创建完成之后在使用刚才创建好的线程的地址:&pthread。
    2: NULL:线程的属性:pthread_attr_t主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。在pthread_create中,把第二个参数设置为NULL的话,将采用默认的属性配置(一般情况下都是使用NULL来表示使用默认的属性)。
    3: demo:线程需要执行的函数 类型是void*(作用是可以指向任何类型的指针,类似于OC语言中的id)。
    4: NULL:函数中需要传递的参数,这里面携程NULL意思就是没有参数需要传递。如果想要传递多个参数,需要使用结构体将参数封装之后将结构体指针传递给线程。(用的少,不用多做研究,也不用自己探索)。
    5: int 类型的result:只需要知道0表示成功就可以了。
*/
    int result =  pthread_create(&pthread, NULL, demo, NULL);

    NSLog(@"touchesBegan  %@",[NSThread currentThread]);

    if (result == 0) {
        NSLog(@"成功");
    }else {
        NSLog(@"失败");
    }
}
void * demo(void *param) {
    NSLog(@"hello %@",[NSThread currentThread]);
    return NULL;
}

{2}: NSThread
NSThread 是对 pthread 进行了面向对象的封装,更加简单易用,但是,还是得程序猿手动管理线程,所以用的不多,据我了解,应该已经没有人使用 NSThread 实现整套的多线程方案, 只是会用到其中的个别方法,例如,使用[NSThread currentThread]打印当前的线程.
NSThread 创建线程的三种方法:

1/ 使用 alloc init
(1)对象方法:- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
(2)方法的作用:创建一个线程对象,执行 Target 的 selector 方法,并且可以给 selector 方法传递一个 object 参数。
(3)参数介绍:
1> Target:表示提供 selector 方法的对象,大多数情况(99%)是 self,但是也有可能不是self。
注意: 这里的 Target 并不一定是self,主要要看 selector 方法是谁的方法,是谁的方法,Target就写谁。
2> selector:交给线程执行的方法
3> object:传递给selector方法的参数。这个例子中,没有传递参数(object是nil)。
(4)通过 alloc init 方法创建线程对象,需要调用 start 方法来启动线程。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方式1: alloc init
    NSThread *thread = [[NSThread alloc] initWithTarget: self selector:@selector(demo) object:nil];
     // 启动线程   
    [thread start];
}

- (void)demo {
    NSLog(@"hello %@", [NSThread currentThread]);
}

2/ 使用 detach 方法
(1)类方法:+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
(2) 方法的作用: 创建并启动线程(实例化一个线程对象之后直接启动线程)
(3) 方法的参数:
1> Selector: 交给线程执行的方法
2> Target: 提供selector方法的对象,通常是self
3> Object: 要传递给 Selector 方法的参数
(4) 通过 detach 类方法创建线程,不需要调用 start 方法来启动线程,因为这个方法的底层会自动调用 start 方法。

- (void)viewDidLoad {
    [super viewDidLoad];    
    // 方式2: detach
    [NSThread detachNewThreadSelector:@selector(demo) toTarget:self withObject:nil];    
}
- (void)demo {
    NSLog(@"hello %@",[NSThread currentThread]);
}

3/ 使用perform 方法
(1) NSObject分类方法:- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
(2) 方法的作用: 隐式(没有任何跟线程相关的字眼)创建并启动线程
(3) 参数介绍:
1> selector: 交给线程执行的方法
2> Object:要传递给 selector 方法的参数

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方式3: perform, 跟这类似的方法还有很多,可以点进去查看
    [self performSelectorInBackground:@selector(demo) withObject:nil];
}
- (void)demo {
    NSLog(@"hello %@",[NSThread currentThread]);
}

线程状态:
如图:

线程状态.png

根据示意图可知: 线程的状态包括: 新建、就绪、运行、阻塞、死亡。
(1)新建(New): 通过 alloc init 方法创建出来一个线程对象,这个时候,线程对象就是新建状态。
(2)就绪(Runnable): 给线程对象发送一个start消息(即调用start方法),线程对象就会被添加到”可调度线程池”,等待CPU调度,这个时候,线程对象就是就绪状态。CPU只会调度可调度线程池里面的线程对象
注意:detach 方法和performSelectorInBackground方法创建线程之后会自动调用start方法,将线程对象添加到”可调度线程池”
(3)运行(Running): 当CPU调度到当前线程的时候,当前线程就是运行状态。
注意:线程执行完成之前,会一直在就绪状态和运行状态之间来回切换.并且这种切换是由CPU负责的,程序员不能干预.
(4)阻塞(Blocked): 当满足某个预定条件时,可以使用休眠或者线程同步来阻塞线程的执行。
休眠

  • (void)sleepUntilDate:(NSDate *)date; // 让线程休眠到指定时间
  • (void)sleepForTimeInterval:(NSTimeInterval)ti; // 让线程休眠一段时间
    线程同步(互斥锁)
    @synchronized(self) {
    }
    (5)死亡(Dead):
    1.正常死亡:线程将指定方法中的所有代码执行完毕,并且没有被复用,就会正常死亡.
    2.非正常死亡:调用 exit 方法,可以强行终止当前线程。线程被终止后,后续的代码就不会再执行。

线程属性:
1 name 属性

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 新建
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:@"xxx"];
    // 设置线程的名称
    thread1.name = @"A";
    // 启动线程
    [thread1 start];
}

- (void)demo {
    NSLog(@"%@", [NSThread currentThread]);
}

打印结果

打印结果.png

(1) 含义: 线程的名称。
(2) 作用: 在多线程开发时,可以用来判断到底是哪个线程在执行指定的任务.在企业级开发中,当程序发生崩溃的时候,可以帮助我们快速定位问题所在.

2 threadPriority(了解)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 新建
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:@"xxx"];
    // 设置线程的名称
    thread1.name = @"t1";
    // 设置线程的优先级
    thread1.threadPriority = 1.0;
    // 启动线程
    [thread1 start];  
    
    // 新建
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:@"xxx"];
    // 设置线程的名称
    thread2.name = @"t2";
    // 设置线程的优先级
    thread2.threadPriority = 0.0;
    // 启动线程
    [thread2 start];
}

- (void)demo {
    for (int i = 0; i < 20; i++) {
        NSLog(@"%d %@", i, [NSThread currentThread]);
    }
}

(1)含义: 线程的优先级
(2)取值: 取值范围 0.0 – 1.0,默认是 0.5,1.0表示优先级最高
(3)使用注意: 优先级高,只表示当前线程被CPU调度的频率相对较高。并不表示CPU会先调度优先级高的线程执行完它的所有任务,才会去调度优先级低的线程去执行任务。

3 stackSize

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    /** 操作主线程 */
    NSLog(@"主线程默认 %tu", [NSThread currentThread].stackSize / 1024);
    // 设置主线程的stackSize
    [NSThread currentThread].stackSize = 1024 * 1024;
    NSLog(@"主线程修改 %tu", [NSThread currentThread].stackSize / 1024);
    
    /** 操作子线程 */
    NSThread *thread = [[NSThread alloc] init];
    NSLog(@"thread默认 %tu", thread.stackSize / 1024);
    // 设置子线程的stackSize
    thread.stackSize = 8 * 1024;
    NSLog(@"thread修改 %tu", thread.stackSize / 1024);
    [thread start];
}

线程安全:(多线程操作共享资源)
线程不安全代码:

#import "ViewController.h"

@interface ViewController ()
//总票数
@property (nonatomic, assign) int ticketsCount;

@property (nonatomic, strong) NSObject *obj;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.ticketsCount = 10;
    
    self.obj = [NSObject new];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //模拟买票窗口1
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    [thread1 start];
    
    //模拟买票窗口2
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    [thread2 start];
}
//线程是不安全的
//模拟卖票的方法
- (void)sellTickets {
    
    while (YES) {
        //模拟耗时
        [NSThread sleepForTimeInterval:1.0];
            //判断还有没有票
         if (self.ticketsCount > 0) {
            self.ticketsCount = self.ticketsCount - 1;
             NSLog(@"剩余%d张票",self.ticketsCount);
          }else{
             NSLog(@"来晚了,票没了");
             break;
         }
    }
}
@end

线程安全代码:互斥锁

#import "ViewController.h"

@interface ViewController ()
//总票数
@property (nonatomic, assign) int ticketsCount;

@property (nonatomic, strong) NSObject *obj;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.ticketsCount = 10;
    
    self.obj = [NSObject new];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //模拟买票窗口1
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    [thread1 start];
    
    //模拟买票窗口2
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    [thread2 start];
}
//模拟卖票的方法
- (void)sellTickets {
    
    while (YES) {
        //模拟耗时
        [NSThread sleepForTimeInterval:1.0];
        //任意一个对象内部都有一把锁
        //加锁会影响程序的性能
        
        //互斥锁
        //线程同步
        @synchronized(self.obj) {
            //判断还有没有票
            if (self.ticketsCount > 0) {
                self.ticketsCount = self.ticketsCount - 1;
                NSLog(@"剩余%d张票",self.ticketsCount);
            }else{
                NSLog(@"来晚了,票没了");
                break;
            }
        }
        
    }
}
@end

互斥锁:
(1) 互斥锁的使用前提: 多条线程同时访问同一块资源。
(2) 互斥锁的格式:
@synchronized(锁对象) { // 需要锁定的代码 }
在Xcode7之前,敲互斥锁的代码没有智能提示。
(3) 互斥锁的优缺点
1> 优点: 能有效防止因多线程抢夺资源造成的数据安全问题
2> 缺点: 会消耗大量的CPU资源,影响程序的执行效率
(4) 相关专业术语: 线程同步
1> 线程同步的意思是: 当多条线程要访问同一块资源的时候,必须等待前一条线程访问完毕,后一条线程才可以访问。
2> 互斥锁,就是使用了线程同步技术。
(5) 使用注意
1> 互斥锁中的锁对象必须是一个全局的锁对象。
2> 互斥锁锁定的代码范围应该尽可能小,锁住读写部分的代码即可。
思考: 如果锁住整个while(YES)死循环,会产生什么后果?

互斥锁原理:
每一个对象内部都有一个锁(变量),当有线程要进入 @synchronized到代码块中会先检查对象的锁是打开还是关闭状态,默认是打开状态。如果是线程执行到代码块内部会先上锁。如果上了锁,再有线程要执行代码块就先等待,直到锁打开才可以进入。
1>线程执行到 @synchronized
2>检查锁状态:如果锁是打开的,就执行第3>步;如果锁是关闭的,就转到第6>步。
3>关闭锁
4>执行代码块
5>执行完毕,开锁
6>线程等待(就绪状态)
加锁后程序的执行效率比不加锁要低,因为线程要等待锁,但是锁保证了多个线程同时操作同一个资源的安全性。

原子属性:
(1)原子属性和非原子属性介绍
1> OC 在定义属性原子特性的时候,有 nonatomic 和 atomic两种选择
nonatomic:非原子属性,不会为 setter 方法加锁。
atomic: 原子属性,为 setter 方法加锁(默认就是atomic)。
通过给 setter 加锁,可以保证同一时间只有一个线程能够执行写入操作(setter),但是同一时间允许多个线程执行读取操作(getter)。atomic本身就有一把自旋锁。这个特点叫做”单写多读”: 单个线程写入,多个线程读取。
注意:atomic 只能保证写入数据的时候是安全的,但不能保证同时读写的时候是安全的。
2> nonatomic 和 atomic 的对比
atomic:线程安全(执行setter方法的时候),需要消耗大量的资源。
nonatomic:非线程安全,适合内存小的移动设备。

(2)使用建议
1> 所有属性都声明为 nonatomic
2> 尽量避免多线程抢夺同一块资源
3> 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减轻移动客户端的压力。

(3)自旋锁和互斥锁的区别
1> 互斥锁
如果发现其它线程正在执行锁定代码,线程会进入休眠(就绪状态),等其它线程时间片到了打开锁后,线程就会被唤醒(执行)。
2> 自旋锁
如果发现有其它线程正在执行锁定代码,线程会以死循环的方式,一直等待锁定的代码执行完成。自旋锁更适合执行不耗时的代码。

{3} GCD
GCD是苹果公司专门为多核的并行运算提出的解决方案,是纯 C 语言的,GCD 会自动利用更多的CPU 内核,且会自动管理线程的生命周期,程序猿只需要告诉 GCD要执行什么任务, 不需要编写任何的县城管理的代码, GCD 提供了非常多的功能强大的函数,所以 GCD 在开发中有广泛的应用.
GCD 的基本演练(使用 GCD 下载网络图片)

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)loadView
{
    self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];

    self.view = self.scrollView;
    
    self.imageView = [[UIImageView alloc] init];
   
    [self.scrollView addSubview:self.imageView];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    //开启异步执行,下载网络图片
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSURL *url = [NSURL URLWithString:@"http://img02.tooopen.com/images/20141231/sy_78327074576.jpg"];
        
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *img = [UIImage imageWithData:data];
        
        //回到主线程更新UI
        dispatch_sync(dispatch_get_main_queue(), ^{
            
            self.imageView.image = img;
            [self.imageView sizeToFit];
            
            self.scrollView.contentSize = img.size;
        });
    });
}

//GCD中最重要的使用方式就是
-(void)GCDDemo{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"我需要执行一段耗时的操作!!!");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"我需要回到主线程刷新UI");
        });
    });
}
@end

GCD 的队列
1.串行队列
串行队列的特点:
以先进先出的方式,按顺序调度队列中的任务去执行,一次只能调度一个任务。
无论队列中所指定的执行任务的函数是同步还是异步,都必须等待前一个任务执行完毕,才可以调度后面的任务。
串行队列的创建:
(1) dispatch_queue_t queue = dispatch_queue_create("wzlbaidu", DISPATCH_QUEUE_SERIAL);
(2) dispatch_queue_t queue = dispatch_queue_create("wzlbaidu", NULL);

1.1 串行队列,同步执行
特点: 不开线程,顺序执行

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo1];
}

#pragma mark - 串行队列, 同步执行
- (void)gcdDemo1 {
    
    // 1. 创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("wzlbaidu", DISPATCH_QUEUE_SERIAL);
    
    // 2. 将任务添加到队列, 并且指定同步执行
    for (int i = 0; i < 10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
}

1.2 串行队列,异步执行
特点:开一条线程,顺序执行

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo2];
}
#pragma mark - 串行队列, 异步执行
- (void)gcdDemo2 {
    // 1. 创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("wzlbaidu", NULL);
    
    // 2. 将任务添加到队列, 并且指定异步执行
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
}

2.并发队列
并发队列的特点:
以先进先出的方式,并发(同时)调度队列中的任务去执行。如果当前调度的任务是同步执行的,会等待当前任务执行完毕后,再调度后续的任务。如果当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,就不会等待当前任务,直接调度任务到新线程去执行。

2.1 并发队列,同步执行
特点:不开线程,顺序执行 (和串行同步一样,所以这种方式没人使用)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo1];
}

#pragma mark - 并发队列, 同步执行
- (void)gcdDemo1 {
    // 1. 创建并发队列
    dispatch_queue_t q = dispatch_queue_create("wzlbaidu", DISPATCH_QUEUE_CONCURRENT);
    
    // 2. 将任务添加到队列, 并且指定同步执行
    for (int i = 0; i < 10; i++) {
        dispatch_sync(q, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
}

2.2 并发队列,异步执行
特点:开多条线程,无序执行

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo2];
}

#pragma mark - 并发队列, 异步执行
- (void)gcdDemo2 {
    // 1. 创建并发队列
    dispatch_queue_t q = dispatch_queue_create("wzlbaidu", DISPATCH_QUEUE_CONCURRENT);
    
    // 2. 将任务添加到队列, 并且指定异步执行
    for (int i = 0; i < 10; i++) {
        dispatch_async(q, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
}

3.主队列
主队列的特点:
主队列是系统提供的,无需自己创建,可以直接通过dispatch_get_main_queue()函数来获取。
(1)添加到主队列的任务只能由主线程来执行。
(2)以先进先出的方式,只有当主线程的代码执行完毕后,主队列才会调度任务到主线程执行

3.1主队列,异步执行
特点:主队列,就算是异步执行,也不会开线程,顺序执行,先把主线程上的代码执行完毕,才会执行添加到主队列里面的任务。主队列就相当于一个全局串行队列。

主队列和串行队列的区别:
串行队列:必须等待一个任务执行完毕,才会调度下一个任务。
主队列:如果主线程上有代码执行,主队列就不调度任务

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo];
}

#pragma mark - 主队列, 异步执行
// 在主线程顺序执行, 不开线程
// 主队列的特点: 只有当 主线程空闲时, 主队列才会调度任务到主线程执行
- (void)gcdDemo {
    // 1. 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 2. 将任务添加到主队列, 并且指定异步执行
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
    
    // 先执行完这句代码, 才会执行主队列中的任务
    NSLog(@"hello %@", [NSThread currentThread]);
}

3.2主队列,同步执行
特点:在主线程执行,主队列同步执行任务,会发生死锁,(同步执行当前队列dispatch_get_current_queue()都会死锁)dispatch_get_current_queue()已经被苹果遗弃,不再使用

- (void)gcdDemo2 {
    
    NSLog(@"begin");
    
    // 1. 获取主队列
    dispatch_queue_t q = dispatch_get_main_queue();
    
    // 2. 将任务添加到主队列, 并且指定同步执行
    // 死锁
    for (int i = 0; i < 10; i++) {
        dispatch_sync(q, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }
    
    NSLog(@"end");
}

3.3解决死锁问题:
当我们将主队列同步执行任务放到子线程去执行,就不会出现死锁。

- (void)gcdDemo{
    
    NSLog(@"begin");
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"--- %@", [NSThread currentThread]);
        // 1. 获取主队列
        dispatch_queue_t q = dispatch_get_main_queue();
        
        // 2. 将任务添加到主队列, 并且指定同步执行
        // 死锁
        for (int i = 0; i < 10; i++) {
            dispatch_sync(q, ^{
                NSLog(@"%@ %d", [NSThread currentThread], i);
            });
        }
    });
    
    NSLog(@"end");
}

4.全局队列
特点:全局队列的工作特性跟并发队列一致。 实际上,全局队列就是系统为了方便程序员,专门提供的一种特殊的并发队列。

全局队列和并发队列的区别
全局队列
1>没有名称
2>无论ARC还是MRC都不需要考虑内存释放
3>日常开发,建议使用全局队列
并发队列
1>有名称
2>如果在MRC开发中,需要使用dispatch_release来释放相应的对象
3>dispatch_barrier 必须使用自定义的并发队列
4>开发第三方框架,建议使用并发队列

//通过dispatch_get_global_queue(long identifier, unsigned long flags)函数来获取全局队列(参数都传0就行)。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self gcdDemo];
}
- (void)gcdDemo {
    // 1. 获取全局队列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    // 2. 将任务添加到全局队列, 异步执行
    for (int i = 0; i < 10; i++) {
        dispatch_async(q, ^{
            NSLog(@"%d %@", i, [NSThread currentThread]);
        });
    }
}

GCD的同步任务
同步任务的作用:网络开发中,通常有些任务执行会彼此依赖,这个时候就需要同步任务。
案例: 去appStore 下载软件
步骤: 1) 输入密码登录 2)付费 3)下载软件
特点: 这3步操作必须按顺序进行,不能颠倒顺序。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self demo];
}

- (void)demo {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 第一步: 输入密码登录
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"输入密码 %@", [NSThread currentThread]);
        });
        // 第二步: 付费
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"付费 %@", [NSThread currentThread]);
        });
        // 第三步: 下载应用
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"下载应用 %@", [NSThread currentThread]);
        });
    });
}

dispatch_barrier(阻塞)
1 barrier的作用
1.1主要用于在多个异步操作完成之后,统一对非线程安全的对象进行更新。
1.2 适合于大规模的I/O操作。
1.3 当访问数据库或文件的时候,更新数据的时候不能和其它更新或读取的操作在同一时间执行,可以使用 dispatch_barrier_async解决。
2 barrier的演练
模拟从网络上下载很多张图片,并且把下载完成的图片添加到mutableArray中(所有图片下载完一起加到数组中)

//第一步: 定义一个用于保存图片的可变数组
@interface ViewController () {
    // 定义一个并发队列
    dispatch_queue_t _queue;
}

/// 图片数组
@property (nonatomic, strong) NSMutableArray *photoList;
@end
//第二步: 懒加载
/// 懒加载
- (NSMutableArray *)photoList {
    if (_photoList == nil) {
        _photoList = [NSMutableArray array];
    }
    return _photoList;
}
//第三步: 拖入素材,模拟从网络下载图片
- (void)viewDidLoad {
    [super viewDidLoad];
     _queue = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 1; i <= 1000; i++) {
        [self downloadImage:i];
    }
}
// 模拟从网络上下载很多张图片, 并且将下载完成的图片添加到 NSMutableArray 中
- (void)downloadImage: (int)index {
    // 异步下载
    dispatch_async(_queue, ^{
        // 模拟从网络下载图片
        NSString *fileName = [NSString stringWithFormat:@"%02d.jpg", index % 10 + 1];
        NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
        
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        // 将图像添加到数组
        dispatch_barrier_async(_queue, ^{
            NSLog(@"添加图片 %@ %@", fileName, [NSThread currentThread]);
            // 将图像添加到数组
            [self.photoList addObject:image];
        });

        NSLog(@"下载完成 %@ %@", fileName, [NSThread currentThread]);
    });
}
//第四步: 打印photoList中的图片数量
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"图片数量: %tu", self.photoList.count);
}

dispatch_after 延迟执行
代码:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self delay];
}

#pragma mark - 延迟执行
/**
从现在开始,经过多少纳秒,由队列调度执行block中的代码
 参数:
 1. dispatch_time_t:   when    从现在开始, 经过多少纳秒(即延迟多少时间)
 2. dispatch_queue_t: queue   队列
 3. dispatch_block_t: block   任务
 */
- (void)delay {
    // 1. when(时间): 从现在开始, 经过多少纳秒
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
    // 2. 队列
    dispatch_queue_t q = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);
    
    // 3.任务
    void(^task)() = ^ {
        NSLog(@"%@", [NSThread currentThread]);
    };
    
    // 经过多少纳秒, 由队列调度任务异步执行
    dispatch_after(when, q, task);
}

精简代码

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
});

dispatch_once 一次性执行(典型应用:单例)
原理:一次都没有执行过的时候onceToken的值是0,执行过一次后onceToken的值变成-1

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self once];
}

#pragma mark - 一次性执行
///  一次性执行的原理: 判断静态全局变量的值, 默认是0, 如果执行完成后, 就设置为-1
///  once 内部会判断 onceToken 的值, 如果是0才执行
- (void)once {
    static dispatch_once_t onceToken;
    // 如果 onceToken == 0 就执行 block 中的代码
    NSLog(@"%zd", onceToken);
    
    for (int i = 0; i < 10; i++) {
        dispatch_once(&onceToken, ^{
            NSLog(@"是一次吗?");
        });
    }
    
    NSLog(@"%zd", onceToken);
}

dispatch_group_t 调度组
1 调度组基本使用
模拟下载3首歌曲A B C,下载完成后,在主线程通知用户。
步骤:
第一步: 获取全局队列
第二步: 创建调度组
第三步: 添加异步任务
第四步: 调度组监听所有异步任务都执行完毕之后,在主线程通知用户

///  演示调度组的基本使用
- (void)group1 {
    // 1. 获取全局队列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    
    // 2. 创建调度组
    dispatch_group_t group = dispatch_group_create();
    
    // 3. 将任务添加到队列和调度组
    dispatch_group_async(group, q, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"download A %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, q, ^{
        NSLog(@"download B %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, q, ^{
        NSLog(@"download C %@", [NSThread currentThread]);
    });
    
    // 4.调度组通知 - 监听群组中的所有异步操作执行完毕之后, 统一获得通知
    // 异步监听
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dowload finished %@", [NSThread currentThread]);
    });
    
    // 测试通知是同步还是异步?
    // come here 先出来, 说明 通知是异步的
    NSLog(@"come here");
}

2、调度组的原理
在终端中敲出: man dispatch_group_async 可以找出关于调度组的信息

// 调度组的内部原理
- (void)group2 {
    // 1. 获取全局队列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    
    // 2. 创建调度组
    dispatch_group_t group = dispatch_group_create();
    
    // 进入群组, 给 group 一个标记, 后续紧接着的队列中的 block 归 group 监听
    // dispatch_group_enter 和 dispatch_group_leave 必须成对出现
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        NSLog(@"download A %@", [NSThread currentThread]);
        // 离开群组, 写在 block 的最后一句
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"download B %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"download C %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    // 调度组监听 - dispatch_group_notify 是异步的
    //    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //        NSLog(@"dowload finished %@", [NSThread currentThread]);
    //    });
    
    // 等待群组空, 一直到永远
    // 同步 - 群组不空, 这句话就死等!
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"come here");
}
注意: 
dispatch_group_enter 和 dispatch_group_leave 必须成对出现。
以后使用,还是使用 group1的写法。

{4}NSOperation
NSOperation是对 GCD 基于面向对象的封装,使用起来比 GCD 更加的简单,而且提供了一些 GCD 比较难实现的功能(例如:添加依赖,设置最大并发数等),NSOperation也不用关心线程的生命周期.
NSOperation 与 GCD 的区别

NSOperation 与 GCD 的区别.png
 NSOperation 是一个抽象类(方法没有实现,但子类必须实现这些方法),类似于UICollectionViewLayout,不能直接使用.但可以使用其子类,系统提供的子类有两个:NSInvocationOperation 以及 NSBlockOperation, 另外我们也可以自定义一个 Operation 继承自 NSOperation.
NSOperation 和 NSOperationQueue 的使用步骤:

(1).先将需要执行的操作封装到一个 NSOperation 对象中
(2).然后将 NSOperation 对象添加到一个 NSOperationQueue 中.
系统会自动将NSOperationQueue中的 NSOperation 取出来并将其放到一条新线程中去执行.
NSOperation 头文件中的一些方法和属性

//所有的并发Operation必需重写这个方法并且要实现这个方法的内容来代替原来的操和。
//手动执行一个操作,你可以调用start方法。因此,这个方法的实现是这个操作的始点,也是其他线程或者运行这你这个任务的起点。
//注意一下,在这里永远不要调用[super start]。
- (void)start;

//这个方法就是你的实现的操作
- (void)main;

//是否取消
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;

//下面两个方法必须实现(如果是并发队列),并发队列负责维持当前操作的环境和告诉外部调用者当前的运行状态。
@property (readonly, getter=isExecuting) BOOL executing;//是否退出
@property (readonly, getter=isFinished) BOOL finished;//是否已经完成

//定义一个并发操作,必须重写这个方法并且返回YES
@property (readonly, getter=isConcurrent) BOOL concurrent; 

@property (readonly, getter=isAsynchronous) BOOL asynchronous 
@property (readonly, getter=isReady) BOOL ready;

- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

@property (readonly, copy) NSArray<NSOperation *> *dependencies;

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

@property NSOperationQueuePriority queuePriority;

@property (nullable, copy) void (^completionBlock)(void) 

- (void)waitUntilFinished 
//线程的优先级
@property double threadPriority 
//队列的服务质量
@property NSQualityOfService qualityOfService 
//操作的名称
@property (nullable, copy) NSString *name 

@end


1/ NSOperation 的子类
1.1 NSInvocationOperation 的使用
NSInvocationOperation需要声明一个方法将,要执行的代码封装到这个方法中,使用起来比较麻烦,所以很少用
方法一:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //创建操作
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
    NSLog(@"%d",op.isFinished);
    //start方法更新操作的状态   调用main方法
    //不会开启新线程
    [op start]; //不开启新线程
    [op start];
    [op start];//start方法虽然调用三次,但只执行一次
    NSLog(@"%d",op.isFinished);
}

- (void)demo {
    NSLog(@"hello %@",[NSThread currentThread]);
}

方法二:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    创建操作
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
    //队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //把操作添加到队列
    [queue addOperation:op];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"hello %@",[NSThread currentThread]);
        
    });

}

- (void)demo {
    NSLog(@"hello %@",[NSThread currentThread]);
}

1.2 NSBlockOperation

#import "ViewController.h"
@interface ViewController ()
//全局队列
@property (nonatomic, strong) NSOperationQueue *queue;
@end

@implementation ViewController
//懒加载
- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self demo5];
}

//演示start
- (void)demo1 {
    //创建操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"hello %@",[NSThread currentThread]);
    }];
    //更新op的状态,执行main方法
    [op start];  //不会开新线程
}

//把操作添加到队列
- (void)demo2 {
    //创建队列
//    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //创建操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"hello %@",[NSThread currentThread]);
    }];
    //把操作添加到队列中, 会开新线程
    [[NSOperationQueue new] addOperation:op];
}
//简写
- (void)demo3 {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperationWithBlock:^{
        NSLog(@"hello %@",[NSThread currentThread]);
    }];
}

//全局队列
- (void)demo4 {
    //并发队列,异步执行
    [self.queue addOperationWithBlock:^{
        NSLog(@"hello  %@",[NSThread currentThread]);
      }];
}

// 操作的 completionBlock
- (void)demo5 {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"hello %@",[NSThread currentThread]);
    }];
    //操作完成之后执行
    [op setCompletionBlock:^{
        NSLog(@"end %@",[NSThread currentThread]);
    }];
    
    [self.queue addOperation:op];
}

@end

2/ 具体操作
2.1.线程间通信

#import "ViewController.h"

@interface ViewController ()
//全局队列, 通常情况下要设置一个全局的队列
@property (nonatomic, strong) NSOperationQueue *queue;

@end

@implementation ViewController

//懒加载
- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.queue addOperationWithBlock:^{
      //异步下载图片
        NSLog(@"异步下载图片");
        
        //获取当前队列
//        [NSOperationQueue currentQueue]
        
        //线程间通信,回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"更新UI");
        }];
    }];
}
@end

2.2最大并发数
概念:同时执行的任务数,例如,同时开3个线程执行3个任务,并发数就是3
代码

#import "ViewController.h"

@interface ViewController ()
//全局队列
@property (nonatomic, strong) NSOperationQueue *queue;
@end

@implementation ViewController
//懒加载
- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
        //设置最大并发数(并不是线程数),NSOperation 与 GCD 不同,如果不设置最大并发数,他会不断地开新线程(线程太多,性能反而下降),
        _queue.maxConcurrentOperationCount = 2;
    }
    return _queue;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    for (int i = 0; i < 1000; i++) {
        [self.queue addOperationWithBlock:^{
            [NSThread sleepForTimeInterval:2.0];
            NSLog(@"%d  %@",i,[NSThread currentThread]);
        }];
    }
    
}

2.3 取消/暂停/继续

#import "ViewController.h"
@interface ViewController ()
//全局队列
@property (nonatomic, strong) NSOperationQueue *queue;
@end

@implementation ViewController
//懒加载
- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
        //设置最大并发数
        _queue.maxConcurrentOperationCount = 2;
    }
    return _queue;
}

//取消所有操作  当前正在执行的操作会执行完毕,取消后续的所有操作
- (IBAction)cancel:(id)sender {
    [self.queue cancelAllOperations];
    NSLog(@"取消");
}

//暂停 操作   当前正在执行的操作,会执行完毕,后续的操作会暂停
- (IBAction)suspend:(id)sender {
    self.queue.suspended = YES;
    NSLog(@"暂停");
}

//继续 操作
- (IBAction)resume:(id)sender {
    self.queue.suspended = NO;
    NSLog(@"继续");
}

//当操作执行完毕,会从队列中移除
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //队列中的操作数
    NSLog(@"%zd",self.queue.operationCount);
}
@end

2.4 操作的优先级和完成回调

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSOperationQueue *queue;
@end

@implementation ViewController
//懒加载
- (NSOperationQueue *)queue{
    if (_queue == nil) {
        _queue = [NSOperationQueue new];
    }
    return _queue;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    //操作1
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 20; i++) {
            NSLog(@"op1  %d",i);
        }
    }];
    //设置优先级最高
    op1.qualityOfService = NSQualityOfServiceUserInteractive;
    [self.queue addOperation:op1];
    
    //等操作完成,执行  执行在子线程上
    [op1 setCompletionBlock:^{
        NSLog(@"============op1 执行完成========== %@",[NSThread currentThread]);
    }];
    
    //操作2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 20; i++) {
            NSLog(@"op2  %d",i);
        }
    }];
    //设置优先级最低
    op2.qualityOfService = NSQualityOfServiceBackground;
    [self.queue addOperation:op2];
}
@end
/*
结论: 设置优先级,不能保证,优先级高的执行完再去执行优先级低的
*/

2.5 操作依赖
模拟软件升级的逻辑:先下载,下载完成后解压,解压完成之后(主线程)提示升级完成

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSOperationQueue *queue;
@end

@implementation ViewController
- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [NSOperationQueue new];
    }
    return _queue;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 下载 - 解压 - 升级完成
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载");
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"解压");
    }];
    
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"升级完成");
    }];
    //设置操作间的依赖
    [op2 addDependency:op1];
    [op3 addDependency:op2];
    
    //错误,会发生循环依赖,什么都不执行
//    [op1 addDependency:op3];
   
    //操作添加到队列中
    [self.queue addOperations:@[op1,op2] waitUntilFinished:NO];
    //依赖关系可以跨队列执行
    [[NSOperationQueue mainQueue] addOperation:op3];
}
@end

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

推荐阅读更多精彩内容