iOS开发中的锁

前言

在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。


概述

锁是多线程开发中最基本的同步工具。开发中常用的锁通常分为以下几种类型:

  • Mutex(互斥锁): 互斥锁是一种信号量,一次只能访问一个线程如果一个互斥体正在使用,而另一个线程试图获取它,则该线程将阻塞,直到互斥体被其原始持有者释放。如果多个线程竞争同一个互斥体。则一次只允许一个线程访问它。

  • Recursive lock(递归锁):递归锁是互斥锁的变体。递归锁允许单个线程在释放之前多次获取锁。其他线程保持阻塞状态。直到锁的所有者释放锁的次数与获取它的次数相同,递归锁主要在递归中使用,但也可能在多个方法需要单独获取锁的情况下使用。

  • Spin lock(自旋锁): 自旋锁重复其锁定条件,直到该条件成立。自旋锁最常用于多处理器系统,其中锁的预期等待时间很短。在这些情况下,轮询通常比阻塞线程更高效,这涉及到上下文切换和线程数据结构的更新。

  • Read-write lock(读写锁):读写锁也被称为Shared-exclusive lock。通常用于较大规模的操作,适用于数据结构被频繁读取和偶尔修改,可以显着提高性能。在正常操作期间,多个线程可以同时访问数据。当一个线程想要写入数据时会阻塞,直到所有的读取线程释放锁。此时,写入线程才能获取锁,并修给数据。写入线程在锁定时,新的线程将被阻塞,直到写入线程完成。

  • Distributed lock(分布式锁): 分布式锁提供进程级别的互斥访问。与真正的互斥锁不同,分布式锁不会阻塞进程或阻止进程运行。它只是报告锁何时忙,让流程决定如何进行。

  • Double-checked lock(双重检查锁): 双重检查锁试图通过在锁定之前测试锁定标准来降低锁定的开销。由于双重检查的锁可能是不安全的,系统不提供对它们的明确的支持,并且它们的使用是不鼓励的。

以上大致介绍了锁的分类,下面将介绍Objective-C中各种实现锁的方式。

一、@synchronized指令

简介

@synchronized指令是Objective-C中易用性和可读性最好的创建互斥锁的方式。我们不用去直接创建锁和锁定对象,它会像其它互斥锁一样,防止其它线程获取同一个锁。传递给@synchronized的对象是区分保护块的唯一标识。它的简单用法是这样

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        //需要加锁的内容
    }
}

实现原理

编译器将@synchronized转化成了一对objc_sync_enter()objc_sync_exit()的调用,通过查看源码我们可以分析得出:

  • @synchronized是通过recursive_mutex_t递归锁实现;
  • @synchronized(object)中传入的object的内存地址,被用作唯一的key,通过hash map对应到一个系统维护的递归锁;
  • objc_sync_enter()objc_sync_exit()并没有对传入的对象做retainsreleases;
  • objc_sync_enter(nil)objc_sync_exit(nil)不起任何作用;

详细的分析可以参见这篇博客:正确使用多线程同步锁@synchronized()

使用注意

通过上面的分析, 我们可以得出@synchronized使用中应该注意的几个问题

  • 因为@synchronized使用递归锁实现的,所以如下代码不会产生死锁;
@synchronized (obj) {
    NSLog(@"1st sync");
    @synchronized (obj) {
        NSLog(@"2nd sync");
    }
}
  • 因为是利用传入的object的内存地址作为唯一标识,所以传入的object理论上可以是任意对象,但应避免不同的critical section使用相同的锁,应该是不同的数据使用不同的锁;
@synchronized (objectA) {
    [arrA addObject:obj];
}

@synchronized (objectB) {
    [arrB addObject:obj];
}
  • 应该注意传入对象的生命周期,因为@synchronized(object)并没有对object进行retains,因此当@synchronized(nil)时,将不起任何作用;

  • 注意@synchronized(object)内部的方法调用,将不需要同步操作的方法放在外面调用;

  • 另外官方文档中提到 :

作为预防措施,@synchronized块隐式地向受保护的代码添加异常处理程序。如果引发异常,该处理程序会自动释放互斥锁。这意味着为了使用@synchronized指令,还必须在代码中启用Objective-C异常处理。如果您不想由隐式异常处理程序引起额外开销,则应考虑使用锁类。

二、pthread_mutex

