iOS 多线程的一些知识

多线程的基本概念

  • 进程:可以理解成一个运行中的应用程序,是系统进行资源分配和调用的基本单位,是操作系统结构的基础,主要管理资源。
  • 【线程】:进程的基本执行单元,一个进程拥有至少一个线程。
  • 【主线程】:处理UI,所有更新UI的操作都必须在主线程上执行。
  • 【多线程】:在同一时刻,一个CPU只能处理1条线程,但CPU可以在多条线程之间快速的切换,只要切换的速度足够快,就造成了多线程一同执行的假象。

线程就像火车的一节车厢,进程则是火车。车厢(线程)离开火车(进程)是无法跑动的,而火车(进程)至少有一节车厢(主线程)。多线程可以看作多个车厢,他的出现是为了提高效率,另外我们可以利用多线程,将耗时的操作放到后台执行,以防止耗时操作阻塞主线程,导致UI卡顿等现象。

线程的状态与生命周期

下图是线程状态示意图,从图中可以看出线程的生命周期:新建->就绪->运行->阻塞->死亡

线程状态示意图

下面分别阐述线程的生命周期中的每一步:

  • 【新建】:实例化线程对象。
  • 【就绪】:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
  • 【运行】:CPU负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
  • 【阻塞】:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期)@synchronized(self),:(互斥锁)。
  • 【死亡】:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部终止执行/在主线程终止线程对象。

补充:

[NSThread exit]:一旦强行终止程序,后续的所有代码都不会被执行。
[thread cancel]:取消线程,并不会直接取消线程,只是给线程对象添加 isCancellled 标记。

多线程的四种解决方案

多线程的四种解决方案:pthread、NSThread、GCD、NSOperation

四种解决方案

NSThread

线程实例

创建一个线程

系统提供了三种创建线程的方法

- (instancetype)init;
// 通过指定对象和方法选择器的方式,argument是传递的参数
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
// 注意,这种方式是iOS10之后才增加的,将任务放在了block种执行
- (instancetype)initWithBlock:(void (^)(void))block;

可设置的一些信息

@property (nullable, copy) NSString *name;  // 线程名称
@property NSUInteger stackSize; // 栈的大小,4k的倍数,在线程start之前设置
@property double threadPriority; // 线程的优先级,【0-1】,值越大,优先级别越高,该属性不久的将来会废弃,可使用下面的属性
@property NSQualityOfService qualityOfService;  // 线程优先级,iOS8.0之后新增,有一下几种

NSQualityOfService优先级分类

NSQualityOfServiceUserInteractive // 与用户交互的任务,这些任务通常跟UI级别的刷新相关,比如动画,这些任务需要在一瞬间完成
NSQualityOfServiceUserInitiated // 由用户发起的并且需要立即得到结果的任务,比如滑动scroll view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成
NSQualityOfServiceUtility // 一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceBackground // 这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时
NSQualityOfServiceDefault // 优先级介于user-initiated 和 utility,当没有 QoS信息时默认使用,开发者不应该使用这个值来设置自己的任务

线程管理

- (void)start; // 启动线程
- (void)cancel; // 取消线程

线程的状态

@property (readonly, getter=isExecuting) BOOL executing; // 线程是否正在执行
@property (readonly, getter=isFinished) BOOL finished; // 线程是否完成
@property (readonly, getter=isCancelled) BOOL cancelled; // 线程是否已经取消

实例

  • 手动创建线程
// 创建线程
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"执行线程任务。。。");
}];
// 设置一些属性
thread.name = @"线程1";
// 开启线程
[thread start];
NSLog(@"线程信息:%@",thread);
// 取消线程
[thread cancel];
NSLog(@"线程是否被取消:%i",thread.isCancelled);
  • 传递一些参数

如果需要在线程任务中传递一些参数,我们需要使用另外一种创建方式。

// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@{@"name":@"LOITA0164",@"age":@"25"}];
// 开启线程
[thread start];

-(void)run:(id)object{
    NSLog(@"传递过来的参数:%@",object);
}

小结

当我们创建NSThread实例后,我们为其设置一些属性,例如线程名称、线程优先级、栈大小等信息,与其相对的,我们需要手动管理线程的开始和取消等操作。

