iOS底层-- 锁

手动目录

  • 什么是锁
  • 锁的工作机制
  • 锁的分类
  • 设计到锁的其他概念
  • 常用锁的用法
    @synchronized()
    NSLock
    信号量 dispatch_semaphore_t
    atomic
    条件锁(NSConditionLock 、NSCondition)
    读写锁 pthread_rwlock
  • 对比理解递归/非递归
  • 更多内容参考文章

不再安全的 OSSpinLock一文中 提到以下问题
1、列举出9种不同的锁(不完全)
2、分析不同锁的执行效率- 测试代码
3、提出OSSpinLock不一定安全:优先级反转------高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock

锁的执行效率

什么是锁

锁是一种同步机制,用于多线程环境中对资源访问的限制,保护数据安全。
锁只是用来保证数据的安全,不保证执行顺序

锁的工作机制

每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待或循环访问,直到锁重新可用。

锁的分类

  • 自旋锁:
    如果共享数据已经有其他线程加锁了,线程会以死循环的方式一直尝试去访问,一旦被访问的资源被解锁(忙等),则等待资源的线程会立即执行。
  • 互斥锁
    分为递归锁/非递归锁
    如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

自旋锁的效率高于互斥锁。但是比互斥锁耗性能

自旋锁:atomic、OSSpinLock、dispatch_semaphore_t 、pthread_rwlock(读写锁)
互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock,os_unfair_lock

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。

注意 @synthesize 和 @ synchronized 的写法

设计到锁的其他概念

  • 临界区
    指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

  • 读写锁:百度百科
    一种特殊的自旋锁,
    它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
    能提高并发性,可以有多个线程来同时读,但是只允许一个线程来写。

  • 条件锁
    互斥锁的一种。
    有一个条件,当进程的某些资源要求不满足这个条件时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

  • 递归锁
    互斥锁的一种
    同一个线程对同一把锁加锁多次,而不会引发阻塞。
    同一个线程必须保证,加锁的次数和解锁的次数相同,其他线程才能够顺利解锁

  • 非递归锁
    互斥锁的一种
    在不解锁的情况下,当同一个线程多次加锁时,会产生阻塞。

常用锁的用法

@synchronized()

递归锁

  • 一般用法
    - (void)task5 {
      dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
      for (NSInteger i = 0 ; i < 1000; i ++) {
          dispatch_async(t, ^{
              @synchronized (self) {
                  NSLog(@"----%@",@(i));          // 打印顺序   不固定
                  [self.array addObject:@(1)];
              }
          });
      }
      NSLog(@"3");              // 打印顺序穿插在  中间
    }
    

关于 @synchronized,这儿比你想知道的还要多这篇文章中讲了具体源码的实现。
同时,你也可以自己通过源码去分析。,通过clang命令将 @synchronized (self) {} 编译成底层源码。可以找到入口函数

{
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            objc_sync_enter(_sync_obj);          // 开始锁
            try {
                struct _SYNC_EXIT {
                    _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {
                        objc_sync_exit(sync_exit);
                    }
                    id sync_exit;
                }
                _sync_exit(_sync_obj);        // 释放锁

            } catch (id e) {_rethrow = e;}
            {
                struct _FIN { _FIN(id reth) : rethrow(reth) {}
                    ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                    id rethrow;
                }_fin_force_rethow(_rethrow);
                
            }
        }

在源码阅读中,有一些点需要注意:
1、 @synchronized (obj) 中的obj 作为锁的标识符,只有标识符相同,才能满足互斥条件
2、如果obj传入的是空(null),那么内部不做任务处理,也就是没有进行加锁解锁操作。

因为是递归锁,我们可以写类似这样的代码:

  - (void)testLock{
  if(_count>0){ 
     @synchronized (obj) {
        _count = _count - 1;
        [self testLock];
      }
    }
}

而如果换成 NSLock ,它就会因为递归发生死锁了。

NSLock

非递归锁
NSLock 属于 pthread_mutex 的一层封装, 设置了属性为nil pthread_mutex_init(mutex,nil) --- 在swift-corefoundation 开源代码里能看到。
也有资料说nil 实际上是PTHREAD_MUTEX_ERRORCHECK

它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

NSLock的API非常简单

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

lock:加锁
unlock:解锁
trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO,相当于判断当前锁的状态
lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回 YES,反之返回 NO
name: 设置锁的名称

NSLock的用法