简介

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。相关函数:

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_destroy(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_init(pthread_mutex_t * __restrict,
        const pthread_mutexattr_t * _Nullable __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_lock(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_trylock(pthread_mutex_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_unlock(pthread_mutex_t *);
  • pthread_mutex 可通过宏定义静态初始化pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
  • 动态方式是采用int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)函数来初始化互斥锁,mutexattr用于指定属性,如果为NULL则使用缺省属性。
  • int pthread_mutex_destroy(pthread_mutex_t *mutex)函数用于销毁一个互斥锁,这意味着释放它所占用的资源,且要求锁当前处于开放状态。
  • pthread_mutexPTHREAD_MUTEX_NORMAL(普通互斥锁)、PTHREAD_MUTEX_ERRORCHECK(检错锁)和PTHREAD_MUTEX_RECURSIVE(递归锁等属性;

它的简单用法如下:

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);// 定义锁的属性

    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr); // 创建锁

    pthread_mutex_lock(&mutex); // 申请锁
    
    //需要加锁的代码
    
    pthread_mutex_unlock(&mutex); // 释放锁

使用注意

  • 同一线程,多次获得同一锁时,会造成死锁,此时应使用PTHREAD_MUTEX_RECURSIVE属性。

三、pthread_rwlock

简介

pthread_rwlockpthread中定义的读写锁,相关函数如下:

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_destroy(pthread_rwlock_t * ) __DARWIN_ALIAS(pthread_rwlock_destroy);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
        const pthread_rwlockattr_t * _Nullable __restrict)
        __DARWIN_ALIAS(pthread_rwlock_init);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_rdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_rdlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_tryrdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_tryrdlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_trywrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_trywrlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_wrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_wrlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_unlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_unlock);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t * __restrict,
        int * __restrict);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_init(pthread_rwlockattr_t *);

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int);
  • pthread_mutex 类似,pthread_rwlock也可以通过宏定义快速初始化:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
  • 也可以通过int pthread_rwlock_init(pthread_rwlock_t * __restrict, const pthread_rwlockattr_t * _Nullable __restrict)初始化,其中pthread_rwlockattr_t是属性对象;
  • 属性对象可以通过int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);初始化。同时可以通过int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int)用来设置读写锁的作用范围,这里需要的int类型有两个宏定义PTHREAD_PROCESS_SHARED:该读写锁可以在多个进程中的线程之间共享。PTHREAD_PROCESS_PRIVATE:仅初始化本读写锁的线程所在的进程内的线程才能够使用该读写锁。

使用注意

  • 由于读写锁的性质,必须要等到所有读锁都释放之后,才能成功申请写锁,这就很容易导致写线程饥饿。

四、NSLock

简介

NSLock是典型的面向对象的锁,遵循Objective-CNSLocking协议接口,该协议定义了lockunlock。此外NSLock类还增加了tryLocklockBeforeDate:方法。tryLock方法尝试获取锁,如果锁不可用返回NOlockBeforeDate:尝试在指定时间内获取锁,如不成功返回NO

    NSLock *lock = [[NSLock alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程1 准备加锁ing...");
        [lock lock];
        NSLog(@"线程1 锁定成功");
        sleep(5);//睡眠5秒
        NSLog(@"线程1 准备解锁");
        [lock unlock];
        NSLog(@"线程1 解锁成功");
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程2 尝试加锁ing...");
        BOOL x =  [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
        if (x) {
            NSLog(@"线程2 锁定成功");
            [lock unlock];
            NSLog(@"线程2 解锁成功");
        }else{
            NSLog(@"线程2 加锁失败");
        }
    });

实现原理

  • NSLock是在内部封装了一个 pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

使用注意

  • NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。解锁来自不同线程的锁可能会导致未定义的行为。
  • 在同一线程上两次调用lock方法,会造成死锁,因此在递归中不能使用NSLock。应使用NSRecursiveLock

五、NSRecursiveLock

简介

NSRecursiveLock是面向对象的递归锁,同样遵循Objective-CNSLocking协议接口。该锁可被同一线程多次获取,而不会造成死锁。只是记录获取锁成功的次数,只有调用解锁的次数与锁定次数相同时,锁才会被真正释放,此时才能被其它线程获取。

NSRecursiveLock *rLock = [NSRecursiveLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [rLock lock];
        if (value > 0) {
            NSLog(@"线程%d", value);
            RecursiveBlock(value - 1);
        }
        [rLock unlock];
    };
    RecursiveBlock(4);
});

实现原理

  • NSRecursiveLock递归锁也是通过 pthread_mutex 来实现,与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型为 PTHREAD_MUTEX_RECURSIVE

使用注意

  • 因为在所有锁定调用与解锁调用保持平衡之前,递归锁定不会被释放,所以我们应该仔细衡量使用递归锁潜在性能影响。长时间保持锁可能导致其他线程阻塞,直到递归完成。如果可以,我们应该从代码设计上尽量避免递归锁。