创建线程

NSThread类不仅为我们提供了创建实例的方法,另外为我们提供了快捷使用线程的类方法,当你使用这类方法时,你的关注点不再是线程的创建和开启,而仅仅是想让线程去执行某些任务,另外,类的方法会自动开始执行线程任务。

注:使用类方法时,针对的都是当前的线程

同样的,有两种快捷使用方式,一种是通过block方式执行线程任务,另一种是通过对象和方法选择器实现线程任务。

// block方式执行任务
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// 方法选择器执行任务
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

示例

[NSThread detachNewThreadWithBlock:^{
    NSLog(@"执行新线程任务");
}];

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@[@"apple",@"pear"]];

-(void)run:(id)object{
    NSLog(@"传递过来的参数:%@",object);
}

一些类方法

NSThread提供了一些类方法,管理线程的调度,获取当前的线程,线程的信息等

@property (class, readonly, strong) NSThread *mainThread;//获取到主线程
@property (class, readonly, strong) NSThread *currentThread;//获取当前线程
@property (class, readonly) BOOL isMainThread;//当前是否是主线程
+ (BOOL)isMultiThreaded;//是否是多线程
+ (void)sleepUntilDate:(NSDate *)date;//当前线程休眠到某时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;//当前线程休眠时长
+ (void)exit;//退出当前线程
+ (double)threadPriority;//当前线程的优先级
+ (BOOL)setThreadPriority:(double)p;//设置当前线程的优先级

示例

我们修改一下任务run的实现

-(void)run:(id)object{
    // 获取主线程
    NSThread *mainThread = [NSThread mainThread];
    mainThread.name = @"主线程";
    NSLog(@"主线程:%@",mainThread);
    // 获取当前的线程,可以通过这个线程实例使用线程方法
    NSThread *currentThread = [NSThread currentThread];
    currentThread.name = @"子线程";
    NSLog(@"当前线程:%@",currentThread);
    // 线程的优先级
    [NSThread setThreadPriority:0.8];
    NSLog(@"%f-%f",[NSThread threadPriority],currentThread.threadPriority);
    // 线程信息
    NSLog(@"多线程?:%i",[NSThread isMultiThreaded]);
    // 是否是主线程
    NSLog(@"当前线程是否是主线程:%i-%i",[NSThread isMainThread],[currentThread isMainThread]);
    // 线程休眠(阻塞)
    // 休眠时长
    [NSThread sleepForTimeInterval:1.0];
    // 休眠到某个时间点
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
    // 终止线程,一旦执行exit,线程之后的任务不再被执行
    [NSThread exit];
}

小结

在某一线程任务中(包括主线程)使用NSThread的类方法时,都是针对某一线程的,如设置优先级、阻塞、终止等

线程通信

截止到现在,我们已经知晓了如何创建线程,管理线程,传递参数等等,但是读者仔细观察一下NSThread的方法,其中并没有为线程赋予任务的方法,这意味着我们通过[[NSThread alloc] init][NSThread new]的线程无法指定其任务,那么如果遇到一个需求:耗时的操作开辟子线程去完成,完成后进行刷新UI,这样的创建线程能否完成呢?我们知道UI需要在主线程中去完成的,那么为了完成上述需求,我们势必要在子线程中取到主线程,让其完成UI的更新,但是,正如之前提到的,NSThread并没有为线程赋予任务的方法,并且当前线程是只读属性,无法更换为主线程,因此,这种需求显然是无法实现的。(笔者试过取消当前线程,调用更新UI;还是创建block作为参数,回调结果更新UI,最终发现当前线程都是子线程,这说明消息传递默认都是在当前线程上完成的)

那么,NSThread无法完成了上述的需求了吗?答案是否定的,在NSThread的最后,apple扩展了NSObject对象,这些方法,是从实例对象角度,选择线程执行任务的,之前的NSThread都是从线程角度完成任务。

// 在主线程完成任务
// waitUntilDone:表示是否阻塞当前线程等待新任务结束(结束后会继续执行后面任务)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
// 相当于第一种方法使用kCFRunLoopCommonModes模式
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 选择某一线程完成任务
// waitUntilDone:表示是否阻塞当前线程等待新任务结束(结束后会继续执行后面任务)
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);//相当于第一种方法使用kCFRunLoopCommonModes模式
// 在后台完成任务
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

