iOS-多线程3-加锁方案1

上篇文章只讲了OSSpinLock,本文继续讲iOS中其他加锁方案。

二. os_unfair_lock

  1. os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁
  2. 需要导入头文件#import <os/lock.h>
#import "OSUnfairLockDemo.h"
#import <os/lock.h>

@interface OSUnfairLockDemo()
@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation OSUnfairLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_UNFAIR_LOCK_INIT;
        self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

// 死锁:永远拿不到锁
- (void)__saleTicket
{
    os_unfair_lock_lock(&_ticketLock);
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

- (void)__saveMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __saveMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__drawMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __drawMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

@end

os_unfair_lock可以进行尝试加锁:os_unfair_lock_trylock(os_unfair_lock_t lock);

等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,所以如果忘记解锁了,那么当下一个线程进来的时候发现没有解锁,就会休眠,当所有的线程都进来 ,就会导致所有的线程都会休眠,这种情况称为死锁,就是永远拿不到锁。

三. pthread_mutex

  1. mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
  2. pthread_mutex是跨平台的,一般pthread开头的都是跨平台的,iOS、Linux、Windows等都能使用
  3. 需要导入头文件#import <pthread.h>
#import "MutexDemo.h"
#import <pthread.h>

@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t ticketMutex;
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
@end

@implementation MutexDemo

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化锁,传空就相当于PTHREAD_MUTEX_DEFAULT
    //这一行代码就相当于上面的代码
    //pthread_mutex_init(mutex, NULL);
}

- (instancetype)init
{
    if (self = [super init]) {
        [self __initMutex:&_ticketMutex];
        [self __initMutex:&_moneyMutex];
    }
    return self;
}

- (void)__saleTicket
{
    //尝试加锁
    //int pthread_mutex_trylock(pthread_mutex_t *);

    pthread_mutex_lock(&_ticketMutex);
    
    [super __saleTicket];
    
    pthread_mutex_unlock(&_ticketMutex);
}

- (void)__saveMoney
{
    pthread_mutex_lock(&_moneyMutex);
    
    [super __saveMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)__drawMoney
{
    pthread_mutex_lock(&_moneyMutex);
    
    [super __drawMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)dealloc
{
    //销毁锁
    pthread_mutex_destroy(&_moneyMutex);
    pthread_mutex_destroy(&_ticketMutex);
}

@end

pthread_mutexattr_settype有三种类型,一般我们只用默认的锁、递归锁,如下:

#define PTHREAD_MUTEX_NORMAL        0 默认的锁
#define PTHREAD_MUTEX_ERRORCHECK    1 检查错误的锁
#define PTHREAD_MUTEX_RECURSIVE     2 递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL 默认的锁
  1. 代码中,如果传NULL,就代表是默认锁。
  2. 也可以进行尝试加锁:pthread_mutex_trylock(pthread_mutex_t *);
  3. 在dealloc中销毁锁。

下面介绍递归锁的作用:

如下代码:

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);

    NSLog(@"%s", __func__);

    [self otherTest2];

    pthread_mutex_unlock(&_mutex);
}

- (void)otherTest2
{
    pthread_mutex_lock(&_mutex);

    NSLog(@"%s", __func__);

    pthread_mutex_unlock(&_mutex);
}

上面代码otherTest和otherTest2中使用了同一把锁。当一个线程进入otherTest,发现没加锁,然后它加上了一把锁。当再进入otherTest2的时候,发现这把锁锁上了,就休眠等待解锁,但是想要解锁就要otherTest执行完,想要执行完otherTest就要执行otherTest2,这样就循环等待造成了死锁。

由于上面是两个不同的方法,解决办法也很简单,每个方法各自使用一把锁就可以解决死锁。

但是如果是如下代码呢?

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s", __func__);
    
    //递归调用10次
    static int count = 0;
    if (count < 10) {
        count++;
        [self otherTest];
    }
    
    pthread_mutex_unlock(&_mutex);
}

otherTest方法递归调用10次,这里不能使用两把锁了,这里只能使用递归锁,解决死锁。

递归锁:允许同一个线程对一把锁进行重复加锁(解锁)

代码如下:

#import "MutexDemo2.h"
#import <pthread.h>

@interface MutexDemo2()
@property (assign, nonatomic) pthread_mutex_t mutex;
@end

@implementation MutexDemo2

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    // 传入PTHREAD_MUTEX_RECURSIVE,就是递归锁
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
}

- (instancetype)init
{
    if (self = [super init]) {
        [self __initMutex:&_mutex];
    }
    return self;
}

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s", __func__);
    
    //递归调用10次
    static int count = 0;
    if (count < 10) {
        count++;
        [self otherTest];
    }
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
}

