iOS 多线程执行完网络任务ABC再执行D

前言
我发现网络上很多介绍多线程的案例感觉都是错误的例子。也不能说错的,例如用异步并发队列的时候,他们只是打印了一个log,这种打印的行为本身就是同步任务,肯定按照最简单的例子进行打印了,看到的效果自然是很多文章所说的。但是如果你Block里面的任务是网络请求呢?还能保证网络异步任务的一致性?很显然大部分文章压根没有考虑任务的异步性,而且显示开发中大部分任务都是异步的,因此本文先从网上的同步任务,也就是比较简单的案例开始介绍,最后再通过一道阿里的面试题来引出实际开发中的问题,这个题目非常有代表性。

线程同步
同步异步并发串行什么的可以看看这个简单的介绍

阿里有个面试题

使用GCD如何实现A,B,C三个任务并发,完成后执行任务D?

// 异步 并发队列 当前线程不等待,而且任务是并发队列,一次可以执行多个
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 第一种 同步任务
NSLog(@"同步任务打印1");
// 第二种 网络请求发送任务 (发送这个操作是任务,而网络返回的报文是不归任务管理的,因此发送任务发送之后,任务已经结束)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"异步任务返回报文打印1");
});
});
可以看到异步并发任务有两种,一种任务是同步的,一种任务是网络请求,发送操作是同步的,但是请求到的结果是异步的。阿里的面试题,肯定是后者异步并发队列里面的任务也是并发的操作,如何实现ABC三个网络请求都回来之后->执行D这样的操作?

异步并发(同步任务)

同步任务很简单,也是所有你能搜到的资料告诉你如何做到的几个方法。

第一种GCD group

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务A");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务B");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务C");
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"任务完成执行");
});

// 2018-04-18 10:18:52.950271+0800 GCD[2283:78081] 同步任务B
// 2018-04-18 10:18:52.950271+0800 GCD[2283:78082] 同步任务C
// 2018-04-18 10:18:52.950273+0800 GCD[2283:78083] 同步任务A
// 2018-04-18 10:18:52.950424+0800 GCD[2283:78082] 任务完成执行

// 2018-04-18 10:19:30.821003+0800 GCD[2315:79354] 同步任务B
// 2018-04-18 10:19:30.821003+0800 GCD[2315:79355] 同步任务A
// 2018-04-18 10:19:30.821003+0800 GCD[2315:79370] 同步任务C
// 2018-04-18 10:19:30.821145+0800 GCD[2315:79355] 任务完成执行
可以看到,异步线程,并发队列里面,三个任务可以同时执行,因此打印顺序随机。但是由于打印的任务,这个打印的动作是同步的,没有再开线程有其他进一步的异步操作,所以你看起来好像没什么问题。

第二种 dispatch_barrier_async

dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"任务A");
});

dispatch_async(queue, ^{
    NSLog(@"任务B");
});

dispatch_async(queue, ^{
    NSLog(@"任务C");
});

dispatch_barrier_async(queue, ^{
    NSLog(@"阻塞自定义并发队列");
});

dispatch_async(queue, ^{
    NSLog(@"任务D");
});

dispatch_async(queue, ^{
    NSLog(@"任务E");
});

注意,这里用到的dispatch_barrier_async如果使用的队列是dispatch_global_queue,那么就等同意dispatch_async,起不到阻塞的作用。我们需要自己创建并发队列,然后再执行barrier函数,前面ABC三个任务随机,后面DE随机,但是DE的执行必须是等待ABC任务执行完的。

第三种 NSOperation

NSBlockOperation *operatioon1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务A");
}];

NSBlockOperation *operatioon2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务B");
}];

NSBlockOperation *operatioon3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务C");
}];

NSBlockOperation *operatioon4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任务D");
}];

[operatioon4 addDependency:operatioon1];
[operatioon4 addDependency:operatioon2];
[operatioon4 addDependency:operatioon3];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operatioon1,operatioon2,operatioon3,operatioon4] waitUntilFinished:YES];
NSLog(@"完成之后的操作");

通过任务之间的依赖关系执行,最后的参数YES的时候是会阻塞当前线程,执行完之后再往后执行,NO的话就不阻塞

可以看到,上面的都是异步并发操作,而里面的任务是同步的,这里的任务指的是Block里面的所有操作,但是如果Block里面的操作是网络请求,也是异步的,那上面的做法就会有问题了,看如下代码

错误例子

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务A");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络异步任务AA");
    });
});

dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务B");
});

dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务C");
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"任务完成执行");
});

