多线程之NSThread/GCD/NSOperation

  • 概述及基本概念
    1.进程和线程
    2.多线程
    3.任务
    4.队列
    5.iOS中的多线程技术
    6.GCD和NSOperationQueue的对比
    7.使线程同步的方法
  • NSThread
    1.创建和使用
    2.线程安全问题
    3.线程间通信
  • GCD
    1.串行/并行,同步/异步
    2.创建/获取队列
    3.线程间通信
    4.GCD常用函数
  • NSOperation
    1.常规创建
    2.添加操作到队列
    3.自定义NSOperation
  • NSThread+runloop实现常驻线程

概述

1.进程和线程

  • 进程
    一个运行起来的程序就是一个进程,每个进程之间是独立的,拥有运行所需的全部资源
  • 线程
    程序执行流的最小单元,是进程中的一个实体,一个进程至少有一个线程
  • 关系
    进程的任务是在线程中执行的,进程内,线程共享资源

2.多线程

  • 什么是多线程
    同一时间内,cup只能处理一条线程,多线程并发其实就是cpu快速的在线程之间调度/切换,造成了多线程的假象
  • 多线程的优点
    1.提高程序的执行效率
    2.充分利用设备的多核,提高资源利用率(cpu,内存...)
  • 多线程的缺点
    1.开启线程需要占用一定的内存空间(主线程:1M,子线程512KB, iOS下创建线程大约需要90毫秒的时间),大量开启会占用大量内存空间,降低程序的性能
    2.如果线程非常多,cpu在N多线程之间调度,消耗大量cpu资源,会降低线程的执行效率
    3.程序设计更加复杂:线程间通信,多线程数据共享
  • 特点
    1.多线程执行顺序不确定,有name属性方便确定bug点
    2.优先级不能保证完全优先,只是大概率优先,执行完自动销毁,

3.任务

  • 任务
    我们要执行的代码(块)
  • 同步sync
    不具备开启线程的能力,将同步任务添加到指定队列中,任务执行完毕之前会阻塞线程
  • 异步async
    具备开启新线程的能力,无需等待继续执行下方的任务,不会阻塞线程,无法确认任务的执行顺序

4.队列

  • 队列
    存放任务的线性表,FIFO(先进先出)
  • 串行队列
    同一时间内,队列中只能执行一个任务(只开启了一个线程),按照任务添加的顺序,一个任务执行完才能执行下一个任务((仅是队列内串行,可以多个串行队列并行))
  • 并发队列
    同一时间内,允许多个任务并发执行(可以开启多个线程),任务之间不会互相等待,且这些任务的执行顺序和执行过程是不可预测的,只有在异步函数下才有效

5.iOS中的多线程技术

//生命周期,线程任务执行完毕后释放,而不是出了作用域释放

  • pThread(c),使用难度大,需要手动管理线程的生命周期(启动、回收...)
  • NSThread(oc),使用简单且灵活,手动管理(performSelector开辟的子线程也是NSThread的另一种体现方式)
  • GCD(c)充分利用设备的多核,自动管理
  • NSOperation(oc)自动管理,封装了GCD,多了一些实用功能,更面向对象

6.GCD和NSOperationQueue的对比

1.GCD执行的是由block构成的任务,执行效率更高
2.GCD只支持FIFO,而NSOperationQueue可以通过设置并发数、优先级,依赖等调整执行顺序
//所以GCD高效,NSOperationQueue多能,根据需要使用
3.NSOperationQueue可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂

4.NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)

7.使线程同步的方法

     0.加锁 / 阻塞任务
     1.nsopreation的依赖关系 / 设置最大并发数
     2.GCD的 dispath_group   / 信号量机制,栅栏

8.自旋锁与互斥锁

  • 常见的自旋锁:atomic
    A线程在执行任务时,B会不听的在外面徘徊,一旦A执行完毕,B立马执行
  • 常见的互斥锁:@synchronized、NSLock
    A线程在执行任务时,B会进入休眠状态,A执行完毕后,B会自动唤醒去执行
  • 对比
    1.自旋锁的执行效率高于互斥锁
    2.如果无法在短时间内获取锁,自旋锁一直占用cup,会降低cpu的执行效率

这里提一下原子属性:atomic(自旋锁-默认为setter方法加锁)

一样会消耗大量的资源,建议使用noatomic,因为开发中出现资源抢夺的可能性极低,也可以尽量将加锁,资源抢夺的业务交给服务端处理

  • 死锁
    死锁原因: 函数未返回阻塞当前任务 + 队列中任务无法并发执行
    解决死锁:1.用异步函数 2.新建异步队列不和main一个队列