六、NSCondition

简介

NSCondition同样遵循NSLocking协议,NSCondition提供了单独的信号量管理接口。

- (void)wait;//阻塞当前线程直到条件锁发出信号为止,在调用此方法之前必须锁定接收器。
- (BOOL)waitUntilDate:(NSDate *)limit;//阻塞当前线程直到条件锁发出信号或达到指定的时间限制为止。
- (void)signal;//条件锁的信号,唤醒一个等待的线程,可多次调用唤醒多个线程,如没有被锁定的线程,则不起任何作用。为了避免竞争条件锁,应该仅在接收器锁定时调用此方法。
- (void)broadcast;//唤醒全部等待的线程。

实现原理

  • NSCondition是通过条件变量(condition variable) pthread_cond_t 来实现的,同时封装了一个互斥锁和条件变量。提供了线程阻塞与信号机制。

使用注意

  • NSCondition是通过条件变量实现,而条件变量必须和一个互斥锁配合, 以防止多个线程同时请求pthread_cond_wait()的竞争条件。所以在调用waitwaitUntilDate方法前,必须在本线程调用lock方法,确保当前线程为锁定状态;
  • wait函数并不是完全可信的,可能存在虚假唤醒。也就是说wait返回后,并不代表对应的事件一定被触发了,因此,为了保证线程之间的同步关系,使用NSCondtion时往往需要加入一个额外的变量来对非正常的wait返回进行规避。
//等待事件触发的线程  
[cocoaCondition lock];  
while (timeToDoWork <= 0)  
    [cocoaCondition wait];  
   
timeToDoWork--;  
   
// Do real work here.     
[cocoaCondition unlock];  

  
//出发事件的线程  
[cocoaCondition lock];  
timeToDoWork++;  
[cocoaCondition signal];  
[cocoaCondition unlock]; 
  • 经笔者测试,当多个线程被阻塞时,通过调用signal进行唤醒操作,线程被唤醒的顺序与wait的调用顺序相关,而与线程的优先级无关。
        NSLog(@"thread1:等待发送1");
        [cocoaCondition lock];
        [cocoaCondition wait];
        
        NSLog(@"thread1:发送1");
        [self.condition unlock];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        sleep(1);
        
        NSLog(@"thread2:等待发送2");
        [cocoaCondition lock];
        [cocoaCondition wait];
        
        NSLog(@"thread2:发送2");
        [cocoaCondition unlock];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);
        [cocoaCondition lock];
        NSLog(@"thread3:收到数据");
        [cocoaCondition signal];
        [cocoaCondition unlock];
        
    });
    

    打印结果:
     thread1:等待发送1
     thread2:等待发送2
     thread3:收到数据
     thread1:发送1

七、NSConditionLock

简介

NSConditionLock遵循NSLocking协议,可以与用户自定义的条件相关联的互条件锁,是互斥锁的变种。提供了更加直观、方便的条件管理接口,可以更方便的实现生产者-消费者模式。

- (instancetype)initWithCondition:(NSInteger)condition;//初始化一个条件锁,并设置条件;
- (void)lockWhenCondition:(NSInteger)condition;//当条件满足时,获取锁。
- (BOOL)tryLock;//不考虑条件,直接尝试获取锁,成功返回YES,反之为NO。
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//条件满足时尝试锁定,成功返回YES,反之为NO。
- (void)unlockWithCondition:(NSInteger)condition;//解锁并重新设置条件。
- (BOOL)lockBeforeDate:(NSDate *)limit;//在限制期限内获取锁,成功返回YES,反之为NO。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//当条件满足时,在指定期限内获取锁,此方法会阻塞线程,直达获取锁(返回YES)或超时(返回NO)。

NSConditionLock可以很方便的实现线程间的依赖关系:

    id condLock = [[NSConditionLock alloc] initWithCondition:0];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:2];
        NSLog(@"线程1");
        [condLock unlockWithCondition:0];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:1];
        NSLog(@"线程2");
        [condLock unlockWithCondition:2];
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condLock lockWhenCondition:0];
        NSLog(@"线程3");
        [condLock unlockWithCondition:1];
        
    });
    
    打印结果:
    线程3
    线程2
    线程1

实现原理

NSConditionLock 借助 NSCondition 来实现 内部持有一个NSCondition对象,和一个_condition_value属性,调用lockWhenCondition:时,只有_condition_value条件值相等时,才能获得锁。

八、dispatch_semaphore

简介