// 2018-04-18 10:53:16.294263+0800 GCD[3287:115681] 同步任务B
// 2018-04-18 10:53:16.294263+0800 GCD[3287:115679] 同步任务C
// 2018-04-18 10:53:16.294263+0800 GCD[3287:115699] 同步任务A
// 2018-04-18 10:53:16.294430+0800 GCD[3287:115699] 任务完成执行
// 2018-04-18 10:53:18.294592+0800 GCD[3287:115646] 网络异步任务AA
用dispatch_after来模拟网络请求,可以看到,同步任务ABC->D这些操作还是正确的,但是里面有个模拟网络请求的任务,就不会按我们所想的顺序执行了。为什么呢?很简单,首先都是异步的,子线程操作,而且是并发队列,那么任务可以多个一起,如果任务是单纯的打印,即同步任务,那么就能完成我们的预期,如果Block任务里面还嵌套异步任务,因为并发队列里面的任务,只是负责打印和发送请求的操作,异步回调数据是不归队列管的,任务的执行完毕,只是Block代码块代码执行完,如果里面还包含异步任务,这里就需要通过信号量dispatch_semaphore来实现了。

下面就来实现如果并发队列里面的任务是网络请求,如何等ABC三个网络请求回调之后,再执行D?

先来介绍下Dispatch_semaphore的使用

先来介绍下Dispatch_semaphore的使用

dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是

dispatch_semaphore_create,
dispatch_semaphore_signal,
dispatch_semaphore_wait。
下面我们逐一介绍三个函数:

dispatch_semaphore_create

dispatch_semaphore_t dispatch_semaphore_create(long value);

传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

关于信号量,我就不在这里累述了,网上很多介绍这个的。我们这里主要讲一下dispatch_semaphore这三个函数的用法)。
dispatch_semaphore_signal的声明为:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

这个函数会使传入的信号量dsema的值加1;(至于返回值,待会儿再讲)
dispatch_semaphore_wait的声明为:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

这个函数会使传入的信号量dsema的值减1;

这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;

如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,

不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,

且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。

如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
dispatch_semaphore_signal的返回值为long类型

当返回值为0时表示当前并没有线程等待其处理的信号量,其处理

的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。
当其返回不为0时,表示timeout发生。

在设置timeout时,比较有用的两个宏:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER。

DISPATCH_TIME_NOW  表示当前;

DISPATCH_TIME_FOREVER  表示遥远的未来;

一般可以直接设置timeout为这两个宏其中的一个,或者自己创建一个dispatch_time_t类型的变量。

创建dispatch_time_t类型的变量有两种方法,dispatch_time和dispatch_walltime。

利用创建dispatch_time创建dispatch_time_t类型变量的时候一般也会用到这两个变量。

dispatch_time的声明如下:

dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
其参数when需传入一个dispatch_time_t类型的变量,和一个delta值。表示when加delta时间就是timeout的时间。
例如:dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 110001000*1000);

表示当前时间向后延时一秒为timeout的时间。
关于信号量,一般可以用停车来比喻。

停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。

信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal

就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),

调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;

当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主

没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,

所以就一直等下去。
参考文章 这里面有个Demo,理解不了的可以看一下,信号量1的时候一般都当做锁来看待

异步并发(模拟网络异步任务)
子线程同时执行ABC三个同步任务、全部执行完成再在子线程执行三个同步任务D。

说队列组/依赖基本可以确定了解GCD/NSOpertion。但是比较麻烦、用线程栅栏dispatch_barrier的话会更简便一些

这个就是上面的同步介绍面试题

上一题中的ABC三个任务改成异步任务(如AFN网络请求)、全部回调成功后进行数据整合。
如果只说队列/任务组肯定不行。因为网络请求本身是异步的、任务会立即完成、但数据还没有回来。
最好的就是在队列组的前提下。把异步的网络请求转化为同步、以捕获正确的完成时机。
具体操作需要使用信号量。

这里有个帖子有介绍这个,但是没有具体Demo,下面看看如何组合信号量实现的

第一种 dispatch_group + semaphore

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_group_async(group, queue, ^{
NSLog(@"同步任务A");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"网络异步任务一");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group, queue, ^{
    
    NSLog(@"同步任务B");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络异步任务二");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务C");
});

dispatch_group_async(group, queue, ^{
    
    NSLog(@"同步任务D");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络异步任务四");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"任务完成执行");
});

分析如下:

异步并发队列,里面添加了四个任务,任务AB有网络请求,C同步任务打印,D也有网络请求,如何在ABCD四个任务完成,而且网络请求也完成之后执行之后的操作?