//pragma mark -- 常见的死锁
//死锁原因:viewDidLoad的任务也是在主队列上的,由于队列的先进先出原则
/*viewDidLoad 来了-- dispatch_sync 来了
viewDidLoad想要执行完走人,viewDidLoad 就得执行完毕,
这时候添加了dispatch_sync,根据FIFO原则,viewDidLoad执行完,dispatch_sync才能走,
但是(串行)viewDidLoad 没执行完,就不会执行dispatch_sync,就造成了死锁
*/
//想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"");

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"");
    });
    NSLog(@"");
}

扩展
不管同步还是异步,在同一个队列中添加任务,就是串行,就会造成阻塞

NSThread

1.创建和使用

    //(4个状态:创建,就绪,阻塞,结束)
    //实例方法,创建
    NSThread * thred1 = [[NSThread alloc]initWithTarget:self selector:@selector(hh) object:nil];
    //设置线程的属性要在start之前
    thred1.name = @"thred1";
    //优先级
    thred1.threadPriority = 1;//To be deprecated; use qualityOfService below
    [thred1 start];//运行/就绪切换
    thred1.qualityOfService = 1; //read-only after the thread is started

    [NSThread sleepForTimeInterval:1];//阻塞方式1
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];//阻塞方式2
    [NSThread exit];//手动强制结束,

    //  获取/判断线程
    [NSThread mainThread];
    [NSThread isMainThread];
    [NSThread currentThread];

    //下面2种创建线程的方法简单,但是无法拿到线程对象,进行设置优先级等
    //类方法-分离子线程,不需要start
    [NSThread detachNewThreadSelector:@selector(hh) toTarget:self withObject:nil];

    //隐式创建后台线程(开辟子线程)
    [self performSelectorInBackground:@selector(hh) withObject:nil];

/*
    //当前线程延迟1秒执行(使用带有参数afterDelay的方法,内部会创建一个NSTimer添加到当前线程中,如果在子线程中,需要开启runloop)
    [[NSRunLoop currentRunLoop] run];
    [self performSelector:@selector(showImage:) withObject:nil afterDelay:1.0];
    //在指定线程执行,waitUntilDone :是否等待方法内代码执行完毕后再去执行本行代码后面的代码
    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
    //主线程执行
    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
*/

2.线程安全问题

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //数据安全问题,多线程访问同一资源
    self.count = 10;
    self.threadA = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
    self.threadB = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
    self.threadC = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
    self.threadA.name = @"threadA";
    self.threadB.name = @"threadB";
    self.threadC.name = @"threadC";
    [self.threadA start];
    [self.threadB start];
    [self.threadC start];
    
}

- (void)mai
{
    while (1) {
        
        if (self.count > 0) {
            
            //添加一个耗时操作,让卖票员手速变慢
            for (int i = 0; i < 1000000; i++) {
                int a = 1+i;
            }
            
            self.count--;
            NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
        }else{
            NSLog(@"mei");
            break;
        }
    }
}

输出结果:(这样目测更清晰)
[2427:109348] threadA卖出了一张票,还剩下9张
[2427:109350] threadC卖出了一张票,还剩下8张
[2427:109349] threadB卖出了一张票,还剩下7张
[2427:109348] threadA卖出了一张票,还剩下6张
[2427:109350] threadC卖出了一张票,还剩下5张
[2427:109349] threadB卖出了一张票,还剩下4张
[2427:109350] threadC卖出了一张票,还剩下3张
[2427:109348] threadA卖出了一张票,还剩下2张
[2427:109349] threadB卖出了一张票,还剩下1张
[2427:109350] threadC卖出了一张票,还剩下0张
[2427:109350] mei
[2427:109348] threadA卖出了一张票,还剩下-1张
[2427:109348] mei
[2427:109349] threadB卖出了一张票,还剩下-2张
[2427:109349] Mei
由上可以看出,由于线程默认是异步的,资源竞争时,会发生我们不想看到的问题
那么接下来解决问题,添加互斥锁

- (void)mai
{
    while (1) {
        //互斥锁,实现线程同步,但是会小号大量CPU资源
        @synchronized (self) {
            if (self.count > 0) {
                
                //添加一个耗时操作,让卖票员手速变慢
                for (int i = 0; i < 1000000; i++) {
                    int a = 1+i;
                }
                
                self.count--;
                NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
            }else{
                NSLog(@"mei");
                break;
            }
        }
    } 
}