@end

总结:

  1. 递归调用产生死锁,用递归锁解决
  2. 递归锁:允许同一个线程对一把锁进行重复加锁(解锁)

可能你会想,允许对一把锁进行重复加锁不就不安全了吗?其实因为是同一个线程访问,其他线程在等待,所以是安全的。

比如:线程1递归调用otherTest三次。调用第一次,加锁1,调用第二次加锁2,调用第三次加锁3。然后解锁第一次,解锁第二次,解锁第三次。只有当otherTest全部解锁之后线程2才可以访问,如果没有全部解锁那么线程2会一直在休眠等待。

线程1:
otherTest(+-)
   otherTest(+-)
      otherTest(+-)

线程2:
otherTest(等待)

四. 自旋锁、互斥锁汇编代码分析

1. 自旋锁汇编代码分析

修改代码:
先创建10条线程来卖票

/**
 卖票演示
 */
- (void)ticketTest
{
    self.ticketsCount = 15;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
    }
.......
}

将卖票等待的时间变600s:

/**
 卖1张票
 */
- (void)__saleTicket
{
    int oldTicketsCount = self.ticketsCount;
    sleep(.600);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}

打断点:

- (void)__saleTicket
{
    OSSpinLockLock(&_ticketLock); //在这里打断点
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&_ticketLock);
}

运行:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJBaseDemo *demo = [[OSSpinLockDemo alloc] init];
    [demo ticketTest];
//    [demo moneyTest];
//    [demo otherTest];
}

如下图:

线程7到线程10.png

可以发现,首先是线程7进入__saleTicket,这时候没有加锁,线程7加锁之后,线程10再进入__saleTicket,这时候已经加锁,线程10处于忙等状态,我们就在这里停下,通过汇编观察忙等状态都发生了什么。

选择Always show disassembly,永远显示汇编。

指令:
step instruction 一行一行汇编代码执行,如果有函数,会进入函数一行一行执行 (简称si)
step 一行一行OC代码执行(会一次过好多汇编代码)
next instruction 一行一行汇编代码执行,如果有函数,会一次性调用完函数,然后往下走。

输入指令si,一直敲回车。
(lldb) si
(lldb) si
......
(lldb) si
(lldb)

来到如下代码:

    0x10d9208d1 <+12>: cmpl   $-0x1, %eax
    0x10d9208d4 <+15>: jne    0x10d9208f1               ; <+44>
    0x10d9208d6 <+17>: testl  %ecx, %ecx
    0x10d9208d8 <+19>: je     0x10d9220f9               ; _OSSpinLockLockYield
    0x10d9208de <+25>: pause  
    0x10d9208e0 <+27>: incl   %ecx
    0x10d9208e2 <+29>: movl   (%rdi), %eax
    0x10d9208e4 <+31>: testl  %eax, %eax
->  0x10d9208e6 <+33>: jne    0x10d9208d1               ; <+12>

第一行的地址是0x10d9208d1,最后一行也会跳到0x10d9208d1,可以看出这是个while循环。当判断到锁一直在加锁状态,就会走这一段while循环,线程处于忙等状态,直至解锁。

总结:

验证了OSSpinLock是自旋锁,这也验证了自旋锁的本质是while循环

2. 互斥锁汇编代码分析

打断点:

- (void)__saleTicket
{
    os_unfair_lock_lock(&_ticketLock); //这里打断点
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

运行:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJBaseDemo *demo = [[OSUnfairLockDemoalloc] init];
    [demo ticketTest];
//    [demo moneyTest];
//    [demo otherTest];
}

同理,线程8会先进入__saleTicket,这时候没有加锁,线程8加锁之后,线程9再进入__saleTicket,这时候已经加锁,线程9处于休眠状态,我们就在这里停下,通过汇编观察休眠状态都发生了什么。

同样,敲si指令:
在如下函数打断点,c 继续来到下面函数:(c是continue的简称)

0x101a8feb1 <+33>:  callq  0x101a921ee               ; symbol stub for: os_unfair_lock_lock

si进去:

->  0x101a921ee <+0>: jmpq   *0x1ebc(%rip)             ; (void *)0x0000000104ae146e: os_unfair_lock_lock

发现真调用了这个函数。

在如下函数打断点,c 继续来到下面函数:

0x104ae1481 <+19>: jmp    0x104ae1bcb               ; _os_unfair_lock_lock_slow

si进去,找到如下函数,打断点:

0x104ae1c4f <+132>: callq  0x104ae65a6               ; symbol stub for: __ulock_wait