可以看到,Block整个就是一个任务,如果没有dispatch_semaphore_wait,带有网络请求的任务,因为网络请求本身是异步的、任务会立即完成、但数据还没有回来。,因为发送网络请求,就已经把Block的任务完成了,异步回来的操作已经不属于并发队列里面的管理的任务了。就和上面的错误例子一样,完成任务的执行,和请求回调没有任何顺序关系了。那么如果我们在Block里面加了dispatch_semaphore_wait,什么意思呢?如果信号量为0的时候,那么就会一直在这里等待。可以理解为一开始发送网络请求出去,这个时候执行到wait函数,信号量为0,等待,队列任务没有执行完,只有当请求回来的时候调用singnal的时候,信号+1,wait的函数随机获取到信号,放开任务,执行完毕一个,剩下的没有获取到的信号继续等待,那么就会按我们的,执行完一个网络请求,信号+1,释放一个wait,执行完一个Block任务,那么,当所有的网络请求执行完,所有的wait都被释放,任务都完成了,才会通知Group调用完成。

第二种 dispatch_group_enter 和 dispatch_group_leave

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"同步任务A");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"网络异步任务一");
dispatch_group_leave(group);
});
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
    
    NSLog(@"同步任务B");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络异步任务二");
        dispatch_group_leave(group);
    });
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
    NSLog(@"同步任务C");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
    
    NSLog(@"同步任务D");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络异步任务四");
        dispatch_group_leave(group);
    });
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"任务完成执行");
});

这种方法也是我之前项目中用的,也能实现上面的执行顺序。

ABCD异步顺序随机,无论任务是否有异步,都会等任务执行完(包括网络请求)再执行任务完成

异步串行(同步任务)
这个没什么好说的,同步任务,在串行队列,肯定顺序执行

异步串行(异步任务)
但是如果串行队列里面的任务是网络请求如何,再让等网络请求回调之后也顺序执行?

看代码 不做处理

dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务一");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"网络任务一");
});
});

dispatch_async(queue, ^{
    NSLog(@"执行任务二");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络任务二");
    });
});

dispatch_async(queue, ^{
    NSLog(@"执行任务三");
});

dispatch_async(queue, ^{
    NSLog(@"执行完成");
});
2018-04-18 12:11:04.956900+0800 GCD[4889:182340] 执行任务一
2018-04-18 12:11:04.957076+0800 GCD[4889:182340] 执行任务二
2018-04-18 12:11:04.957183+0800 GCD[4889:182340] 执行任务三
2018-04-18 12:11:04.957290+0800 GCD[4889:182340] 执行完成
2018-04-18 12:11:05.557397+0800 GCD[4889:182291] 网络任务二
2018-04-18 12:11:07.156658+0800 GCD[4889:182291] 网络任务一

可以看到串行队列任务执行是顺序的,但是异步网络请求回调是不按顺序的。下面我们用dispatch_semaphore处理下

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务一");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"网络任务一");
dispatch_semaphore_signal(semaphore);
});
});

dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务二");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"网络任务二");
        dispatch_semaphore_signal(semaphore);
    });
});

dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务三");
    dispatch_semaphore_signal(semaphore);
});

dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行完成");
    dispatch_semaphore_signal(semaphore);
});

2018-04-18 13:54:20.412918+0800 GCD[6841:262952] 执行任务一
2018-04-18 13:54:22.413131+0800 GCD[6841:262916] 网络任务一
2018-04-18 13:54:22.413369+0800 GCD[6841:262952] 执行任务二
2018-04-18 13:54:23.073214+0800 GCD[6841:262916] 网络任务二
2018-04-18 13:54:23.073512+0800 GCD[6841:262952] 执行任务三
2018-04-18 13:54:23.073697+0800 GCD[6841:262952] 执行完成

当异步串行队列,而且任务有网络异步请求,我们需要用信号量,每次开放一个任务,当一个任务无论是同步还是异步,都是操作完之后再把信号量+1,然后下一个任务才会解锁。这里的信号量可以当做锁来理解。

总结

异步并发执行同步任务,可以用dispatch_group,dispatch_barrier和NSOperation的依赖

异步串行执行同步任务,默认就是顺序执行

异步并发执行异步任务,可以用dispatch_group+semaphore 初始化信号为0,执行完一个异步通过singnal释放一个wait任务

或者用disaptch_group_enter 和 dispatch_group_leave

异步串行执行异步任务,可以用dispatch_semaphore 初始化信号为1,顺序执行任务,每个任务都加锁,执行任务消耗锁,顺延任务等待,执行完一个任务,singnal,然后下一个任务开锁顺序执行

如果多个任务网络请求,NSOperation我感觉无法实现 多网络任务执行完之后再最终统一执行某一个人任务。这个时候只有用dispatch_group + semaphore来实现

多线程面试题

一个简书的好文章

dispatch_semaphore

NSOperation
————————————————

原文链接:https://blog.csdn.net/Deft_MKJing/article/details/51518556

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

推荐阅读更多精彩内容