示例

我们来完成上述需求

@interface ViewController (){
    NSInteger _count; // 定义一个计数器
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 开启一个线程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

-(void)run{
    NSLog(@"%s,当前线程:%@",__func__,[NSThread currentThread]);
    _count = 1;
    NSLog(@"UI更新前的count:%ld",_count);
    // 在主线程上更新UI,这里只做计算器加1
    // 注:这里的waitUntilDone是YES,表示等待新任务结束后再继续执行
    [self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:YES];
    NSLog(@"UI更新后的count:%ld",_count);
}
-(void)updateUI:(NSNumber*)count{
    NSLog(@"%s,当前线程:%@",__func__,[NSThread currentThread]);
    _count++;
}

结果

结果

我们发现,updateUI中的线程是主线程,而不是子线程,另外我们发现,count变成了2,说明run中的任务是顺序执行的。

为了对比waitUntilDone的效果,我们将其改为NO


结果

我们发现,count依旧输出了两次,但是数量都是1,这说明run中的任务并没有顺序执行,因此waitUntilDone为YES还是NO需要根据需求而定。

另外需要注意的是,此时updateUI的主线程任务是加到主线程任务最后才被执行的,先来先服务的原则。

为了对比NSThreadPerformAdditions扩展调用方法和普通的访问的不同,我们将

[self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:NO];

改为

[self updateUI:@(_count)];

结果:

结果

我们发现结果似乎和第一次一样,但是,需要注意的是,<NSThread: 0x1700798c0>{number = 3, name = (null)},这里,这说明线程依旧是原来的子线程(number为1时,才是主线程,因为number是递增的,主线程最先被创建出来,其值为1)

那么,这种调用方式似乎等同于下面的方法

[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

结果:

结果

结果一致,[object method]这种方式都是在当前线程上执行的。

类比拓展:延迟执行

[self performSelector:@selector(updateUI:) withObject:@(_count) afterDelay:1.0];

等同于

[NSThread sleepForTimeInterval:1.0];
[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

总结

1、我们可以创建子线程完成一些其他任务
2、NSThread提供了一些类方法,让我们可以便捷的创建使用子线程
3、NSThread的对象都是在创建时指定了线程任务,并未提供给线程赋予执行新任务的方式(因此即使你在子线程中拿到了主线程对象,你也无法执行主线程完成一些任务)
4、NSObject扩展了一些方法,用来指定在某些线程上完成任务
5、子线程执行耗时操作,主线程更新UI的任务需求
6、[object method]这种调用方式是以当前线程来执行的


NSOperation和NSOperationQueue简介

NSOperation:

即操作对象,是一个抽象类,用于封装和单个任务关联的代码和数据。因为它是抽象的,所以不直接使用这个类,而是使用子类(NSInvocationOperation或NSBlockOperation)或使用自定义的子类之一来执行实际任务。
尽管NSOperation是抽象类,但其基本实现重要的逻辑来协调我们任务并安全执行,这种内置的逻辑使得我们可以专注于我们任务的实现,而不是和其他事务逻辑耦合。
NSOperation是一次性对象,即它只会执行一次任务,不能再使用它来执行其他任务。我们通常将操作对象添加到操作队列(NSOperationQueue类的实例)来同步或异步的执行操作,除了在操作队列上使用,我们还可以开子线程运行他们或使用结合GCD来执行操作,此时这些操作任务会在当前线程上执行。
如果将NSOperation对象添加到NSOperationQueue队列上,操作对象们会被操作队列管理开启操作,如果单独使用操作队列,则需要直接调用其start方法来开启任务。手动执行操作会给代码带来更多的负担,因为启动未处于isReady状态的操作会触发异常。

NSOperationQueue:

NSOperationQueue类是管理调用一组NSOperation对象的类。当操作对象加入到队列,该操作直到被明确cancelledfinished 前都会被保留在队列中。操作队列中的操作本身根据优先级和其他操作项的对象依赖性进行组织,并相应的执行。应用程序可能会创建多个操作队列并将操作提交给它们中的任何一个。
即使这些操作对象位于不同的操作队列中,操作间的依赖关系也是有效的。操作对象在其所有依赖操作完成执行之前不会进入isReady就绪状态。对于isReady状态的操作对象,最高优先级的操作的对象将会被优先执行。
对于已加入到操作队列里的操作对象,我们无法将其移除,操作队列会保持在队列中,直到其完成其任务,当然,这里的完成是一种状态,并不一定意味着真正的完成了其任务,因为操作对象在未被执行前,是可以取消的。取消操作对象依旧被保留在队列中,但是会通知对象尽快终止其任务。对于当前正在执行的操作,这意味着操作对象的工作代码必须检查取消状态,停止正在执行的操作,并将自己标记为finished已完成状态。对于'isReady'状态但是尚未执行的操作,队列仍必须调用该操作对象的start方法,以便它可以处理取消事件并将自己标记为finished已完成状态。
取消操作会导致操作忽略一切和它具有依赖关系。这意味着和其有依赖关系的操作可能更早的被执行。
和NSOperation一样,操作队列通常会在当前线程。另外,操作队列会使用GCD来启动其操作对象的执行,因此,操作对象总是在单独的线程上执行,而不管它们是否被指定为异步或同步操作。

上面是官方给出的解释,总体而言,NSOperation是操作对象类,NSOperationQueue是操作对象的调度者。

NSOperation类介绍

NSOperation类是一个抽象类,该类并没赋予其完成任务的能力,我们可以使用它的两个子类NSBlockOperationNSInvocationOperation,前者是通过block的形式完成任务,后者是则使用方法选择器实现。另外,我们还可以自定义自己的操作类。

  • NSBlockOperation

NSBlockOperation用于管理一个或多个block任务的并发执行,我们可以使用该对象实现一次执行多个block任务,而无需为每个block任务创建单独的操作对象。当执行多个block任务时,只有当所有的block完成时才认为操作本身已完成。

注:添加到block的任务是以默认优先级调度到队列的,并且block任务内部不可以在去更改执行环境。

-(void)useBlockOperation{
    // 创建操作对象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    // 单独使用操作对象时需要手动开启任务
    // 启动
    [bop start];
}

当我们在主线程中测试时:

 [self useBlockOperation];
主线程

当我们在某线程中测试时:

[NSThread detachNewThreadSelector:@selector(useBlockOperation) toTarget:self withObject:nil];
子线程

上面两个测试可知:操作对象会在给定的线程中执行任务。

NSBlockOperation对象可以添加更多的block任务,并在主线程中调用

-(void)useBlockOperation{
    // 创建操作对象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"origin:%@",[NSThread currentThread]);
    }];
    for (int i=0; i<10; i++) {
        // 添加更多的额外任务
        [bop addExecutionBlock:^{
            NSLog(@"%@:%@",[NSString stringWithFormat:@"%i",i],[NSThread currentThread]);
        }];
        NSLog(@"操作任务是否完成:%@",bop.isFinished?@"YES":@"NO");
    }
    // 启动
    [bop start];
    NSLog(@"操作任务是否完成:%@",bop.isFinished?@"YES":@"NO");
}
新增任务

我们发现,block中的操作任务是并发执行的,系统会在开启一个或多个线程执行这些任务,可以看到任务是在1和3号线程执行的,其中1号线程是主线程,因为我们是在主线程中调用操作对象的。如果我们在子线程中调用,则不会出现主线程,而会继续创建子线程去执行任务。另外,我们发现,只有当所有的block完成时,操作本身才会被标记为已完成状态。