3.线程间通信

以下载图片为例,贴代码了,不过多解释

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{    //线程间通信
    [NSThread detachNewThreadSelector:@selector(downLoadImage) toTarget:self withObject:nil];
}

- (void)downLoadImage
{
    NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
    
    //计算下载时间,方法随意,这是其一
    NSDate * std = [NSDate date];
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    NSDate * end = [NSDate date];
    NSLog(@"%f",[end timeIntervalSinceDate:std]);//0.404483
    
    UIImage *image = [UIImage imageWithData:imageData];
    
    //一个简单的做法(performSelector方法,可以由任何继承nsobject的对象调用)
    [self.imagev performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
  
}

- (void)showImage:(UIImage *)image
{
      
    self.imagev.image = image;
}

GCD

1.串行/并行,同步/异步

  • 由此,两两组合,有4中情形(过于基础,有兴趣的自己尝试)
    1.异步+并行:多个任务会开启多个线程,队列中的任务的并行的
    2.异步+串行:多个任务只会开启1个线程,队列中的任务的串行的
    3.同步+并行:多个任务不会开启线程,任务是串行的(在主线程)
    4.同步+串行:多个任务不会开启线程,任务是串行的(在主线程)
    注意:
    1.并不是有多少个任务就开启多少个线程,由系统决定
    2.异步+主队列:所有任务都在主队列中执行,不会开启新的线程
    3.同步+主队列:死锁

2.创建/获取队列

GCD有3种队列类型
1.mainqueue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列
2.globalqueue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue
3.自定义队列:通过函数dispatch_queue_create创建的队列

  • 创建队列
//创建并发和串行队列:2个参数,一个标记字符串, 一个串行还是并发的宏
    dispatch_queue_t concurrent_queue = dispatch_queue_create(@"demo_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t serial_queue = dispatch_queue_create(@"demo_serial_queue", DISPATCH_QUEUE_SERIAL);
  • 获取队列(本身存在队列)
//获取主队列(串行) 及 常用的全局队列(并发)
    dispatch_queue_t main_queue = dispatch_get_main_queue();
    ////2个参数, 一个是优先级, 另一个是 : 保留供将来使用的标志。总是为这个参数指定0。
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

3.线程间通信

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:imageData];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imagev.image = image;
        });
    });
}

4.GCD常用函数

  • 1.延迟
    //5. dispatch_after 延时,内部使用的是dispatch_time_t 管理时间,子线程中不用关心runloop是否开启
    dispatch_time_t d = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 3.0));
    dispatch_after(d, dispatch_get_main_queue(), ^{
        NSLog(@"hhh");
    });

相对于 performSelector 和 NSTimer,它的优点是,可以控制在主线程还是子线程执行

  • 2.单例
        static objectA * ob = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ob = [[objectA alloc]init];
        });
        reture ob;
  • 3.栅栏函数
    //栅栏函数:先执行前两个函数,然后执行完栅栏函数才执行后续函数(不能使用全局并发队列)
    //dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
    dispatch_queue_t concurrent_queue = dispatch_queue_create(@"concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrent_queue, ^{
        NSLog(@"0000");
    });
    dispatch_async(concurrent_queue, ^{
        NSLog(@"1111");
    });
    //栅栏函数(遵循同步/异步规则)
    dispatch_barrier_sync(concurrent_queue, ^{
        NSLog(@"++++++++");
    });
    dispatch_async(concurrent_queue, ^{
        NSLog(@"2222");
    });
  • 4.快速迭代
    //快速迭代(类似for循环:主线程操作),会开启子线程完成任务,任务的执行是并发的,效率更高
    //参数:遍历的次数  队列(并发) 索引
    dispatch_apply(100, dispatch_get_global_queue(0, 0), ^(size_t ind) {
        //场景比如:大量文件操作
    });
  • 5.组队列
    //dispatch_group_t 监听任务的执行情况
    //等待一组操作都完成后执行后续操作(如大图分成几块下载,下载后拼接)
    dispatch_queue_t global_queu = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t grou = dispatch_group_create();
    dispatch_group_async(grou, global_queu, ^{/*操作1*/});
    dispatch_group_async(grou, global_queu, ^{/*操作2*/});
    dispatch_group_async(grou, global_queu, ^{/*操作3*/});
    //拦截通知:当队列组中的任务都执行完毕时,会执行这个方法
    dispatch_group_notify(grou, dispatch_get_main_queue(), ^{
        /*后续操作-合并图片0绘图*/
    });
    /*
    dispatch_group_notify(grou, global_queu, ^{
        //后续操作-合并图片-绘图
        dispatch_async(dispatch_get_main_queue(), ^{
            //更新ui
        })
    });
    */
    //阻塞等待组内所有任务执行完毕后,执行之后的代码,效果同上dispatch_group_notify,栅栏函数一样也可以实现
    dispatch_group_wait(grou, DISPATCH_TIME_FOREVER);
  • 6.信号量:Dispatch Semaphore