c继续来到这个函数,si进去,再si,最后会来到:

0x104a9f9dc <+8>:  syscall 

可以发现,最后来到了syscall系统调用,syscall调用之后模拟器就自动弹出了,说明线程不做事情了,线程休眠去了。

总结:

验证了os_unfair_lock是互斥锁,互斥锁是通过线程休眠实现的。同理也可以验证pthread_mutex是互斥锁,验证方式和上面类似,这里就省略了。

补充:

High-Level Lock:高级锁
特点:会一直等待,不会休眠。
比如:OSSpinLock

Low-Level Lock:低级锁
特点:等不到锁就休眠。
比如:os_unfair_lock、pthread_mutex

五. pthread_mutex – 条件锁的用法

上面说了,如果pthread_mutexattr_settype类型传PTHREAD_MUTEX_DEFAULT或者NULL就是默认锁,如果传PTHREAD_MUTEX_RECURSIVE就是递归锁。

下面讲一下pthread_mutex – 条件锁

上面我们讲的解锁都是通过unlock解锁,如果使用了条件就不一定通过unlock也能解锁。

假设需求是,我们有两个线程,一条线程是删除东西,一条线程是添加东西,删东西和添加东西都要加锁,删除东西时候有个条件,就是必须要有东西才进行删除,那么如何实现?

#import "MutexDemo3.h"
#import <pthread.h>

@interface MutexDemo3()
@property (assign, nonatomic) pthread_mutex_t mutex; //锁
@property (assign, nonatomic) pthread_cond_t cond; //条件
@property (strong, nonatomic) NSMutableArray *data; //数组
@end

@implementation MutexDemo3

- (instancetype)init
{
    if (self = [super init]) {
        // 初始化属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        //GUNstep源码里面就是传入PTHREAD_MUTEX_ERRORCHECK,我也模仿着做了
        pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_ERRORCHECK);
        // 初始化锁
        pthread_mutex_init(&_mutex, &attr);
        // 销毁属性
        pthread_mutexattr_destroy(&attr);
        
        // 初始化条件
        pthread_cond_init(&_cond, NULL);
        
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    //一个线程删除东西
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    //一个线程添加东西
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 线程1
// 删除数组中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    //如果数组为0,就先等等,等有东西了再删除
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 线程2
// 往数组中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信号 唤醒上面那个线程
    pthread_cond_signal(&_cond);
    
    // 广播 唤醒所有等待这个条件的线程
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
    //销毁锁,销毁条件
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}
@end

注意:等待和唤醒必须是同一个条件

上面的代码:

  1. 如果线程1先进来__remove删除方法,会先加一把锁,然后发现没有东西可删,就让线程睡觉,这时候这把锁就会暂时解开。
  2. 然后线程2进来__add添加方法,由于上面暂时解锁了,线程2发现没锁,就加锁,然后添加东西,添加完东西通过signal唤醒线程1,线程1被唤醒之后又会拿到这把锁加锁,然后线程1继续执行代码,执行完代码线程1解锁,线程2执行完自己的东西也会解锁。最后的结果是线程1加锁解锁两次,线程2加锁解锁一次。

打印:

__remove - begin
添加了元素
删除了元素

可以发现,就算先进来的是remove方法,也会先添加元素再删除元素。

应用场景:

线程1依赖线程2,需要线程2做完某件事再回到线程1继续做事情。比如厂家和消费者的关系,消费者要想买东西,必须要厂家先生产东西。

六. NSLock、NSRecursiveLock、NSCondition

NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装

1. NSLock

NSLock相关API:

@protocol NSLocking

- (void)lock; //加锁
- (void)unlock; //解锁

@end

@interface NSLock : NSObject <NSLocking> {

//尝试加锁
- (BOOL)tryLock; 
//传入一个时间,在这个时间之前等待这把锁放开(阻塞),如果等到这把锁放开了,就加锁,加锁成功返回YES。如果在规定时间内别人还是没把这把锁放开,就加锁失败,返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit; 

//锁的名字
@property (nullable, copy) NSString *name;

@end

可以看出NSLock继承于NSObject,遵守了NSLocking协议,NSLocking协议有两个方法:加锁、解锁。

使用方法很简单,如下:

#import "NSLockDemo.h"

@interface NSLockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@property (strong, nonatomic) NSLock *moneyLock;
@end

@implementation NSLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.ticketLock = [[NSLock alloc] init];
        self.moneyLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)__saleTicket
{
    [self.ticketLock lock];
    
    [super __saleTicket];
    
    [self.ticketLock unlock];
}

- (void)__saveMoney
{
    [self.moneyLock lock];
    
    [super __saveMoney];
    
    [self.moneyLock unlock];
}