  • NSInvocationOperation

NSInvocationOperation对象用于指定调用者的任务。我们可以通过某个对象开启一个操作,并指定对象调用选择器执行任务。

[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

// invocation操作对象
-(void)useInvocationOperation{
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation:) object:@{@"name":@"NSInvocationOperation"}];
    [op start];
}
// 任务
-(void)invocationOperation:(NSDictionary*)dic{
    NSLog(@"%@\n%@",[NSThread currentThread],dic);
}
子线程

我们看到NSInvocationOperation在新开启的线程被执行。

  • 自定义操作类

NSOperationQueue 操作队列

对于NSOperation类,我们发现,NSOperation的对象执行任务时,除了BlockOperation中的addExecutionBlock开启了新的线程,似乎和我们普通对象调用方法没什么不同,都是在当前线程执行任务,想要实现多线程编程,都需要借助GCD或NSTread来开启子线程,那这个操作类是否显得非常鸡肋呢?当然不是,其实NSOperation是需要结合NSOperationQueue才能发挥其作用的,NSOperationQueue是基于GCD的封装对象,用来便捷的完成NSOperation对象执行操作事务的调度工作。

NSOperationQueue 可以调度 NSOperation 对象,队列中的操作对象都是并发执行。

-(void)useOperationQueue{
    NSLog(@"queue:%@",[NSThread currentThread]);
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for (int i=0; i<3; i++) {
        NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"%@:%@",[NSString stringWithFormat:@"%i",i],[NSThread currentThread]);
        }];
        // 添加到操作队列
        [queue addOperation:bop];
    }
    // 或者快捷添加操作对象
    [queue addOperationWithBlock:^{
        NSLog(@"Add directly:%@",[NSThread currentThread]);
    }];
}
queue队列

