iOS 之 线程锁整理

前言

最开始我想把线程和线程锁放在一起整理出一篇文章,结果整理了线程发现有点长,于是便把线程锁单独拿出来了。感兴趣的小伙伴也可以去看下线程的生命周期,NSThread、GCD、NSOperation的使用与总结,因为两篇文章原本是想放在一起的。


正文

一、锁的一些概念和性能对比

1.1 为什么要使用锁(线程安全)

线程安全是指,当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

举例来说:现在仅剩余一张火车票,每一个购票请求都是一个线程,那么同一时刻有多个线程同时请求出票,那么剩余的这一张票将会同时出票多次,这明显是不合理的,所以锁的出现,就是为了确保线程安全问题。

1.2 锁的一些概念
  • 临界资源: 多个线程共享各种资源,然而有很多资源一次只能供一个线程使用。一次仅允许一个线程使用的资源称为临界资源。

  • 临界区:访问临界资源的代码区。

  • 死锁:指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(都要等待对方完成某个操作才能进行下一步),若无外力作用,它们都将无法推进下去,这时就会发生死锁。

  • 上下文切换(Context Switch):在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。

  • 轮询(Polling):一种CPU决策如何提供周边设备服务的方式,又称“程控输入输出”(Programmed I/O)。轮询法的概念是,由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。

  • 原子属性:

    • atomic:原子属性,设置成员变量的@property属性时,默认为atomic,提供多线程安全。atomic是为setter方法加锁,将属性以atomic的形式来声明,该属性变量就能支持互斥锁了。而这种机制是耗费系统资源的。

    • nonatomic:非原子属性,不会为setter方法加锁,声明为该属性的变量,客户端应尽量避免多线程争夺同一资源。

  • 自旋锁:线程反复检查锁变量是否可用(类似于while(锁没解开)),因此是一种忙等状态,将一直占用CPU资源。

  • 读写锁:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

  • 互斥锁:在被访问的资源被锁时,会让当前线程进入休眠状态,不再占用CPU资源,一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

  • 条件锁:在一定条件下,让其等待休眠,并放开锁,等接收到信号或者广播,会从新唤起线程,并重新加锁。

  • 递归锁:同一个线程可以多次加锁,不会造成死锁,不同线程来访问这段代码时,发现有锁要等待所有锁解开之后才可以继续往下走。

  • 信号量(Semaphore):有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

1.3 性能对比
锁的性能比较.png
//10000000
OSSpinLock:                 112.38 ms
dispatch_semaphore:         160.37 ms
os_unfair_lock:             208.87 ms
pthread_mutex:              302.07 ms
NSCondition:                320.11 ms
NSLock:                     331.80 ms
pthread_rwlock:             360.81 ms
pthread_mutex(recursive):   512.17 ms
NSRecursiveLock:            667.55 ms
NSConditionLock:            999.91 ms
@synchronized:             1654.92 ms
//1000
OSSpinLock:                   0.02 ms
dispatch_semaphore:           0.03 ms
os_unfair_lock:               0.04 ms
pthread_mutex:                0.06 ms
NSLock:                       0.06 ms
pthread_rwlock:               0.07 ms
NSCondition:                  0.07 ms
pthread_mutex(recursive):     0.09 ms
NSRecursiveLock:              0.12 ms
NSConditionLock:              0.18 ms
@synchronized:                0.33 ms

二、锁的使用(种类)

上锁有两种方式trylocklock:当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock
注:以下大部分锁都会提供trylock接口,不再作解释。

2.1 OSSpinLock (自旋锁)
  • OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。
  • OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。
  • OSSpinLock 有潜在的优先级反转问题,iOS10.0以后弃用了这种锁机制,使用os_unfair_lock。
需要导入头文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
OSSpinLockTry(&spinLock)

以GCD为例,代码以及执行结果如下:

2.1.1 OSSpinLockLock
// OSSpinLockLock
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程1  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程2  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程3  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });
}
OSSpinLockLock

我们可以看到,我们用的并发异步线程,但是加锁之后,执行结果并没有并发异步执行。

2.1.2 OSSpinLockTry
// OSSpinLockTry
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程4  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程5  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程6  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });
}
OSSpinLockTry-1

执行结果可以看出来,OSSpinLockTry并没有阻塞线程。也符合上面所说:当前线程锁失败,也可以继续其它任务。但是这只是测试一下,项目中不要这么写,因为这样没有意义,可以如下:

//OSSpinLockTry
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程7  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁7失败,执行一些其他事情");
        }
    });

    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程8  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁8失败,执行一些其他事情");
        }
    });

    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程9  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁9失败,执行一些其他事情");
        }
    });
    
}
OSSpinLockTry-2

执行结果可以看出,加锁失败后执行另一部分代码,并没有自旋去等待加锁,执行后其他锁释放也不会再次加锁,所以用的时候要考虑场景。

2.2 os_unfair_lock(互斥锁)

os_unfair_lock网上很多文章,有说它是自旋锁的,但是官方文档中有一段是这样的:

This is a replacement for the deprecated OSSpinLock. This function doesn't spin on contention, but instead waits in the kernel to be awoken by an unlock.

自我理解为:“这是对已弃用的osspinlock的替换。这个函数不会在争用时自旋,而是在内核中等待解锁来唤醒。”所以,它应该是互斥锁,并不是自旋锁。

需要导入头文件
#import <os/lock.h>
// 初始化
 os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&unfair_lock);
// 解锁
os_unfair_lock_unlock(&unfair_lock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
os_unfair_lock_trylock(&unfair_lock);
/*
注:解决不同优先级的线程申请锁的时候不会发生优先级反转问题.
不过相对于 OSSpinLock , os_unfair_lock性能方面减弱了许多.
*/

使用方法上同,不做示范。

2.3 dispatch_semaphore (信号量)

信号量,是持有计数的信号。个人觉得有点类似于引用计数:create时定义最大线程数,使用时wait进行计数-1,结束时signal进行计数+1,当计数大于零时可执行,等于零时阻塞线程进行等待执行。
信号量的作用,个人觉得有以下几点:

  • 控制线程数量(最大并发数),我们知道NSOperationQueue我们可以设置maxConcurrentOperationCount来控制最大并发数,但是GCD的话,并没有一个属性可以控制最大并发数,所以我们可以用信号量来控制GCD的最大并发数。
  • 线程安全(锁),信号量的本质还是控制子线程并发数量,而我们可以设置最大并发量为1,然后在临界区(多条线程都会访问的代码区)进行信号量控制,保证同一时刻只有一条线程执行此段代码,从而保证线程安全。
  • 线程同步,会在后面代码说明。
  • 阻塞线程,会在后面和同步代码一起说明。
// 初始化
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore_t);
2.3.1 最大并发数

不多说,直接上代码:

//2.dispatch_semaphore
- (void)UseSemaphore{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程1 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程2 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程3 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程4 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
}

最大并发数

通过结果我们可以看到,一共是四个并发异步线程,但是由于设置信号量,间接控制了最大并发数。值得注意的是:最大并发数,是指执行任务的线程最多是两个(信号量设置的是两个),但是,处于回收状态的线程不算此列.也就是说,执行任务的时候不只有两个线程,还有处于回收状态的线程,所以子线程个数不为2;

2.3.2 线程安全

线程不安全时:

- (void)UseSemaphoreLock{
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    self.number = 50;
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
}

- (void)changeNumber{
    _number = _number - 1;
    sleep(1);
    NSLog(@"number == %ld",_number);
}
线程不安全时

我们可以看到,如果异步线程,同时更改同意资源时,那么可能出现数据混乱。所以我们可以用信号量加锁,保证线程安全:

- (void)UseSemaphoreLock{
    self.semaphore1 = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    self.number = 50;
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
}

- (void)changeNumber{
    //相当于加锁
    dispatch_semaphore_wait(_semaphore1, DISPATCH_TIME_FOREVER);
    _number = _number - 1;
    sleep(1);
    NSLog(@"number == %ld",_number);
    //相当于解锁
    dispatch_semaphore_signal(_semaphore1);
}

线程安全时

如上,用信号量可以处理线程安全问题。(当然我们也可以把waitsignal加到异步线程当中,但是觉得那么做的话,实际上还是控制了最大并发数,并不是解决线程安全。)