- (void)task5 {
    dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
    
    NSLock *lock = [NSLock new];
    
    for (NSInteger i = 0 ; i < 1000; i ++) {
        dispatch_async(t, ^{
            [lock lock];
                NSLog(@"----%@",@(i));
                [self.array addObject:@"1"];
            [lock unlock];
        });
    }
    NSLog(@"------------3");
}

信号量 dispatch_semaphore_t

其用法在 上一篇文章 iOS底层--GCD应用 的信号量有讲解,不再赘述。

atomic

atomic 用于声明属性的时候的修饰符。与之对应的是 nonatomic。
atomic相对于 nonatomic 来说,能够保证数据安全,但是执行效率低(因为内部要加锁,大概慢20倍)。一般不实用这个来修饰属性。

关于 atomic 的知识点,可能涉及到2个面试题:
1、atomic的原理
2、atomic一定是安全的吗?

  • atomic的原理

在 objc 源码中 通过 objc_setProperty_atomic 找到 真正的实现源码 reallySetProperty

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];             // copy 修饰 进行copy操作
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];      // mutableCopy 修饰 进行mutableCopy操作
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);                   // 其他 修饰 进行retain操作
    }
    
    if (!atomic) {                                          // 非原子修饰  直接新值替换掉旧值
        oldValue = *slot;
        *slot = newValue;
    } else {                                                // ⚠️ 原子修饰
        spinlock_t& slotlock = PropertyLocks[slot];         // mutex_tt --- os_unfair_lock  锁  
        slotlock.lock();                                    // 加锁
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();                                  // 解锁
    }

    objc_release(oldValue);
}

通过源码一目了然,内部通过 mutex_tt 加锁 在临界区内进行旧置换新值的操作。
get方法同样可以找到

  • atomic 是否安全?
    我们常说的 atomic 能保证数据安全,是给予对数据的读写(set/get)来说的,对于数据的修改,其并不能保证数据安全
    比如说NSString *,我们可以在多线程中进行读写(set/get),是安全的。
    比如 NSMutableArray、NSMutableDictionary,进行读写(set/get)是安全的,但是如果对数据进行修改,就不再是安全的(增、删、改)。
  • 安全的写 --- 没有问题
@property (atomic, strong) NSMutableArray *array;
- (void)task5 {
    dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger i = 0 ; i < 1000; i ++) {
        dispatch_async(t, ^{
            _array = [NSMutableArray new];
        });
    }
}

// 这种写法  如果 @property (nonatomic, strong) NSMutableArray *array; 就是不安全的
  • 不安全的改 -- 数据不安全
@property (atomic, strong) NSMutableArray *array;
_array = [NSMutableArray new];

- (void)task5 {
    dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
//    NSLock *lock = [NSLock new];
    for (NSInteger i = 0 ; i < 1000; i ++) {
        dispatch_async(t, ^{
  //          [lock lock];
            [self.array addObject:@"1"];
   //         [lock unlock];
        });
    }
}

NSRecursiveLock - 递归锁

NSRecursiveLock 也是对 pthread_mutex 的一层封装, 设置了属性为PTHREAD_MUTEX_RECURSIVE
--- 在swift-corefoundation 开源代码里能看到。

pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE)) 

条件锁(NSConditionLock 、NSCondition)

每次提到条件锁,我们一般用生产者和消费者老举例:
在一个面包店里:面包师生产面包,将面包放入货架,消费者从货架上拿走面包,前提是这个货架上有面包才能拿走。在谈 iOS的锁一文中的条件变量中 也进行了类似的解释。

在这篇文章中特别提到:(用循环测试 也会出现这个问题)
实际操作 NSCondition 做 wait 操作时,如果用 if 判断,是不能保证消费者是线程安全的。 --- (文做中搜索 <生产者-消费者问题> 有具体解释)

if(count==0){
    [condition wait];
}

所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:

while (count == 0) {
    [condition wait];
}
--- NSCondition

非递归锁
NSCondition 属于 pthread_mutex 的一层封装, 设置了属性为nil --- 在swift-corefoundation 开源代码里能看到。

pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)

看到这里是不是觉得很熟悉? 和 NSLock 一样。不过是在此基础上 加上了一个条件标示。

NSCondition 遵循NSLocking协议,具体API