我们发现,队列虽然是在主线程中执行,但是操作对象都是运行在子线程中,并且所有的操作对象都不需要手动开启,都是由队列自动调度开启。

关于操作对象的执行顺序会根据当前处于isReady状态的操作对象的优先级调用,另外,设置依赖关系的操作对象会在依赖对象完成后进入isReady状态,换句话说,被依赖对象优先于依赖对象执行。

首先我们先创建两个操作对象

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block:%@",[NSThread currentThread]);
}];
[bop addExecutionBlock:^{
    NSLog(@"blockNew:%@",[NSThread currentThread]);
}];
[bop addExecutionBlock:^{
    NSLog(@"blockNew2:%@",[NSThread currentThread]);
}];
NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation:) object:nil];
[queue addOperations:@[iop,bop] waitUntilFinished:NO];
没有依赖关系的

我们发现这两个操作对象的任务都是并发的。

  • 依赖

我们为两个操作项添加依赖。

// 添加依赖关系
[iop addDependency:bop];
添加依赖

我们发现,由于iop依赖bop,所以iop始终会等到bop完成后才被执行。

添加依赖时,不要出现循环依赖的情况,会导致死锁,死锁的两个操作对象都等待对方完成,结果都不能执行。

  • 死锁

另外,如果A依赖B,B手动先于A执行,系统则会抛出异常。

[iop addDependency:bop];
[iop start];
异常操作
  • 等待所有操作完成

如果我们需要在所有操作任务完成之后执行一些事务,则可以设置等待。

[queue addOperations:@[iop,bop] waitUntilFinished:YES];

或者

[queue waitUntilAllOperationsAreFinished];

该方法会阻塞当前线程,等待所有操作对象完成任务。在此期间,当前线程不能再往队列中添加操作对象,当然,其他线程可以添加操作对象,一旦所有挂起的操作对象完成,此方法返回,如果队列中没有操作对象,则此方法立刻返回。

  • 操作队列数量
@property NSInteger maxConcurrentOperationCount;

默认为-1,不限制,当设置为1时,相当于一个同步执行的操作队列。

其他的属性/方法

  • NSOperation

可监听的一些属性:

// 操作已取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 操作正在执行
@property (readonly, getter=isExecuting) BOOL executing;
// 操作已完成
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
// 任务是并发还是同步执行,当操作任务加入到操作队列后,会忽略该属性
@property (readonly, getter=isAsynchronous) BOOL asynchronous ;
// 任务是否已经就绪,当其依赖的操作任务都执行完时,改状态才会是YES
@property (readonly, getter=isReady) BOOL ready;

添加/移除依赖

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

开启/取消操作任务

- (void)start;
- (void)cancel;

优先级

@property NSOperationQueuePriority queuePriority;
// 取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

阻塞当前线程直到操作完成

- (void)waitUntilFinished;

操作完成回调

@property (nullable, copy) void (^completionBlock)(void);
  • NSOperationQueue

当前操作数量

@property (readonly) NSUInteger operationCount;

暂停/恢复队列

@property (getter=isSuspended) BOOL suspended;

取消队列中所有操作任务

- (void)cancelAllOperations;

当前队列/主队列

