多线程编程----GCD

从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了

    dispatch_queue_t squeue = dispatch_queue_create("squeue", NULL);
    //    dispatch_queue_t mainQueue = dispatch_get_main_queue();
  
    NSLog(@"Main Queue-->%@", dispatch_get_main_queue());
    dispatch_sync(squeue, ^{
        NSLog(@"Task 1-->%@", [NSThread currentThread]);
    });

在main()中执行没问题,如果dispatch_sync改用mainQueue的话就卡死了。
squeue和mainQueue都是主线程里创建的串行队列,为什么前者正常,后者会卡死?

问题描述得可能不太准确,重新整理。
如下:

//-------------------------------------------
int main(void) {
dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil);
dispatch_async(queue, ^{ //任务1
    [self goDoSomethingLongAndInvolved]; 
    dispatch_sync(queue, ^{ // 任务2
        NSLog(@"Situation 1"); 
    });
});
return 0;
}

这种场景下包了2层的的调用会死锁,任务1和任务2起始条件互为执行结果,形成直接制约,死锁。

//-------------------------------------------
int main(void) {
dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil);
dispatch_sync(queue, ^{ // 任务1
        NSLog(@"Situation 1"); 
});
return 0;
}

这种场景正常。注意,这里只包了1层,queue是主线程生成的串行队列。

//-------------------------------------------
int main(void) {
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ // 任务1
        NSLog(@"Situation 1"); 
});
return 0;
}

这种场景线程卡死。注意,这里只包了1层,queue是主队列。
那么,对比上一场景,同样只包了1层同样是主线程里的串行队列同样sync调用,为何上一个正常这个卡死?
结合第一个场景来推理,场景3卡死是因为sync函数调用时本身就运行在主队列中?

话题是开放式的,所以我觉得有必要从最基本的东西来说说我的看法,因为我想很多人跟我刚开始一样,有些基本概念容易弄不清楚,甚至混淆在一起,然后用的时候只知其形而不掠其意,所以才会出现很多的问题。多线程编程尤其gcd这块理解起来确实有一定的难度,所以我想用非常通俗的例子来说出我的理解,毕竟是个人理解,如果有偏差和错误,还请各位不吝赐教,互相学习

首先,队列、线程、runloop到底是什么?
关于这点别人说的太多我就不复制了,但是都很官方,我谈谈我的看法

gcd队列(先不讲nsoperation了)其实就是个任务调度系统,何为调度?打个比方,现在我是大厨,我手底下有很多小厨,今天给一家喜酒做菜,很多菜,作为大厨,我改怎么分配任务呢?怎样才能以最快的速度,最好的质量完成这次喜酒呢? 这就是大厨要做的工作,所有的这些菜,每一个菜就是一个任务,比如现在需要做四道菜西红柿炒蛋,蛋炒西红柿,炒西红柿蛋,炒蛋西红柿,我会写张条子,把四道菜列上去,这个条子就是队列,其实应该叫任务队列更准确,本身队列自己没有什么特别的属性,你可以认为他就是个容器,但是有两种不同的容器,一个是串行队列,一种是并行队列,这两个队列都是先入先出队列,除非有特别情况(nsoperation)指定队列中任务的优先级,这个不再我们讨论范围之内,记住,不管是串行还是并行队列,只能一个个进队列,一个个出队列,即使是并行队列,也是一个个出 不是有些人想想的 并行可以一起出。。宏观上讲,一个个出的速度很快,给你的错觉就是同一时间一起出来的,其实不是,并行队列也是一个个出来的

现在我们有两种队列,一种串行的 一种并行的,本质讲它们并无区别,但是它们允许的行为有很大区别,串行队列 你做完一个任务,才允许你从里面执行下一个任务,并行,无次限制,你可以一次性取完,你也可以一次性只取一个 这都不是问题,而队列是通过什么手段达到这种效果? 这就是队列的本质,其实是对线程的封装。
各位都知道线程就是一个任务的执行过程,所以到这你也应该直知道了,串行队列里面跑的,肯定是唯一的一个线程, 而并行队列里面跑的,可以有多个线程

队列是对线程的封装,但是一定要清楚,队列本身 和线程本身就是两码事。算了我举个例子吧,我自己都糊涂了。。。

以前没有gcd的时候,或者以前写linux程序的时候,我们不是没队列照样码的很开心吗?gcd 属于苹果核心系统,是封装了之前我们熟知的多线程操作,他是在线程之上,给一个容器,这个容器就是任务队列,所以反复的讲,队列是对线程的封装,他是在线程的基础上加上了任务容器

这才组成了现在的 串行队列 并行队列,用户使用的时候,不需要关心线程问题,如果你要读写线程,肯定还是操作nsthread 但是你在队列里面基本非常少的用到这个,这就是苹果的目的,淡化线程概念,强化更加现实的,可见的,更容易理解的任务队列
即: 线程+任务容器 = 队列


下面来讲讲runloop,至于runloop 是什么鬼,本质是啥不啰嗦,度娘懂得比我多。我只想说runloop 跟上面那俩货是怎么个关系
runloop 本身跟队列没什么鸟关系,跟它有不正当男女关系的是线程

线程才跟runloop 有纯洁的友谊,本身来讲所有线程,包括系统创建的、你手动创建的 屁如:nsthread alloc ........

这种,本身都自带runloop。只不过,主线程比较特殊,他自己的runloop 进来就打开的,谁打开的?反正不是你。。。其他你能接触到的线程,默认都是关闭runloop的,那么带这么个东西的目的是为了个啥? 你们都懂得我知道,无非就是防止线程退出,可以让线程休眠、唤醒等等