@interface NSCondition : NSObject <NSLocking> {
- (void)wait;                              //进程进入等待状态
- (BOOL)waitUntilDate:(NSDate *)limit;    //线程等待一定的时间
- (void)signal;                            //唤醒一个等待的线程
- (void)broadcast;                        //唤醒所有等待的线程

@property (nullable, copy) NSString *name;

先用代码建立生产/消费 面包的场景

@property (nonatomic, assign) NSInteger number;    // 面包数量

self.number = 0;      // 初始化面包数量为0

//   开始上班工作
- (void)task6 {
    for (NSInteger i = 0; i < 1000; i ++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self takeAwayOne];          // 消费者来买面包
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self productOne];           // 生产一个面包
        });
    }
}

- (void)takeAwayOne {        // 拿走一个面包
    self.number --;
    NSLog(@"拿走一个,剩余%ld",self.number);
}

- (void)productOne {        // 生产一个面包
    self.number ++ ;
    NSLog(@"------生产一个,剩余%@",@(self.number));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self task6];
}

多执行几次touchues ,发现有时候是先拿走,再生产,这样的结果显然不是我们理想的结果。

为了解决这个问题,我们需要用锁来处理。
生产/消费 都加锁,在因为消费的前提是有生产,所以在消费里面 wait、在生产里面发信号 signal

@property (nonatomic, strong) NSCondition *condition;  //条件锁

_condition = [[NSCondition alloc]init];      // 初始化锁


- (void)takeAwayOne {
    [_condition lock];
    
    while (self.number == 0) {
        [_condition wait];
    }
    self.number --;
    NSLog(@"拿走一个,剩余%ld",self.number);
    [_condition unlock];
    
}

- (void)productOne {
    [_condition lock];
    self.number ++ ;
    NSLog(@"------生产一个,剩余%@",@(self.number));
    [_condition unlock];
    [_condition signal];
}

这样就能满足我们实际的场景。
在生产者中, 是先 unlock 还是先signal? 我认为是都可以的。
至于 broadcast 在什么情况下用? 从上面的例子看,如果生产者一次生产2个面包,但是消费者只买一个,那么我如果用signal ,就是生产2个,只让一个消费者来买走一个,剩下的一个 放在那里,其他想买的人都以为没有面包了,就一直等,是影响实际生产的。

--- NSConditionLock

NSConditionLock 其实是对NSCondition的封装 在此基础上添加一个 value(NSInteger)的一个条件
-- 在swift-corefoundation 开源中能找到这样一段:


open class NSConditionLock : NSObject, NSLocking {
    internal var _cond = NSCondition()          // 内用用的就是 NSCondition
    internal var _value: Int                    // 在NSCondition的基础上 加了 value的条件

    ....
        _cond.broadcast()          // 内部的发送信号 用的是广播,而不是 signal 。  一般来说 broadcast 比signal的效率低。
        _cond.unlock()
    .....
}

看其API

@protocol NSLocking
- (void)lock;                // 不管什么条件  直接锁
- (void)unlock;              // 不管什么条件  直接解锁
@end

- (instancetype)initWithCondition:(NSInteger)condition;      // 初始化新分配的NSConditionLock对象并设置其条件
@property (readonly) NSInteger condition;                            // 条件 (只读)
- (void)lockWhenCondition:(NSInteger)condition;               // 根据条件加锁
- (BOOL)tryLock;                                                    // 能否加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;    // 如果接收器的条件等于指定的条件,则尝试获取锁。
- (void)unlockWithCondition:(NSInteger)condition;           // 放弃锁定并设置接收器的条件
- (BOOL)lockBeforeDate:(NSDate *)limit;                            // 尝试在指定时间之前获取锁。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;     // 尝试在指定时间之前获取锁。(condition 是匹配条件)

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

特别注意的是:
unlockWithCondition:(NSInteger)condition 是放弃锁并重新设置条件。
用为代码来理解流程

[_conditionLock lockWhenCondition:0];
类似于:
if (value == 0) {
 [ _conditionLock lock];          // 符合条件 就进行加锁
...... 执行操作
}

[_conditionLock unlockWithCondition:0];
类似于:
 [ _conditionLock unlock];      // 解锁
value = 0;          // 重设条件
[_conditionLock  broadcast];      // 发送广播 (告诉其他的线程任务,  服务value = 0 的可以执行了)
}

类似于银行取号办业务: 有20个人,每个人的号分别是 1---20;
_conditionLock = [[NSConditionLock alloc]initWithCondition:1]; // 开始办业务了, 当前叫的号是1号

[_conditionLock lockWhenCondition:5]; // 我持有5号,当前不是我的号 我要等着(直到有人通知 5号可以办业务 我才可以办)
.......... 5号要办的业务
[_conditionLock unlockWithCondition:7]; // 5号业务办完了, 我要通知7号办业务