@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
@property (class, readonly, strong) NSOperationQueue *mainQueue;

我们可以获取到主队列,再调用

- (void)addOperationWithBlock:(void (^)(void))block;

例如在主队列中完成UI的更新。

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    self.contentImageView.image = image;
}];

GCD简介

GCD 是 Apple 开发的一个多核编程的解决方法,简单易用,效率高,速度快。通过 GCD,开发者只需要向队列中添加一段代码块,而不需要直接和线程打交道。GCD在后端管理着一个线程池,它不仅决定着你的代码块将在哪个线程被执行,还根据可用的系统资源对这些线程进行管理。这样通过GCD来管理线程,从而解决线程被创建的问题。

GCD基本概念

任务和队列

名词 说明
线程 程序执行任务的最小调度单元
任务 一段代码:GCD中,block中需要执行的代码
队列 存放任务的数组,FIFO(先进先出)的原则

异步、同步,并行和串行

名词 说明
异步(async) 具备开新线程的能力
同步(sync) 不具备开新线程的能力
并行 任务可以并发执行
串行 任务按顺序执行

说明:使用GCD开启多线程执行多个任务时,需具备两个条件:

1、能开启新线程
2、任务可以并发执行

即:"异步"+"并行"

GCD几种组合

- 并行队列 串行队列 主队列
异步 开启新线程,任务同时执行(1) 开启新线程,任务顺序执行(2) 不开新线程,任务顺序执行(5)
同步 不开启新线程,任务顺序执行(3) 不开启新线程,任务顺序执行(4) 主线程中:死锁。子线程中:不开启新线程,任务顺序执行(6)

详细说明:
(1)异步使得队列开启了新的线程,并行队列让任务可以同时执行(常用)
(2)虽然开启了新线程,但是队列调度方式是串行的,因此任务只能顺序执行
(3)同步意味着不能开启新线程,虽然是并行队列,但线程只有一个,因此任务只能顺序执行
(4)不能开新线程,任务队列是串行,任务顺序执行

总结:正如上面说到,使用GCD完成多线程多任务时,需要具备两个能力:开启新线程的能力,任务可以同时执行的能力,即"异步"+"并行"。两者缺一不可,缺少任何一个,队列里的任务都是顺序执行。

接下来说明一下主队列:主队列其实是一个串行队列

(5)主队列里的任务都是在主线程中完成的,即使使用异步(async),也不会开启新线程,并且主队列是一个串行队列,任务顺序执行。
(6)在主线程中出现死锁,是因为任务被加到主队列中,想要被执行block中的代码必须等到主线程上的任务都执行完毕,但是,因为是同步任务,想要主线程上的任务执行完毕,势必需要执行任务中的block中的代码,因此两者相互等待,出现死锁;但是如果在子线程中添加同步任务,并不会阻塞主线程上的任务执行完毕,因此结果会和"同步"+"串行"一致。

GCD的基本使用

GCD使用步骤分两步:

1、获取一个队列
2、将任务添加到队列中

系统会自动调度任务,通常是FIFO(先来先服务)

获取队列

// DISPATCH_QUEUE_SERIAL 串行
// DISPATCH_QUEUE_CONCURRENT 并行
dispatch_queue_create("队列标识符",队列类型);

我们可以使用 dispatch_queue_create 来创建队列,队列类型有两种:DISPATCH_QUEUE_SERIAL串行队列,DISPATCH_QUEUE_CONCURRENT并行队列。

GCD为我们提供了两种快捷获取队列的方式,一个是主列队(串行队列),一种是全局队列(并行队列)

1、获取主队列

dispatch_get_main_queue();

主队列中任务都会放到主线程中执行,并且是顺序执行。

2、获取全局并发队列

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

第一个参数是队列的优先级,一般选择默认即可;
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台

创建任务的方法

GCD为我们提供了创建同步和异步的方法,分别是dispatch_syncdispatch_async,其中第一个参数是队列,第二个是需要执行的block块,我们的任务就放在这里

// 创建同步任务
dispatch_sync(queue, ^{
    // 同步任务
});
// 创建异步任务
dispatch_async(queue, ^{
    // 异步任务
});

GCD线程通信