各位都知道,新开线程是需要开销的,如果执行一个任务,就开一次线程,执行完毕就释放,新任务来 再开线程执行完,再释放,如果任务大 还无所谓,如果任务特别细碎,并且非常多,你开线程,释放线程的开销,比你本身处理这些细碎任务造成的消耗要大得多。因此才带了runloop,用来讲线程就是说 有任务的时候唤醒,无任务的时候休眠但不退出,有任务再来,再换型

所以现在脑子里应该有个模型 队列处于最上面-》线程-》线程代的runloop

当你创建了一个队列的时候,实际上这时候这个队列是空的,这没问题因为你没有给这个队列里面丢进去任何的任务,所以他是空的,在没执行的时候,这个队列里面的任务在哪个线程执行并不能确定

举个例子

- (void)viewDidLoad {
    [super viewDidLoad];
    queue = dispatch_get_mainqueue();
    dispatch_sync(queue, ^{
        NSLog(@"111");
    });
    NSLog(@"2");
}

同步提交的block块,如果同步的目标队列是当前上下文环境一样的队列,ok 那会导致死锁,原因很简单,还是看上面的代码,viewdidload 是主队列执行的,viewdidload{}本身可以看作主队列里面一个正在执行的任务,当执行到dispatch_sync的时候,注意这时候主队列的任务还未完成,因为还没跑到nslog2, dispatch_sync 是同步操作,同步这个词用的太差了。。。应该说是等待操作。dispatch_sync 等待 queue(主队列) 执行完最后一个任务之后,把nslog(@"111")递交给他,坏就坏在这个等待,等待最后一个任务?? 那意思就是说执行完nslog(@"2")之后才执行nslog(@"1"); 所以 dispatch_sync 一直等着执行 NSLog(@"2"); 但是。。。主队列是串行的,麻痹的单行车道你不动别人就没法动。。。

所以 死锁就产生了,我们可以总结一个规律:

如果使用dispatch_sync,如果执行dispatch_sync 这句的上下文环境的“队列”,跟 dispatch_sync(queue, ^{ 的这个queue 是同一个队列,不用想了绝逼死锁。。。当然还有一个条件,这个队列必须是单行车道,也就是串行队列,如果是并行的,不会有死锁,为什么呢?

假如queue是个并行队列,那就是多车道,当执行到dispatch_sync 的时候,dispatch_sync需要等待当前任务执行完毕,但由于并行车道,他可以把车挪到一边去等,后面的车就可以继续通行,当走到NSLog(@"1");执行完了,来一句,我执行完了,你快发动吧。。。这时候,NSLog(@"2");才会打印出来

也就说别管你 dispatch_sync 放在什么位置你绝对是你提交的目标队列里面 在你提交的瞬间,最后一个执行的,也就说你前面1000个任务在排队,在串行队列的情况下,你只能最后一个执行

强调一下:

如果使用dispatch_sync,如果执行dispatch_sync 这句的上下文环境的“队列”,跟 dispatch_sync(queue, ^{ 的这个queue 是同一个队列,不用想了绝逼死锁。。。当然还有一个条件,这个队列必须是单行车道,也就是串行队列

记住这点,dispatch_sync 用起来很容易死锁,而且死了你都不知道死在哪。。。因为程序本身并为出错,线程被阻塞,或者说队列被阻塞,
如果阻塞的是主队列,那你完全不能交互,因为主队列负责响应用户交互,一切可见的由主队列绘制的ui 都停止不动,但是由gpu绘制的不受此影响,所以这种死锁要么整个屏幕都动不了,要么有一部分动不了,反正就是很明显

如果阻塞的是其他队列,那完了。。。。。。比如,我下载图片的队列,那完了。。。明明网络好的很,就是他妈的不给我刷新图片,你找后台打了一顿架,p用没有,因为你阻塞你的图片队列了。。。。。。。然后回头后台再把你打一顿

你俩互相都挨了一顿打,貌似对整个事件没有产生任何影响。。。。。。。

继续往下讲吧,上面讲了个同步队列的操作。关于异步队列的操作也就是dispatch_async 就不说了 这个都会。。。。区别就是这玩意“不等待”

关于队列和线程,上面说了,队列是对线程的封装,苹果目的是让开发者淡化线程的概念,而强调实实在在的任务队列的感念,因为线程操作从某种角度来说,属于很虚的范畴,比如线程我可以比喻成 做一道菜的过程,重点在这个过程,而不是做菜,如果是队列,强调的是做菜而不是过程,所以这个玩意,我词穷了,,,意会一下吧。。。

线程相对抽象了,但是队列封装了线程之后就变得不怎么抽象了,容易理解一些,但是讲真,我朝p民跟番外蛮夷的脑子结构真不一样,反正我觉得具象成队列之后反而有些把简单的事情搞复杂了。。。

我在强调一下,只有线程才跟这个runloop有不正当的男女关系,跟队列没什么鸟关系,一个线程,有一个runloop 对应,一谈runloop 必谈线程,因为没有现成runloop 就无从谈起,但是谈线程,不一定牵扯到runloop因为 默认的,除主线程之外,所有线程的runloop 都是关闭的

这里有个插曲,大家都对nstimer很熟悉,经常用,但是大家知道nstimer 是怎么回事吗? nstimer 其实基于的runloop,一般情况下,nstimer用在主线程的多,因为主线程的runloop 是打开的,并且你是关不了的。但是用在子线程的timer就不一定了,那要看runloop 是不是打开的,如果是关的,那timer 不会起作用,timer 是在线程之上,在某些感兴趣的点上设置标记,在runloop 开启的情况下,runloop 跑到这个点就会有这个timer时间点执行,所以说 nstimer 依存于一个开启了runloop 的线程,也可以说timer 就是runloop的一种封装

串行队列异步执行的情况下,执行一个任务,这个任务在线程a ,等他执行完,又从串行队列拿一个任务出来,继续执行,这时候发现执行第二个任务的线程很大可能不是a 而是b, 他说的是对的,这也就是为啥 推荐使用全局的并行队列,而不是自己整一个队列出来,因为全局并行队列维护着一个线程池,啥意思呢,就是他的线程池,可以智能的调度线程使用情况,比如上面的情况,执行了两个任务,用了俩线程,有必要吗?必然没有,为啥我不能用一个线程搞完?因为我说了,线程执行完一个任务,就真没他什么事情了。。可以释放了,因为你没runloop

假如我们做一个改进,自己通过 nsthread alloc 一个线程,然后开启这个线程的runloop,然后run起来,那我岂不是就能一个线程把队列里面所有的任务都执行完了,那我就不用开几个线程就没有线程创建的消耗了?对吧 在执行串行队列的第一个任务的时候,这个线程我们将他的runloop打开,执行完第一个任务之后,自动休眠,第二个任务的时候通过 addport换型这个线程,这样,就可以一个线程把任务干完了。。

现在使用gcd之后,相信很多人都对这个很熟悉,而且所谓的我会多线程大多数也做的很6,因为封装了嘛,但是很少人去优化自己的多线程代码。就比如说22楼说的那种情况,一个串行队列,你完全可以用1个线程 还是用100个线程 最后的效果速度是一样的,而且我敢说,你用1个线程执行比用100个执行的块,因为串行,智能一个个来,1个线程少了线程的切换、创建、销毁所以会快

有一个经典的场景,客户端跟服务器之间是tcp 链接,服务器会不定时的给客户端发送数据,这些数据呢,可能是经过压缩的,可能是经过加密的,那客户端就必须要有 对应的解码、解密的过程,这过程说长不长,说短也不短,看很多app的代码 发现从网络层丢上来之后的数据直接用主线程去做解压解密的处理了,这是最低级的一种,略高级的,知道把这操作方到其他队列执行,如果使用异步操作,其他队列必然是在子线程进行,执行完之后抛给主线程做ui刷新,这种已经有了初步的多线程编程的意识了。但是最高级的一种,是维护一个常驻线程,这个线程不干别的,专门用来处理解密解压,也有的设计成,解密用一个常驻线程,解压用一个常驻线程,这样就可以并线操作,速度会快一些
如果数据量不是很大,这个没必要,如果数据量非常大,那就非常有必要,但是想象手机那点性能,所以后面这种并不常用,开一个常驻线程够了,但是也不排除会有线程阻塞,这种阻塞就是说你解密解压的速度赶不上数据来的速度,这种就需要使用并行队列了。。

对一个并行队列做同步操作,就如同对一个串行队列做异步操作

这句话比较绕 也不太容易理解

还记得同步操作的特点嘛?就是等 并行队列,正常来讲如果做异步操作,会同时有多个线程去执行,因为不用等 不用等第一个任务完成才去执行第二个,但是如果是同步操作,那就必须要等第一个任务完成才开始执行第二个,及时你队列是并行的,但我做的是同步操作,对吧?
好好理解一下。。。

另外我前面说了,不管是并行队列还是串行队列,都是先入先出队列(排除队列优先级调整)就是说 你这个任务先来的,那必然线执行,不管是并行队列还是串行队列都是如此

哪有人问了,那为啥并行队列的异步操作 输出结果还是不按顺序来? 其实这就是线程创建、启动快慢的问题了

在并行队列第一个任务拿出来执行的时候,假如我们用的是系统提供的全局并行队列,当第一个任务来临,这时候线程吃中没有空闲的线程,那它必须得重新创建一个,当第一个任务拿出来之后,ok 我可以那第二个任务了,因为是异步操作,我不用等第一个任务完成,当第二个任务拿出来的时候,发现线程池有空闲线程了,这时候直接就加进去执行了,所以2先出来1后出来

这个不用纠结,并行队列的异步操作本来就不保证执行顺序的先后

如果有人跟你说,并行队列一定不能确定任务执行的先后,别怕,上去照它脸上糊 然后说:你放屁!并行队列的同步操作就能确定任务执行的先后

对一个并行队列做同步操作,就如同对一个串行队列做异步操作

回到最开始,一个个问题解答

/-------------------------------------------
int main(void) {
dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil);
dispatch_async(queue, ^{ //任务1
    [self goDoSomethingLongAndInvolved]; 
    dispatch_sync(queue, ^{ // 任务2
        NSLog(@"Situation 1"); 
    });
});
return 0;
}

这种场景下包了2层的的调用会死锁,任务1和任务2起始条件互为执行结果,形成直接制约,死锁。

好了开始分析 dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil); 这句在主队列(注意我强调的是队列不是线程,即使这个代码确实在主线程跑的)里面CREATE了一个queue 串行队列

dispatch_async(queue, ^{ //任务1 异步操作,dispatch_async 跑在主队列,异步操作吧BLOCK 内部提交给queue去执行

[self goDoSomethingLongAndInvolved]; 这局代码属于queue自定义队列的第一个任务的上半部分

dispatch_sync(queue, ^{ // 任务2
NSLog(@"Situation 1");
});
这个属于queue自定义队列的第一个任务的下半部分部分

好了 结合我们之前的分析,dispatch_sync 这句执行的的时候,如果上下文环境所处的队列,跟 dispatch_sync(queue, ^{ // 任务2 里面的这个QUEUE 同属于一个队列,那必然导致死锁,原因不在赘述,所以这个死锁的问题很简单

int main(void) {
dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil);
dispatch_sync(queue, ^{ // 任务1
        NSLog(@"Situation 1"); 
});
return 0;
}

这种场景正常。注意,这里只包了1层,queue是主线程生成的串行队列。

开始分析。。。
dispatch_queue_t queue = dispatch_queue_create(“com.somecompany.queue”, nil);////////////// 这句在主队列新建一个自定义队列

dispatch_sync(queue, ^{ // 任务1 ///////////////这句,dispatch_sync 执行的上下文环境是主队列,dispatch_sync(queue, 内部提交队列是queue队列,不是同一个队列,所以不会死锁

int main(void) {
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ // 任务1
        NSLog(@"Situation 1"); 
});
return 0;
}

这种场景线程卡死。注意,这里只包了1层,queue是主队列。
那么,对比上一场景,同样只包了1层同样是主线程里的串行队列同样sync调用,为何上一个正常这个卡死?
结合第一个场景来推理,场景3卡死是因为sync函数调用时本身就运行在主队列中?

继续分析

首先纠正一点,说线程卡死不太准确,准确的说应该是队列卡死,因为队列卡死,才造成线程卡死

dispatch_queue_t queue = dispatch_get_main_queue();///////主队列里面去获取主队列本身

dispatch_sync 上下文环境是主队列

dispatch_sync(queue, ^{ // 任务1 //////// 提交到的目标队列是dispatch_get_main_queue 还是朱主队列,死锁

所以:dispatch_sync 执行的上下文环境所处的队列,如果跟提交到的目标队列是同一个队列,别管这个队列是main队列还是你手动创建的,都会死锁(仅限于串行)

多线程分析的要点是:

1、上下文环境所处的队列 2、执行同步或者异步操作所提交到的目标队列 掌握这两点,是必须的

下面来看一段代码


111.png

这是输出:

222.png

可以发现 输出 111 222 333 的语句 是在同一个线程中执行的,线程地址是0x17006ff40 而输出444的却是在另外一个线程进行,但是它们同属一个串行队列,所不同的仅仅是 444 延迟了几秒执行, 这就使用的新的线程? 这是为何?

首先从上面的输出结果来看,同一个串行队列,使用了不同的线程。。。这似乎很奇怪,按道理来说,它根本没有必要对一个串行队列分别去开两个线程执行对不对?我一个线程就可以干完所有的活,但实时上它确实先后开辟了两个线程
所以,第一:线程跟队列并不是一一对应的,队列处于线程的上层,线程属于被队列调度的地位,我们使用队列的意义就是脱离线程管理,专注于任务的处理而不是线程的调度管理,所以从这点上来说苹果的出发点是好的,但是不好的地方也要指出来,如果使用队列不去关心下面的线程,很容易让人产生疑惑,毕竟,一些老的开发者熟悉的还是线程而不是队列

那么我们说说这个原因:为何同一个串行队列 先后使用了两个线程。原因分析起来其实不难,首先第一点确定:串行队列里面所调度的线程,可以肯定的说,在一个runloop的区间内用完就释放了。

我们从最开始来分析这段代码, viewdidload{} 这段代码,可以认为是 主队列的一个任务片段,这个任务片段处于同一个runloop周期,这点不难理解对吧?另外再看内部代码。前3个dispath_async 同属于这个任务片段内的一个小片段,也就说 这三个小片段也同属一个主队列的runloop 周期,另外有由于是异步操作,因此很快能够返回, 也就说:主队列(ui队列)在一个他自己的runloop 周期之内(就是主队列的runloop周期)将这三个任务调度进入了queue队列,queue队列在第一个任务加入之后开启一个线程,然后开始运行加入到queue中的任务
,我们应该知道,一个thread 应该是在runloop开始的时候开启,然后在runloop 结束的时候 释放, 所以这个thread 开启的点就是第一个任务加入,然后第三个任务释放,因此,前三个任务才是同一个thread 在执行

然后看最后一个dispath_async, 他外面包含一层延迟函数,我们也知道所有的延迟函数都是基于runloop的,这个延迟函数时在主线程上跑起来的,那必然也就使用了主线程的这个runloop
我们经常讲runloop,字面意思就是循环,它来循环的唤醒、挂起她所在的线程,在这里也就是循环的唤起、挂起主线程,我举个通俗的例子来说下什么是主线程的唤起和挂起
假如我们的app启动,这时候就有一个开启了runloop的主线程跑起来,因为开启了runloop,所以这线程不会退出,然后做一系列的初始化工作,直到所有初始化做完,然后没有了用户交互(比如点击),这个时候,runloop 就让主线程处于挂起状态,不再跑,这时候如果有一个交互,比如说手指点击屏幕,ok这个时间就被捕捉到然后传递给runloop 点击事件被加入主队列,然后我们又知道 runloop 接受的事件大概包括那么几种(自己百度 我也忘了。。。) 这个点击事件就属于其中一种,而后runloop接受到这个时间之后发现:卧槽,可以的,我可以被叫醒,然后驱动主线程去读主队列的任务,这时候主队列里面可能有1个或者多个任务需要处理,到这个时间点为止,主队列里面的所有未处理事件就同属于当前的这个runloop 循环了
同一个runloop循环,你这个queue队列又是在这循环产生的,所以主队列的当前这个runloop的当前这个循环结束的时候,queue最开始开启的这个thread 就释放了。。。这是为什么 前三个任务在同一个thread 以及 这个thread 什么时候开启什么时候释放的故事

至于最后一个dispatch_asnc 因为又个延迟函数,他在主runloop的 当前循环的下一个或者几个循环里面 由时间源出发从而运行,这里为什么说一个或者几个? 以当前这个runloop的当前循环开始算起,如果代码跑到viewdidload的}之后,没有其他能触发runloop循环的输入员,那3秒之后,就会由时间源触发 主线程的runloop 跑起来,如果3秒之内有交互,比如我点了一下屏幕,那这中间会插入由交互所产生的这个循环

记住一点,一个runloop 可以有无数个循环,但这些循环都是结束一个再开起一个,串行的,

所以讲到这点,为啥说nstimer不太准确? 原因就在这了,按道理 我3秒之后时间源触发runlopp 从而主线程去给我执行任务,但是,假如3秒的时候正好有用户 点了屏幕,触发了其他可以唤醒runloop的操作咋办?? 线程是唯一的啊 ,同一时间智能进行一项工作,所以timer 可能前移,也可能后移,一般后移的几率较大
扯来扯去,又扯到timer的不精准的特性上了,素以能明白这快,以前很多问题自己就能推敲出来了。。。。。。。。。

接下来说下队列和任务:

队列分为串行和并行

任务的执行分为同步和异步

这两两组合就成为了串行队列同步执行,串行队列异步执行,并行队列同步执行,并行队列异步执行

而异步是多线程的代名词,异步在实际引用中会开启新的线程,执行耗时操作。

那我们先来知道一个非常重要的事情:

------- 队列只是负责任务的调度,而不负责任务的执行 ---------

------- 任务是在线程中执行的 ---------

队列和任务的特点:

队列的特点:先进先出,排在前面的任务最先执行,

串行队列:任务按照顺序被调度,前一个任务不执行完毕,队列不会调度

并行队列:只要有空闲的线程,队列就会调度当前任务,交给线程去执行,不需要考虑前面是都有任务在执行,只要有线程可以利用,队列就会调度任务。

主队列:专门用来在主线程调度任务的队列,所以主队列的任务都要在主线程来执行,主队列会随着程序的启动一起创建,我们只需get即可

全局队列:是系统为了方便程序员开发提供的,其工作表现与并发队列一致,那么全局队列跟并发队列的区别是什么呢?

1.全局队列:无论ARC还是MRC都不需要考录释放,因为系统提供的我们只需要get就可以了

2.并发队列:再MRC下,并发队列创建出来后,需要手动释放dispatch_release()

同步执行:不会开启新的线程,任务按顺序执行

异步执行:会开启新的线程,任务可以并发的执行

那么有这么几种组合
串行队列同步执行:综合上面阐述的串行队列的特点 --- 按顺序执行,同步:不会开启新的线程,则串行队列同步执行只是按部就班的one by one执行。
串行队列异步执行:虽然队列中存放的是异步执行的任务,但是结合串行队列的特点,前一个任务不执行完毕,队列不会调度,所以串行队列异步执行也是one by one的执行
并行队列同步执行:结合上面阐述的并行队列的特点,和同步执行的特点,可以明确的分析出来,虽然并行队列可以不需等待前一个任务执行完毕就可调度下一个任务,但是任务同步执行不会开启新的线程,所以任务也是one by one的执行
并行队列异步执行:再上一条中说明了并行队列的特点,而异步执行是任务可以开启新的线程,所以这中组合可以实现任务的并发,再实际开发中也是经常会用到的

GCD实现原理:

GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决、、、、、定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?)

而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。
如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。
如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。
这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开58条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:35条最为合理。

333.png

同步&异步

所以说很多人 不太了解同步 异步到底该怎么解释。 所谓同步其实就是等待,所谓异步就是不用等待,这样简单的理解就足够了
dispatch_sync(queue, ^{
NSLog(@"111");
});

很重要下面这句话
这段代码的意思就是 把 NSLog(@"111"); 这个“任务”,提交给 queue队列,并且等待 queue 把这个任务执行完。

所以 120 楼的结果就不难理解了,虽然队列是并行的,但是 我提交一个任务就等待 执行结果,否则 队列一直就处于“被等待” 或者说阻塞的状态,只有执行完了,NSLog(@"111") 这个任务,才能继续进行

但是所谓 并行队列一定 职能同步等待一个任务吗? 其实不一定的,这需要看 你是以同步的方式 还是异步的方式 提交任务给队列的,

同步任务: 我需要把一个任务提交给队列,并且等待,直到 这个任务执行结束。
异步任务: 我需要把一个任务提交给队列,但是我提交完成就ok,我不需要知道这个任务什么时候开始执行,什么时候执行结束,这些我不关心

更复杂一些, 我们可以考虑 在一个并行队列中,同时有同步任务 还有异步任务,这时候就热闹了,不过根据我们最基本的分析,还是能猜到正确的执行过程

假如下面这个例子,在上面例子的基础之上,我随意添加了几个异步的任务,这时候顺序就有点意思了,我先把结果放出来

444.png
555.png

我们先分析 “睡3秒”之前的代码,有同步 有异步, 同步就等待执行完,异步不等待,所以 111 的代码肯定先执行完, 然后紧接着 来个两个异步任务,用来打印
异步111、异步222 这时候代码直接把这俩打印任务 交给队列,然后一瞬间直接去执行 同步打印 222 的任务了,这时候的情况很微妙。 首先异步111 异步222 是先后提交到 queue队列的 这点没问题,而且是 异步111 首先提交,然后 在提交异步222,这没问题,然后又在一瞬间 提交了 同步222 这个任务

所以这个代码到现在可以人为, 在极短的时间内,并行队列“先后受到三个任务”,分别是异步111,异步222 然后还有同步222(222),提交之后立刻执行,所以宏观上来看,打印 异步111、异步222 以及 222 是同时进行的!!! 这就有意思了。。。 打底 异步111、异步222、 222 哪个先出来???讲真,,,,每次执行的结果都不太一样。。。。这就要比较哪个操作比较耗时了,因为从宏观上来讲他们三个同事进行,这就要看每个任务的复杂度,以及线程池线程的使用情况了。。。所以说 这三个任务,到底哪个先执行完毕,我只能说。不知道。。。

如果不信的话,请按同样的分析方法 去分析 睡3秒之后的代码,然后你就会明白 为什么444 555 还又 异步333 这么怪的输出顺序了

所以根据以上的特点,我们可以利用任务同步操作 做许多有意思的事情,
比如多线程里面 修改同一个值 可能会造成资源竞争的情况出现,或者其他不可预知的错误,这就是多线程同时操作同一个内存,会造成混乱

假如我们使用同步操作,意思就是,我他妈闲修改,等老子修改完之后你们在再去读或者去写,这岂不是同步在队列里的一个很好的应用

接下来再看一段代码:

666.png

结果:

777.png

首先讲,这段代码是跑在主队列里面,然后主队列又是个串行队列,并且有且只有一个主线程,sync同步操作,“同步” 这两个字应该这样理解:就是把提交到某个队列的任务,强制让它用上下文所处线程来执行,大家注意看那个线程的名字 name = main 这样说就很容易理解了,同步的意思就是 把提交到队列的某个任务,让他用上下文所处的线程执行,至于如何让提交到队列的任务跑在上下文所处的线程上,这就是前面一再强调的:

队列的作用就是把任务容器的任务 调度 到线程上执行

ok 现在我们不写demo,用现在我们之前推论并且验证掌握的知识,再推:假如对一个并行/串行队列执行异步任务,那异步任务是跑在哪个线程?会不会和上下文所处的线程是同一个?

我们知道 同步就是强制队列把任务调度到上下文所处的线程上来执行,那么异步,光看名字,我就知道必然跟上下文环境的线程不一样。。。so

很容易理解了,异步提交的任务到底在哪个线程执行并不一定,但是肯定不是上下文所处的线程

这也就反向验证了度娘说的:同步无论如何不会开启新线程,异步肯定会开启新线程

其实这句话后半部分并不精准, 应该说:异步开不开新线程不知道,但是肯定不会让异步提交的任务 让 上下文所处的线程执行

这也就是为什么我前面说的:好多人都被度娘的这句话误导了。前半部分是对的,但是后半部分 只说对了一半。。

因为开不开新线程,并不是异步提交来决定的,而是由队列这个线程调度的掌控者来决定的

因为每个队列下面都维护有线程池,如果线程池的线程有空闲,我为何要重新开辟一个新线程呢?我只需要线程池的线程,有空闲,并且空闲的线程根山下文的线程不是同一个 就可以了。。。

我们拿上面的例子来解释一下:

同步就是强制把提交的任务 让当前上下文环境所处的线程来执行,首先你要知道,当前上下文所处的线程,肯定是在跑任务,肯定是在跑你的同步提交的代码。这个强调的意思就是:你上下文所处的线程现在正忙。ok 突然,你同步提交了个任务。你强制让你这个线程去执行新任务。这时候它就必须断掉原来的任务(断掉的任务就是后面还未跑完的代码,尤其注意的是 } 也算代码!!)去跑你这个同步提交的任务,所以 这也就是我说的等待 等待 你同步提交的任务执行完毕之后,再来执行下面没执行完的代码

下面从线程的角度来说阻塞的问题(死锁的本质)

假如上下文环境是主队列,然后你同步提交一个任务给主队列,根据以前说的,如果同步提交任务的目的地队列,跟现在上下文环境所处的队列是同一个队列,那么会产生死锁,为何呢?来分析

首先代码在主队列里安静的跑,跑着跑着碰到了你同步提交的代码。那么紧接着,就开始跑你的同步代码,首先你同步代码要求把任务提交给主队列,这没问题,但是问题在那? 当你提交了你的任务之后,队列就要开始调度线程来执行,这没问题吧?主队列现在开始它的调度工作,他发现,它所能调度的只有一个线程,那就是主线程。为何?因为主队列里面只有一个线程在跑,一个串行队列 不可能同时跑多个线程,ok现在静下来想一想,是不是所有的串行队列 在任意一个时间点内,只有一个线程在跑? 如果想不明白,那就想想主队列,是不是自始至终都是只有一个主线程在跑?(其实串行队列可以跑多个线程,但是这些线程是先后跑的,不能同时跑!!比如,123秒之内跑的是线程1,456秒之内跑的是线程2,纵向来看,串行队列确实跑了两个线程,但是横向来看,某个时间点之内有且同时只有一个线程在跑,至于为何会这样,原因就跟runloop有关,主队列之所以自始至终只有一个追线程,原因就是这个线程开启了runloop 所以他不会推出,而串行队列同时只能跑一个线程,结合来看,那主队列只能跑一个主线程,至于其他串行队列,之所以前后可以跑不同的线程,原因就是默认的线程runloop 并未打开,在这个线程的生命周期之内接到的任务,都会用这个线程跑,任务执行完,就退出了,因为没有runloop,那么下一次再来一个任务,队列只能再从线程池拿一个线程,至于这个线程是否跟上面那个刚刚退出的线程是一个线程,这个不一定,这要看队列的心情。。。)
好了。思维跳回来,你同步提交了任务给主队列,意思就是说:主队列,你必须给我把任务执行调度到我现在所处的线程上来!
现在,作为主队列,它很为难,为啥?因为它只有一个线程可用。。。那就是主线程。但是。。你要知道。。。主队列早已经把主线程的调度权给交出去了。。交给谁了?交给从一开始执行到你同步提交任务这段代码上了,你一个那啥不可能同时让两个男人那啥吧(有人说:我可以的。我要说:下次请带上我!)?你已经调度了一次主线程了,你无权在调它一次,因此,这时候就卡死在 主队列调度线程这个点上,,所以为什么说,阻塞并非是线程卡死,而是队列卡死,因为队列这个调度权的冲突,导致无法调度,从而导致线程已经断开了原来的执行,等待队列调度它去执行同步任务,而队列这时已经无权再调度一个任务了,这时候的情况是,线程停下来手头的工作,等待被召唤,而召唤师此时网费不够了,无权再玩游戏

好了 做个总结:

从线程角度来讲:同步的意义就是:告诉提交到任务的目标队列,你必须马上立即开始你的调度权利,把我(同步操作)给你(目标队列)的任务(同步提交的任务)调度到我现在所处的线程上来运行,代码就是dispatch_sync(同步操作)(queue(目标队列),^block(同步提交的任务));

假如这时候queue 是一个串行队列,那么在这个瞬间,他只有1个线程可以调度,假如这个线程没有被他调度,也就是说队列现在是空闲的,OK那没问题我直接调度一个线程,并且我调度的这个线程就是你跑dispatch_sync 的这个线程立刻暂停原来的任务,转而来执行同步提交给我的任务,等我这个任务执行完毕之后,这个线程再继续原来的暂停的任务

假如queue已经调度这个任务去执行dispatch_sync 这段代码,现在你又让我(queue)去调度我唯一能调度的任务去执行同步提交的任务,臣妾表示做不到啊。。。。。我只能对一个线程同时调度一次!

番外篇:

在app 的框架角度来看,一个大功能模块的处理:比如一个底层tcp 通信模块,你可以用一个串行队列来处理,为何建议用串行队列而不是并行?
因为说实话我现在理解的是 本身tcp socket 是不具备并行能力的,这个我理解的可能有偏差,放下这点不说,我测试过 如果用串行队列,玩去啊不会对数据收发造成阻塞,并且这个队列只负责维护数据的收发,不负责数据封装,另外用串行队列的好处就是 在这个串行队列维护的环境下,任何变量或者奖任何资源都没有竞争的前提,因为串行,所以一次只能有一个写/度,这就不会造成多线程操作造成的资源枪战,或者一些莫名其妙的问题。
实际验证中也显示:确实没有任何问题。 因此 通信模块整个都跑在一个串行队列里面

下面说业务层,从tcp 串行队列里面出来的数据,异步提交给业务层数据处理模块, 业务层数据处理这个模块 整个夜跑在一个串行队列里面,之所以不把这两个串行队列合并成一个,是为了排除互相影响的可能性,这两个串行队列是通过异步来联系的,所以谁也不会阻塞谁,数据经过处理模块的串行队列 处理之后,然后直接再异步提交给ui队列也就是 主线程刷新,这就形成了三个串行队列并行的情景

在某个时间点内,ui队列正在刷新数据,数据处理队列正在处理将要提交给ui队列的数据,tcp队列正在接受将要提交给数据处理队列的数据
整体来看,三个串行队列 是并行运行的,互不影响。互不干涉,这个架构是我根据我现在的app 做的一个最终选择,之所以避免使用并行队列,主要考虑的就是一个资源竞争的问题。

因为我看到过一句话,一个好的多线程编程,不是多么精妙的使用什么锁去避免资源竞争,而是从设计最开始要尽量不做出有可能多线程操作同一个资源的情况

分层设计:
分层设计的好处不言而喻,底层就处理底层的事情,业务层就处理业务层的事情,UI层就处理UI层的额事情,这就好比我早上起床上班,刷牙洗脸肯定在家里进行,吃早餐肯定在早餐店里,上班肯定在公司,如果我洗脸刷牙在早餐店,吃早餐在公司,上班在家,老板会砍了我,当然,吃早餐这点小事也可以在公司进行,这就好比有些业务层的功能放到UI层去处理,虽无伤大雅,但是严格来说 公司并非吃东西的地方。这是架构级别的分层,如果说到模块级别的分功能模块,基本也是这个套路,就是有相似功能的模块尽量分到一层,或者将一个模块,分层有点像切片,一层一层的,而每一层的分功能又像切蛋糕,一块一块的。

单纯这样说可能大家理解的不透彻,我举个例子好了,我很喜欢举例子

不仅是我,我相信很多人都有这样的体会,我们去做一个简单的HTTP请求,大部分人可能都会用到af库,比如我现在在界面上点了1个按钮,然后我可能在按钮响应的函数里直接去组装各种参数,然后直接去调af的http请求,然后等待数据相应,数据相应回来之后假如说有入数据库的需要,那么我可能直接在回调blocK里面 直接去写数据库。oK现在我们简化一下这个流程

ui -> 业务->网络层->业务->ui->业务 第一个业务我值得是数据封装,比如说有数据加密的需要,第二个业务我指的是解析数据,比如二进制数据解析成JSON,第三个业务我指的是类似写数据库这种操作,到这里我们发现一个问题。就是所有的操作都在同一个按钮响应函数里完成了。似乎没什么鸟问题。但是我们仔细分析一下:

第一个业务需要加密,那么必须要有加密的函数实现或者API,如果势函数实现,那你这里必须要有加密算法,那如果掉很多接口,那是不是很多地方都要写一遍函数加密? 很显然不能。有人说了,我把函数加密方法封装起来,然后直接在UI这个文件里面加入头文件就好了。OK 问题又来了,是不是所有的地方都需要加这个头文件?有人又说了 我可以加载PCH全局头文件,我也要说了,如果加密算法有改动,所有源文件,都需要重新编译一次,如果工程很小无所谓不花时间,如果工程很大。。。。到这里 我们可以说 第一个业务的加密算法即使加载PCH里面也是有缺点的。 在同一个UI层,我们分别调了三次业务以及一次网络 这似乎很为难UI,假如说UI并不是阻塞的,比如随时可以允许用户退出页面而释放,你再看看ui -> 业务->网络层->业务->ui->业务 这个结构,只要UI一断,剩下的不用想了,很可能丢失操作。有人讲我从来没碰到过丢失操作的问题,比如由于页面退出而没写数据库。原因很简单:1、很多http请求是阻塞操作,就是等待相应不允许用户推出,除非有个结果 2、很多封装af库的方法用的是BLOCK回调,BLCOK回调有个特点,在你调用AF请求API的时候,这BLOCK会有系统接管生命周期,在有回调之前这个BLOCK 不会释放,这点可以请有兴趣的同学找个demo试一下,在delloc里面打日志,细节不多说。

其实说到底 产生这样问题的原因就是一个 没有吧业务层单独分出来作为一个层级 假如现在分层成这样 UI<- ->业务<- ->网络 然后我们来假设一下 现在ui发起了一个请求。但是业务层作为一个单例或者全局对象,他的生命周期跟app是一样一样的。那么UI退出了无所谓,数据经由业务层的数据加密,然后推给网络层发送请求,然后有相应之后网络层把二进制数据丢给业务层做解码、然后需要入库的数据在这时写入数据库,等写入数据库完成之后 回调给UI层刷新,这时我们发现,UI层的按钮相应函数里,只有传入原始参数,还有等待数据回来的结果,然后根据结果刷新UI,没有任何加密算法,解密算法,入库代码、出库代码,这时其中一点,另外点就是 根本不需要任何的 数据加密算法的头文件,数据库头文件等等一些跟UI毫无关系的头文件,所有的数据UI 都是从业务层获取的,包括读数据库数据,比如我想读写到数据库的数据,我根本无需知道在哪个数据库,我只需要告诉业务层,我需要读什么什么数据,然后业务层根据你的调取的api来决定读哪个数据库的什么数据

这样的话,整个UI层相当清爽了、业务层相当丰满了、AF层相当寂寞了。。。

再假如,老板要求你该某个接口的业务逻辑,然而你发现你根本就忘记了你是在哪写的整个接口,因为你把业务跟UI混在一起,这时候你就需要先找到对应的UI,然后再去找对应的接口

假如我直接告诉你,在业务层直接去找接口就好了,你觉得哪个更方便?

这只是举的一个非常简单的例子,实际开发中完全不可能都这么简单,但是就是这么个简单的例子都会让你逻辑清晰很多,头文件减少很多,那么如果接口非常非常多。那会很酸爽。

现在常见的很多程序员,包括我自己,早起都有这样的问题,就是把业务层跟UI 还有网络层混在一起,这样做不是不行,而是你维护起来不方便,别人读你代码也很难,别人想读你UI的代码发现一堆业务逻辑,想读你业务逻辑发现分混在各个UI里面
想想都很蛋疼

还拿AF 举例子,现在老板要求你:在你做网络请求的时候不能给我转菊花,我等的太蛋疼。你要让我随时退出。而且我退出之后你还必须得有相应的对策,对于刚才那个接口返回的数据有接收,比如入库,这个要求恐怕很多人都碰到过。咋办?原先入库代码写在UI里头,UI 我都释放了,我怎么写数据库? 那我必须转菊花直到数据返回为止啊!你跟老板说,那不可能!没人这么干,老板说好吧,小伙子,我看好你,敢跟老板较真,加薪2K、.... 假如换成我们改进的方案,那你爱啥时候退出啥时候退出,写库操作不会终止,只要有数据返回改写就写,根本不受你UI影响。你UI的作用仅仅是用来提示一下用户而已,你退出了 大不了我提示不了,亦或者我其他地方提示都行,反正我数据库写成功了,业务层生命周期跟app一样,只要你APP妹退出,我想在哪提示在哪提示 ,想怎么提示怎么提示

综上所述,本身你并未多写一句代码,只是调整了一下结构,整个需求你都满足了。分层设计好处远不止我说的这些,尤其当一个app够大的时候,层分的好不好,决定着你APP 是不是能满足更多更无耻的需求。。。

从另一方面来讲:分层设计更利于多线程操作。

因为层级设计的清晰明了。简单点来说,我每个层都可以用一个队列来维护。ui层我自不必说,都知道肯定是主队列在维护。网络层也不用说,读一读af内部代码就知道怎么回事。来说下业务层,本身来讲,我可以将业务层简单的放到一个队列里面去维护。

我们还拿ui -> 业务->网络层->业务->ui->业务 这个结构举例:假如加密算法非常复杂耗时,有阻塞UI的可能,有人讲 我可以异步到其他线程计算,没问题 这时候有了一个dispath_async 假如业务的解码操作同样很耗,有人讲我同样可以一步到线程去操作,OK 又一个dispatch_async 这段小小的 按钮响应函数,这就有俩异步操作了,假如更细得分可能需要更多的dispatch。。。。直到多到你看到就头大。。

如果我们用改进方案 UI<- ->业务<- ->网络 我按钮相应函数里面什么都无需去做,直接去调用业务层接口,在接口函数里面直接去dipatch_aync 异步到业务层的任务队列里面,首先你会发现 UI层没有了任何的dispath, 然后数据经过业务再异步给af队列,然后数据返回给了af,af再把数据异步给业务层,业务层队列 经过一系列的计算 啊,解码啊,写数据库啊(写数据库是其他队列)最终得到UI需要的结果,然后再把这个结果异步给UI,这时候你发现,我UI层还是没有任何的dispatch!!!!然而最终结果就是你吧事情干完了。该计算的计算了 该入库的入库了,并且任何这些业务相关的数据全部都在业务层的队列进行,丝毫不会影响到UI层的主队列。不知不觉中,我层也分了。多线程也用上了,界面也不卡了,UI代码也清爽了,多余的头文件没了,该一个地方的代码再也不会引起所有地方都重新编译了。四不四很神奇?好处简直不要太多。。。。。

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

推荐阅读更多精彩内容