- (void)__drawMoney
{
    [self.moneyLock lock];
    
    [super __drawMoney];
    
    [self.moneyLock unlock];
}

@end

如何验证NSLock是对mutex普通锁的封装?
如果使用打断点,一句一句查看汇编,会先进入objc_msgSend的找lock方法的过程,比较麻烦,我们直接看GNUstep的实现:

在NSLock.m文件找到如下代码:

+ (void) initialize
{
......
      pthread_mutex_init(&deadlock, &attr_normal); //传入的是normal,普通锁
      pthread_mutex_lock(&deadlock);
......
}

上面代码验证了NSLock是对mutex普通锁的封装。

2. NSRecursiveLock

NSRecursiveLock是对mutex递归锁的封装,它的API和NSLock一样。

NSRecursiveLock相关API:

@interface NSRecursiveLock : NSObject <NSLocking> {

//尝试加锁
- (BOOL)tryLock; 
//传入一个时间,在这个时间之前等待这把锁放开(阻塞),如果等到这把锁放开了,就加锁,加锁成功返回YES。如果在规定时间内别人还是没把这把锁放开,就加锁失败,返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit; 

//锁的名字
@property (nullable, copy) NSString *name;

@end

使用方法就省略了,下面验证NSRecursiveLock是对mutex递归锁的封装。

也是在GNUstep的NSLock.m文件里面,找到NSRecursiveLock的init方法实现:

- (id) init
{
  if (nil != (self = [super init]))
    {
      if (0 != pthread_mutex_init(&_mutex, &attr_recursive)) //递归锁
    {
      DESTROY(self);
    }
    }
  return self;
}

可以发现,NSRecursiveLock的确是对mutex递归锁的封装。

3. NSCondition

NSCondition是对mutex和cond的封装。

NSCondition相关API:

@interface NSCondition : NSObject <NSLocking> {

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end

我们可以在GNUstep的NSLock.m文件里面找到源码实现:

- (void) signal
{
  pthread_cond_signal(&_condition);
}

- (void) wait
{
  pthread_cond_wait(&_condition, &_mutex);
}

- (BOOL) waitUntilDate: (NSDate*)limit
{
  NSTimeInterval ti = [limit timeIntervalSince1970];
  double secs, subsecs;
  struct timespec timeout;
  int retVal = 0;

  // Split the float into seconds and fractions of a second
  subsecs = modf(ti, &secs);
  timeout.tv_sec = secs;
  // Convert fractions of a second to nanoseconds
  timeout.tv_nsec = subsecs * 1e9;

  /* NB. On timeout the lock is still held even through condition is not met
   */

  retVal = pthread_cond_timedwait(&_condition, &_mutex, &timeout);
  if (retVal == 0)
    {
      return YES;
    }
  if (retVal == ETIMEDOUT)
    {
      return NO;
    }
  if (retVal == EINVAL)
    {
      NSLog(@"Invalid arguments to pthread_cond_timedwait");
    }
  return NO;
}

- (void) broadcast
{
  pthread_cond_broadcast(&_condition);
}

可以发现,上面封装的API的确是上面我们讲过的条件锁调用的API,验证了:NSCondition是对mutex和cond的封装。

NSCondition使用起来和mutex的条件锁是一样的,不解释了,如下:

#import "NSConditionDemo.h"

@interface NSConditionDemo()
@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation NSConditionDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.condition = [[NSCondition alloc] init];
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    //一个线程删除
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    //一个线程添加
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 线程1
// 删除数组中的元素
- (void)__remove
{
    [self.condition lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    [self.condition unlock];
}

// 线程2
// 往数组中添加元素
- (void)__add
{
    [self.condition lock];
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信号
    [self.condition signal];
    
    // 广播
    //[self.condition broadcast];

    [self.condition unlock];
}

打印也是:

__remove - begin
添加了元素
删除了元素

补充:

上面代码是先发送signal,再unlock的,其实先unlock再发送signal也可以。

  1. 先发signal,再unlock:线程2发送信号,线程1收到信号的时候,想要加锁,但是发现还没解锁,线程1继续等待,直到线程2解锁。然后线程1才开始执行接收到信号后要做的事,加锁、删除、解锁。
  2. 先发unlock,再signal:线程2先解锁,再发送信号,线程1收到信号的时候已经解锁了,然后线程1开始执行接收到信号后要做的事,加锁、删除、解锁。

这两行代码如果很近,就没啥区别,如果中间有很多其他代码,那就看你自己想达成什么效果了。

Demo地址:加锁方案-1

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

推荐阅读更多精彩内容