[_conditionLock lockWhenCondition:1]; // 我持有1号
.......... 1号要办的业务
[_conditionLock unlockWithCondition:2]; // 我的业务办完 了 通知2号来办业务。

读写锁 pthread_rwlock

读写锁的效果是:可以很对线程读,但是只能一个线程来写。
那么原理就应该是这样的:异步读数据,栅栏写数据

self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);       // 自定义一个并发队列

- (id)readData:(id)key {
    __block id obj;
    dispatch_async(self.concurrent_queue, ^{
        /// 读取数据
    });
    return obj;
}

- (void)setDataObject:(id)obj forKey:(NSString *)key{
    dispatch_barrier_async(self.concurrent_queue, ^{
        // 设置数据
    });
}

对比理解递归/非递归

递归 ----- 同一个线程对同一把锁加锁多次,而不会引发阻塞。
非递归 -- 同一个线程多次加锁时,会产生阻塞。

NSLock -----非递归
NSRecursiveLock -----递归
@synchronized() -----递归

用这段代码来测试:

  - (void)task5 {
      NSLock *lock = [[NSLock alloc] init];
      
      dispatch_async(dispatch_get_global_queue(0, 0), ^{
          // 定义一个block 任务
          static void (^testMethod)(int);
          testMethod = ^(int value){
              [lock lock];
              if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
              }
              [lock unlock];
          };
          
          testMethod(10);             // 在同一个线程进行多次加锁/解锁
      });
  }

// 打印结果  
11:33:00.347074+0800 [7720:95610] current value = 10
只有这一条打印

NSLock 是非递归锁,在同一个线程下多次加锁解锁,就会出现阻塞现象
因为在 第一次执行testMethod(value - 1); 的时候 ,已经加了锁,再次执行这个代码的时候,还没有解锁,又进行了一次加锁,在没有解锁的情况下线程要进行休眠等待。 这样的话,这个线程就一直休眠,直到天荒地老。

为了解决这个问题,可以用递归锁。

方案一 :NSLock 换成 NSRecursiveLock
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

方案二: 用 @synchronized
dispatch_async(dispatch_get_global_queue(0, 0), ^{

static void (^testMethod)(int);
testMethod = ^(int value){
    @synchronized (self) {
        if (value > 0) {
            NSLog(@"current value = %d",value);
            testMethod(value - 1);
        }
    }
};

    testMethod(10);             // 在同一个线程进行多次加锁/解锁
});

NSRecursiveLock 与 @synchronized 都是递归锁,所以在同一个线程、未解锁的情况下,可以多次加锁而不会产生阻塞,可以正常执行。

额外注意:

上面的测试代码中,是在同一线程下进行多次加锁。
如果在不同线程下,进行多次加锁会怎么样?

- (void)task5 {
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
    for (NSInteger i = 0; i < 2000; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            // 定义一个block 任务
            static void (^testMethod)(int);
            testMethod = ^(int value){
                if (value == 10) {
                    NSLog(@"%@",[NSThread currentThread]);
                }
                [lock lock];
                if (value > 0) {
                    testMethod(value - 1);
                }
                [lock unlock];
            };
            
//            调用任务
            testMethod(10);
        });
    }
}

这样就会产生死锁
在不同的线程,同一个锁进行多次加锁,在某一个时刻,就可能产生 上一个锁还没解锁,下一个锁又来加锁。

线程1: lock 等 线程2 的unlock
线程2: lock 等 线程1的 unlock
形成了相互等待,产生死锁。

  • 如何解决上面的问题?

    为了解决循环调用递归锁产生死锁,可以使用@ synchronized 来解决。
    (代码就不上了)

为什么同样是递归锁,在不同线程循环调用的时候 NSRecursiveLock 不行,而@synchronized 可以?
源码分析@synchronized 之后会发现 传入的参数(obj)的作用:
当第一次进行synch加锁的时候,将信息存入syncdata里面,放入syncList 表里
当第二次在进行加锁的时候,根据obj 来找,发现已经有锁了,就不进行lock 。直接用。

总结:
一般任务的锁:使用NSLock。
递归调用的锁:使用NSRecursiveLock
多线程 递归调用: 注意死锁的发生 。

更多内容参考文章:

谈 iOS 的锁🔥
深入理解 iOS 开发中的锁
iOS 开发中的八种锁(Lock)
iOS开发中的11种锁以及性能对比 🔥

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