在开发过程中,我们通常将一些耗时的操作放在子线程,如数据请求,文件下载等,当这些任务完成后,我们需要即使的更新到UI上,这时我们就需要回到主线程

//获取【并行】队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建【异步】任务
dispatch_async(queue, ^{
    // do something ...
    [NSThread sleepForTimeInterval:2.0];
    // 回到主线程
    dispatch_queue_t main = dispatch_get_main_queue();
    dispatch_async(main, ^{
        // 更新UI
    });
});

整个过程如上述代码所示,我们获取并行队列,创建异步任务,但完成耗时操作后,再获取到主线程,将更新UI的任务添加到主线程上去。

dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
    // 更新UI
});

验证组合

(1)【异步】+【并行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【并行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

我们发现,"end"在线程输出之前,说明当前线程并未等待,而是开启了新的线程执行任务(3、4、5号线程);任务1、2、3交替完成,说明并发队列在同时执行多个任务。

(2)【异步】+【串行】

我们将【并行】队列改为【串行】队列。

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【串行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

我们发现,虽然开启了新的线程,但是队列是一个串行队列,因此任务是顺序执行的:1->2->3,另外,我们注意到,串行队列下,dispatch_async只会开启一个线程。

(3)【同步】+【并行】

我们再稍稍改动代码,从而产生并行队列,同步任务。

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【并行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

我们可以看到,虽然是并行队列,但是dispatch_sync同步的条件使得任务并没有开启新的线程,而是在当前线程(例子中是主线程)执行,并且按照顺序执行,另外,我们可以看到,"end"是在最后才输出,这就是同步的原因。

(4)【同步】+【串行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取【串行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

我们发现,同步+串行和同步+并行的结果是一致的。

最后,我们来看下,比较特殊的串行队列的主队列

(5)【异步】+【主队列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

我们发现,结果似乎和【异步】+【串行】一样,其实主队列就是一种串行队列,不同的是,主队列并不会开启新的线程,所有的任务都会放在主线程中完成,并且服从FIFO先来先服务的原则

(6)【同步】+【主队列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
结果

呃呃。。。我们发现,在主线程中,【同步】+【主队列】的方式发生了死锁,这个是因为dispatch_sync将任务添加到主队列中,任务block部分需要等待主线程上的任务执行完毕之后才会执行,但是由于dispatch_sync会阻塞当前线程,直到之前的任务都完成才会继续执行,这导致主线程的任务永不能完成,任务block里的代码也用不能被执行,从而产生了死锁。

既然dispatch_sync会阻塞当前线程,那我们将其放在子线程中会怎么样呢?

我们开启一个子线程测试

[NSThread detachNewThreadSelector:@selector(mainThreadVSSync) toTarget:self withObject:nil];

在子线程任务中,重新执行【同步】+【主队列】任务

-(void)mainThreadVSSync{
    NSLog(@"current thread:%@",[NSThread currentThread]);
    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 开启多个【同步】任务
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"3:%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end");
}
结果

我们发现,程序可以继续执行。我们使用【同步】+【主队列】时,线程不在是主线程,而是3号线程,这样dispatch_sync开启同步任务时,并不会影响到主线程,同步任务可以继续执行,只不过都是在主线程中,而且是顺序执行。

总结

1、GCD方式实现多线程多任务必须是【异步】+【并行队列】,缺一不可,其他方式的任务都是顺序执行的,无论异步还是同步。

2、主队列是一种串行队列,切忌在主线程中使用【同步】+【主队列】的方式开启任务,会出现死锁。

GCD其他

队列组:dispatch_group和dispatch_group_notify

有时,我们需要开启多个异步任务,并且所有任务都结束之后,再回到主线程执行任务,那么该如何做呢?这里我们就需要dispatch_group了。

步骤:创建group->关联任务、队列、group->接受group通知

a、我们通过dispatch_group_create()创建一个队列组

b、使用dispatch_group_async方法,将任务放在队列中,队列则关联到group

c、接收完成通知dispatch_group_notify

// 创建一个队列组
dispatch_group_t group = dispatch_group_create();
// 获取一个【并行】队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 使用队列组发起一个耗时任务
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
// 另外一个耗时任务
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
// 完成通知,在主队列中完成UI更新
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);
});
结果