用途:(有需要自行研究)
1.保持线程同步,将异步执行任务转换为同步执行任务
2.保证线程安全,为线程加锁

NSOperation

1.常规创建

    //NSOperation 是基于GCD的一个抽象(基)类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强,可以加入 操作依赖 控制操作的执行顺序,设置队列最大可并发执行的操作个数,取消操作等,GCD不能中途取消
    //NSBlockOperation 执行代码块
    //NSInvocationOperation 执行指定的方法
    //配置完成后便可以调用start函数在当前线程执行,如果要异步,加入NSOperationQueue中异步执行
    
    //NSInvocationOperation 常规使用
    //创建操作,在主线程中执行,类似于直接调用方法
    NSInvocationOperation * inv = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(hh) object:nil];
    [inv start];

    //NSBlockOperation 常规使用
    //可以后续添加block块,操作启动后会在不同线程并发执行这些执行快
    NSBlockOperation * bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"dd");//任务
    }];
    //追加任务,如果一个操作中的任务数大于1,那么会开子线程并发执行任务
    [bo addExecutionBlock:^{
        NSLog(@"aa");//任务
    }];
    [bo start];

2.添加操作到队列

    //获取主队列
    NSOperationQueue * que = [NSOperationQueue mainQueue];
    //创建非主队列,具备并发+串行,默认是并发队列
    NSOperationQueue * quu = [[NSOperationQueue alloc]init];
    //可以通过设置最大并发数量(同一时间最多有多少任务可以执行)来设置串行,注意,串行!=只开一条线程,只是任务按序执行
  //默认值-1:表示最大值,大于1:并发,等于1:串行,等于0:不执行任务
    quu.maxConcurrentOperationCount = 1;

    //创建abc 3个操作
    NSOperation * a = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"o");
    }];
    NSOperation * b = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"m");
    }];
    NSOperation * c = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"g");
    }];
/*
    //快捷创建操作并添加到队列
    [quu addOperationWithBlock:^{
        NSLog(@"addOperationWithBlock");
    }];
*/
    //添加操作依赖 c依赖于a b,c在a b完成后执行
    [c addDependency:a];
    [c addDependency:b];
    //添加操作到队列
    [que addOperation:c];
    [que addOperation:b];
    [que addOperation:a];

3.自定义NSOperation

1.自定义一个继承于NSOperation的子类,实现main方法

//告诉要执行的任务是什么,自动调用
-(void)main
{
    NSLog(@"开线程了吗 %@",[NSThread currentThread]);
    //[980:25414] 开线程了吗 <NSThread: 0x600003cf7ac0>{number = 7, name = (null)}
    //[980:25411] 开线程了吗 <NSThread: 0x600003cdddc0>{number = 5, name = (null)}

}

2.调用

- (void)yshOperation
{
    //创建操作/封装性,提高复用性,不需要关注内部实现
    YSHOperation * ysho1 = [[YSHOperation alloc]init];
    YSHOperation * ysho2 = [[YSHOperation alloc]init];
    //创建队列
    NSOperationQueue * oq = [[NSOperationQueue alloc]init];
    //添加操作
    [oq addOperation:ysho1];
    [oq addOperation:ysho2];
}

4.常用属性及方法

    que.suspended = YES/NO;//暂停/恢复
    [que cancelAllOperations];
    //值得一提的是
    //自定义NSOperation中,main方法内为一个任务,无法对此方法中的操作进行 暂停和取消
    //内部有多个耗时任务(for循环大量数据)时,如果想要取消,在耗时操作(for循环)后添加,不建议在for循环内判断,耗费性能

    /*
     if (self.isCancelled) {//cancelAllOperations内部会判断这个属性,yes则取消
         return;
     }
     */

NSThread+runloop实现常驻线程

由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程
那么我们可以用GCD实现一个单例来保存NSThread

+ (NSThread *)shareThread
{
    static NSThread * shareThread = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareThread = [[NSThread alloc]initWithTarget:self selector:@selector(shareThread) object:nil];
        [shareThread start];
    });
    return shareThread;
}

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