dispatch_semaphore信号量,GCD中基于信号控制访问资源的线程数量。当限定的线程数量为一时,就起到了和同步锁相同的效果;信号量主要的函数如下:

dispatch_semaphore_create(long value);//用初始值创建新的计数信号量。注意:value值必须>=0,否则返回NULL。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待(递减)信号量。
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//信号(增加)信号量。

信号量的简单用法如下:

    //value表示最多几个资源可访问
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
    dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //线程1
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程1:启动");
        sleep(1);
        NSLog(@"线程1:完成");
        dispatch_semaphore_signal(semaphore);
    });
    //线程2
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程2:启动");
        sleep(1);
        NSLog(@"线程2:完成");
        dispatch_semaphore_signal(semaphore);
    });
    //线程3
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"线程3:启动");
        sleep(1);
        NSLog(@"线程3:完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    当value = 3时,打印结果:
    线程1:启动
    线程2:启动
    线程3:启动
    
    线程1:完成
    线程2:完成
    线程3:完成
    
    当value = 2时,打印结果:
    线程1:启动 
    线程2:启动
 
    线程1:完成
    线程2:完成
    线程3:启动
    
    线程3:完成
    
    当value = 1时,打印结果:
    线程1:启动
    
    线程1:完成
    线程2:启动
    
    线程2:完成
    线程3:启动
    
    线程3:完成

当信号计数大于0时,每条进来的线程使调用dispatch_semaphore_wait函数使计数减1;直到信号技术为0,阻塞其他线程,直到执行的线程调用dispatch_semaphore_signal函数使级数加1,信号技术大于0,允许阻塞的线程启动;可见,当信号计数控制为1时,可实现同步锁的作用。

实现原理

想要深入的理解dispatch_semaphore,可以参考这篇博客源码

使用注意

  • 创建信号量时,传入的value参数必须>=0,否则返回NULL;
  • timeout参数为dispatch_time_t类型。比较有用的两个宏是DISPATCH_TIME_NOW(表示当前,立即返回超时)和DISPATCH_TIME_FOREVER (表示遥远的未来,一直等待下去)。一般可以直接设置timeout为这两个宏其中的一个,或者自己创建一个dispatch_time_t类型的变量。创建dispatch_time_t类型的变量有两种方法,dispatch_timedispatch_walltime
  • 通过dispatch_semaphore特征我们不难发现, 它不支持递归。

九、OSSpinLock

简介

OSSpinLock是iOS/MacOS自有的自旋锁,其特点是线程在等待取锁时,不会被挂起,而是保持空转,这避免了上下文切换等锁的操作。适用于临界区耗时短的操作,如果等待取锁的时间过长,轮训操作会消耗大量CPU资源。其主要函数和简单适用如下:

typedef int32_t OSSpinLock;

bool    OSSpinLockTry( volatile OSSpinLock *__lock );

void    OSSpinLockLock( volatile OSSpinLock *__lock );

void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(& spinLock);
        NSLog(@"线程1");
        sleep(10);
        OSSpinLockUnlock(& spinLock);
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(& spinLock);
        NSLog(@"线程2");
        OSSpinLockUnlock(& spinLock);
    });

因优先级反转的问题,在不同优先级的线程中OSSpinLock已不在安全,详细的介绍可参见这篇博客不再安全的 OSSpinLock,这里不在做过多介绍。

性能对比

这里贴出一张ibireme测试的结果图:


性能对比

总结

以上介绍的几种加锁方式,在原理、用法和性能等方面上各有不同。我们不能单从性能方面评论孰好孰坏。应该根据不同的需求和场景,选取合适的加锁方式。

  • 对性能要求苛刻,且临界区耗时很短,可以考虑自旋锁;
  • 同样对性能要求高,dispatch_semaphorepthread_mutex也是不错的选择;
  • 如果要临界区有大量的读写操作,且读多写少,那么pthread_rwlock或许是不错的选择;
  • NSLockNSRecursiveLock更加面向对象,用起来或许跟顺手;
  • NSConditionLock虽然性能不强,但是提供了方便的面向对象的条件管理接口;
  • 在递归中使用pthread_mutex(recursive)NSRecurisiveLcok都不错;
  • @synchronized虽然性能最差,但是操作简单快捷,如果是低频操作,或者紧急修复,它也是很好的选择;

参考资料

Synchronization

More than you want to know about @synchronized

pthread_mutex_lock.c 源码

深入理解 iOS 开发中的锁

深入理解GCD

pthread的各种同步机制

不再安全的 OSSpinLock

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

推荐阅读更多精彩内容