2.3.3 线程同步
//semaphore实现线程同步
- (void)semaphoreSync {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务1
        [NSThread sleepForTimeInterval:1];              // 模拟耗时操作
        NSLog(@"任务1 %@",[NSThread currentThread]);      // 打印当前线程
        number = 100;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end1,number = %d",number);
    dispatch_async(queue, ^{
        // 追加任务2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"任务2 %@",[NSThread currentThread]);      // 打印当前线程
        
        number = 50;
        
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end2,number = %d",number);
    dispatch_async(queue, ^{
        // 追加任务3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"任务3 %@",[NSThread currentThread]);      // 打印当前线程
        number = 10;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end3,number = %d",number);
}

同步

可以看到,虽然我们建立的并行异步线程,但是执行结果却是同步执行。原因如下:在主线程程中设置信号量为0dispatch_semaphore_create(0);当执行到追加任务1的子线程时,进入子线程,主线程继续执行dispatch_semaphore_wait,但是此时信号量为0,dispatch_semaphore_wait在此处阻塞主线程进入等待状态,直到任务1的子线程执行dispatch_semaphore_signal使信号量+1,此时主线程中处于等待的dispatch_semaphore_wait可以使信号量-1,于是停止阻塞线程并继续向下执行。(利用阻塞线程实现线程同步)

2.4 pthread_mutex(互斥锁)
  • pthread_mutex 是 C 语言下多线程加互斥锁的方式。
  • 被这个锁保护的临界区就只允许一个线程进入,其它线程如果没有获得锁权限,那就只能在外面等着。
需要导入头文件
#import <pthread/pthread.h>
//声明锁
pthread_mutex_t mutex_t;
// 初始化(两种)
1.普通初始化
pthread_mutex_init(&mutex_t, NULL); 
2.宏初始化
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
// 加锁
pthread_mutex_lock(&mutex_t);
// 解锁
pthread_mutex_unlock(&mutex_t);
// 尝试加锁,可以加锁时返回的是 0,否则返回一个错误
pthread_mutex_trylock(& mutex_t)
// 释放锁
pthread_mutex_destroy(&_lock)
  • 锁类型:
pthread_mutex_init(&mutex_t, NULL);
初始化锁 NULL等同于PTHREAD_MUTEX_DEFAULT

PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。

PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。

PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。

PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列
2.5 NSCondition(条件锁、对象锁)
// 初始化
NSCondition *_condition= [[NSCondition alloc]init];
// 加锁
[_condition lock];
// 解锁
[_condition unlock];
/*
其他功能接口
wait 进入等待状态
waitUntilDate:让一个线程等待一定的时间
signal 唤醒一个等待的线程
broadcast 唤醒所有等待的线程
注: 所测时间波动太大, 有时候会快于 NSLock, 我取得中间值.
*/
2.6 NSLock(互斥锁、对象锁)
// 初始化
NSLock *_lock = [[NSLock alloc]init];
// 加锁
[_lock lock];
// 解锁
[_lock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_lock tryLock];
2.7 pthread_rwlock(读写锁)

读写锁又称共享-互斥锁:

//加读锁
pthread_rwlock_rdlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
//加写锁
pthread_rwlock_wrlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
2.8 pthread_mutex(recursive)(递归锁)
// 初始化
pthread_mutex_t mutex_t;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&mutex_t, &attr);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
// 加锁
pthread_mutex_lock(&mutex_t);
// 解锁
pthread_mutex_unlock(&mutex_t);
/*
注: 递归锁可以被同一线程多次请求,而不会引起死锁。
即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
这主要是用在循环或递归操作中。
*/
2.9 NSRecursiveLock(递归锁、对象锁)
// 初始化
NSRecursiveLock *_recursiveLock = [[NSRecursiveLock alloc]init];
// 加锁
[_recursiveLock lock];
// 解锁
[_recursiveLock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_recursiveLock tryLock];
/*
注: 递归锁可以被同一线程多次请求,而不会引起死锁。
即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
这主要是用在循环或递归操作中。
- (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
*/
2.10 NSConditionLock(条件锁、对象锁)
// 初始化
NSConditionLock *_conditionLock = [[NSConditionLock alloc]init];
// 加锁
[_conditionLock lock];
// 解锁
[_conditionLock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_conditionLock tryLock];
/*
其他功能接口
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; //初始化传入条件
- (void)lockWhenCondition:(NSInteger)condition;//条件成立触发锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//尝试条件成立触发锁
- (void)unlockWithCondition:(NSInteger)condition;//条件成立解锁
- (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//触发锁 条件成立 并且在等待时间之内
*/
2.11 @synchronized()递归锁、互斥锁
// 初始化
@synchronized(锁对象){
}
底层封装的pthread_mutex的PTHREAD_MUTEX_RECURSIVE 模式,
锁对象来表示是否为同一把锁
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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