我们可以看到,只有当多线程中多任务完成后,dispatch_group_notify中更新UI的操作才会被执行

dispatch_group_wait

当方法会阻塞当前线程,等待指定当group中当任务执行完成后,才会继续执行。

1中的例子,也可以使用该方法达到同样的效果

    // 创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    // 获取一个【并行】队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 使用队列组发起一个耗时任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    // 另外一个耗时任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    // 阻塞当前线程
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);
结果

但是,需要注意的是,dispatch_group_wait是会阻塞当前线程的,而dispatch_group_notify则不会。

延迟执行:dispatch_after

我们可以使用GCD快速的创建一个延迟执行的任务。当然,由于是添加到主队列的中的,因此这个延迟的时间是不准确的,这里还包括了队列前的任务时长。

NSLog(@"执行前:%@",[NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"执行时:%@",[NSThread currentThread]);
});
结果

我们发现,时差为2.2的左右,是大于我们所设的2.0秒的。

一次性代码:dispatch_once

GCD可以创建一次性代码,在制作单例时,我们常常使用到它dispatch_once,该函数可以保证某段代码在程序中只执行1次,并且在多线程的环境下,也可以保证线程安全。

-(void)onceTask{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 一次性任务
    });
}
快速迭代方法:dispatch_apply

通常我们会使用for循环遍历数组,GCD中的dispatch_apply提供了类似的方法,不同的是,dispatch_apply不仅可以是顺序的遍历,还可以是并发的遍历,主要看队列是串行的还是并行的。

NSLog(@"apply---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
并行迭代

我们发现遍历并不是顺序执行的,如果使用的谁串行队列,和使用for循环遍历是一样的效果。

信号量:dispatch_semaphore

信号量类似生活当中的信号灯,红灯停,绿灯行。GCD中的信号量Dispatch Semaphore是持有计数的信号,计数为0时等待,不可通行,计数为1或者大于1时,计数减1且允许通过。

dispatch_semaphore 有三个函数,分别用来创建信号量,增加计数量

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

使用信号量时,需要清楚地分清等待和执行的线程。

应用:

1、保持线程同步,将异步执行的任务转为同步执行任务
2、保证线程安全,为线程加锁

应用一:异步转同步

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");
结果

我们在当前线程中开启了一个异步并行队列任务,由于我们使用了信号量semaphore(其初始值为0,不可通行),当我们使用dispatch_semaphore_wait,会阻塞当前线程,直到信号量不为0时,才会执行NSLog(@"semaphore end")的输出,上述例子中,并没有给信号量计数器加1,因此不会执行后面输出任务。

我们为其添加加一

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");
结果

此时我们发现,程序正常输出。另外,我们这里通过通过信号量实现线程的同步操作(输出在异步任务之后),原本的异步线程在这里并没有什么效果,和同步任务没有任何区别

应用二:线程安全

结合GCD的信号量的特性,我们还可以使用其达到线程安全的目的,即在多线程下,保证事务的原子性。

假设我分别开启两个线程去做加一操作,在不保证线程安全的情况下,势必会出现线程争抢资源,导致意想不到的错误产生。

首先我们来看下非线程安全。

dispatch_queue_t queue1 = dispatch_queue_create("one", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("two", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
    [self addCount];
});
dispatch_async(queue2, ^{
    [self addCount];
});

-(void)addCount{
    while (1) {
        if (count>=100) {
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
    }
}
结果

我们发现,在此次过程中,线程3和4发生了资源争抢的问题,导致在加1的过程中发生了错误,两个线程累加的次数总和超过了100次。

接下来,我们使用信号量来保证事务的原子性。

// 创建信号量
semaphore = dispatch_semaphore_create(1);
// 在事务的开始和结束时操作信号量
-(void)addCount{
    while (1) {
        // 信号量减一,进入等待状态
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if (count>=100) {
            // 信号量加一,进入可通行状态
            dispatch_semaphore_signal(semaphore);
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
        // 信号量加一,进入可通行状态
        dispatch_semaphore_signal(semaphore);
    }
}
结果

我们发现,线程3和4交替操作累加,数字累计达到100时,操作总和为100,因此我们可以认定,信号量起到了很好的效果,保证了事务的原子性。

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

推荐阅读更多精彩内容