iOS底层-27:锁的原理

看了这么多的源码,相信大家对锁已经见得很多了。在iOS中有8大锁,他们的性能如下:



下面我们将会分析锁的底层原理,看看锁的性能为什么有好有坏?
在这之前,我们需要先了解一些名词:

TLS线程相关解释

线程局部存储(Thread Local Stirage,TLS):是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下,通常是通过pthread库中的
pthread_key_create()
pthread_getspecific()
pthread_setspecific()
pthread_key_delete()

互斥锁

Posix Thread中定义了有一套专门用于线程同步的mutex函数。

mutex用于保证在任何时刻,都只有一个线程访问该对象。当获取锁操作失败的时候,线程会进入睡眠,等待锁释放时被唤醒。

1. 创建和销毁
A. Posix定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁
B. int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
C. pthread_mutex_destroy()用于注销一个互斥锁

2. 互斥锁属性

3. 锁操作
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)语义和int pthread_mutex_lock(pthread_mutex_t *mutex) 类似,不同的是在锁已经被占据时返回busy而不是挂起等待

互斥锁分为递归锁和非递归锁。

@synchronized

  • 递归互斥锁
  • 可重入 嵌套
    准备一分简单的测试代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    for (int i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self addMethod];
        });
    }
  
}
- (void)addMethod {
    
    @synchronized (self) {
        self.number = [NSNumber numberWithInteger:(self.number.integerValue + 1)];
        NSLog(@"%@",self.number);
    }
}

由于加锁的原因,nunber的增长没有受到多线程的影响

  • @synchronized出打上断点,然后bt堆栈

    并没有看出什么,接着我们打开Debug -->Debug Workflow —> Always Show Disassembly显示汇编,发现了一个没有见过的方法objc_sync_enter

继续往下翻,还可以找到一个退出的方法objc_sync_exit


我们有理由怀疑这两个就是加锁和解锁的实际方法。也可以用clang编译,可以更直观的看到这两个方法。下面是我用clang编译出来的代码。

下面我们下一个符号断点,看看这两个方法在哪个源码中?


  • 打开libobjc源码,搜索objc_sync_enter
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();//加锁
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();//当传入空的时候,什么都没做
    }

    return result;
}
  • 查看SyncData
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

  • 查看recursive_mutex_t
using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;


这是一个递归锁的lockunlock方法

  • 查看id2data
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS //在本地线程空间中查找
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;
        //缓存中的data与传入的匹配
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;//通过COUNT_KEY获取lockCount
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");//没有线程,而你从线程中找到了,直接报错
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);//在缓存中查找
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                //在全局缓存中找到意味着开辟了新的线程 threadCount ++ 
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // 第一次进来,没有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {//把数据设置到线程缓存中
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // 把数据设置到缓存中
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}
  • 搜索objc_sync_exit
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();//解锁
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    return result;
}

总结:

  1. 第一次进来的时候,当前线程空间没有缓存,全局的线程缓存也不会有,首次赋值将threadCount、lockCount设置为1保存在tls和cache中
  2. 下次同一个线程进来,在tls中找到,lockCount++ tls重新保存。
  3. 下次不同线程进来,在cache中找到,lockCount++threadCount++tlscache重新保存

正因为@synchronized需要在各种线程缓存中查找、设置、保存才导致性能不佳。
优点是方便简单,不用解锁

注意

使用@synchronized要注意的问题

  1. 加锁的对象不能为nil
- (void)viewDidLoad {
    [super viewDidLoad];
    
    _mArr = [NSMutableArray array];
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self addMethod];
        });
    }
   

}
- (void)addMethod {
    
    @synchronized (_mArr) {
        _mArr = [NSMutableArray array];
    }
}

上面例子运行会出现崩溃。


打开Xcode中僵尸模式,更能说明问题。

再次运行

对一个已经释放了的对象发送release方法,才会崩溃。
这是因为_mArr一直在创建,新值retain,旧值release_mArr在某一个时刻会置为nil,而@synchronizednil的时候会直接return,没有效果。一般我们都是锁self,他和_mArr生命周期一样,而且不会置为nil;当他置为nil的时候,_mArr也没有意义了。

  • 当然self也不宜锁过多,如果所有的锁都锁self,底层缓存的链表里就到处都是self,不便查找也浪费性能。

将上述的锁换为NSLock,一样可以达到效果。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _mArr = [NSMutableArray array];
    
    [self addMethod];
   

}
- (void)addMethod {
    
    
    NSLock *lock = [[NSLock alloc] init];
    
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [lock lock];
            _mArr = [NSMutableArray array];
            [lock unlock];
        });
    }
}

运行不会报错。

NSLock

NSLock使用的非常多,也很简单。

  • 点击- (void)lock方法,进入NSLocking协议

往下翻我们可以看到NSLock、NSConditionLock、NSRecursiveLock和NSCondition都遵循NSLocking协议

  • 通过符号断点查看NSLock的源码归属


    NSLock源码在Foundation库中,我们大家都知道Foundation没有开源,这里我们可以借助swfit开源代码。

  • 搜索NSLock:找到源码


    我们可以看到init方法做了很多操作,我们使用的时候一定要调用init方法

  • 向下查找lock方法


    直接调用pthread_mutex_lock互斥锁

  • 向下查找unlock方法


    调用pthread_mutex_unlock解锁,然后让所有等待的线程wakeup

NSLock只是简单的封装pthread,消耗了一点性能,所以它的性能略低于pthread

NSLock的弊端

NSLock *lock = [[NSLock alloc] init];
    for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
            };
            testMethod(10);
        });
    }

上面的代码运行会出现问题,程序会一直等待。原因是NSLock只是一个普通的互斥锁,递归加锁会在还没有解锁的情况下重复加锁,造成死锁。


解决方案:

  • NSLock锁移至递归的外面


    这种方法的堵塞情况可能会很差,它把所有的方法都锁住了。

  • 换一个递归互斥锁


image.png

NSRecursiveLock

查看swfitFoundation源码

  • 搜索NSRecursiveLock

  • 查找lock

  • 查找unlock


    这两段代码和NSLock一模一样,那么为什么NSRecursiveLock是一个递归锁呢?

问题出在init方法中


NSRecursiveLock标识为递归锁。

NSRecursiveLock的弊端

  • 使用比较复杂,稍微写错位置可能就会崩溃。不如@synchronized简便

总结:

  • 如果只是简单的加锁,优先使用NSLock,性能最好
  • 嵌套加锁推荐使用@synchronized,如果你技术比较熟练使用NSRecursiveLock没有问题,那么它比@synchronized的性能更好。

NSCondition

NSCondition的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被堵塞。

  1. [condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock外等待,直到unlock才可访问
  2. [condition unlock];//与lock成对使用
  3. [condition wait];//让当前线程处于等待状态
  4. [condition signal];//CPU发信号告诉线程不用再等待,可以继续执行

例子

- (void)ly_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ly_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ly_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ly_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self ly_producer];
        });
        
    }
}

- (void)ly_producer{
    [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];

}

- (void)ly_consumer{
    
    // 线程安全
     [_testCondition lock];

    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
         [_testCondition wait];
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}

NSConditionLock

  • NSConditionLock是锁,一旦一个线程获得锁,其他线程一定要等待。
  • [xxxx lock];表示xxxx期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition)那么它可以执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者是无条件锁),则等待,直至其他线程解锁。
  • [xxxx lockWhenCondition:A条件]; 表示如果没有其他线程获得该锁但是锁的内部condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他线程都将等待它的代码完成,直至它解锁。
  • [xxxx unlockWhenCondition:A条件];表示释放锁,同时把内部的condition设置为A条件
  • return = [xxxx lockWhenCondition:A条件 beforeDate:A时间];表示如果被锁定(没获得锁),并且超过该时间则不再阻塞线程。但是注意,返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。
  • 所谓的condition就是整数,内部通过整数比较条件

例子

- (void)lg_testConditonLock{
    // 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1];
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

这次我们换一种方式查看NSConditionLock的原理,我们直接查看汇编代码。


在下面断点处,打开汇编。

  • 添加符号断点


  • 进入汇编


  • 往下翻断点在callq方法(call 一般是调用方法

  • register read 查看寄存器内容


可以看到调用了lockWhenCondition:beforeDate:方法

  • 添加符号断点-[NSConditionLock lockWhenCondition:beforeDate:]

  • 进入lockWhenCondition:beforeDate


    我们可以看到调用了lock方法,之后有调用了waitUntilDate:

NSConditionLock源码
  • 搜索NSConditionLock
  • 向下查看lock()方法

    实际调用的是lock(before limit: Date)
  • lock(before limit: Date)
 open func lock(before limit: Date) -> Bool {
        _cond.lock()//[condition lock];
        while _thread != nil {
            if !_cond.wait(until: limit) {//当时间到达之后,不在等待了,直接unlock
                _cond.unlock()            //然而这里传入的时间是遥远的未来
                return false
            }
        }
#if os(Windows)
        _thread = GetCurrentThread()
#else
        _thread = pthread_self()//获取自身线程
#endif
        _cond.unlock()//[condition unlock];
        return true
    }
  • 搜索unlock
    open func unlock() {
        _cond.lock()
#if os(Windows)
        _thread = INVALID_HANDLE_VALUE
#else
        _thread = nil
#endif
        _cond.broadcast()//广播
        _cond.unlock()////[condition unlock];
    }

  • 搜索lock(whenCondition

    实际执行的是lock(whenCondition: before:
  • 搜索lock(whenCondition:
  open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        _cond.lock()//[condition lock];
        while _thread != nil || _value != condition {//条件不匹配的时候,一直等待
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
#if os(Windows)
        _thread = GetCurrentThread()
#else
        _thread = pthread_self()
#endif
        _cond.unlock()
        return true
    }
  • 搜索unlock(withCondition
   open func unlock(withCondition condition: Int) {
        _cond.lock()
#if os(Windows)
        _thread = INVALID_HANDLE_VALUE
#else
        _thread = nil
#endif
        _value = condition//把_value设置为condition
        _cond.broadcast()//发出广播
        _cond.unlock()//
    }
  • broadcast()
open func broadcast() {
#if os(Windows)
        WakeAllConditionVariable(cond)
#else
        pthread_cond_broadcast(cond) // wait  signal
#endif
    }

唤醒所有等待的线程。

总结

  • 线程1 调用[conditionLock lockWhenCondition:],此时因为不满足当前条件,所以会进入waiting状态,当前进入waiting状态会释放当前的互斥锁。
  • 线程3 此时调用[conditionLock lock];实际上是调用[conditionLock lockBeforeDate:],这里不需要条件对比,所以3会打印
  • 接下来是线程2 执行[conditionLock lockWhenCondition:],因为满足条件所以线程2会打印,打印完成后调用[conditionLock unlockWithCondition:1];,将value设置为1,并发送broadcast,此时线程1接到信号,条件也符合,线程1才会打印。
  • [NSConditionLock lockWhenCondition:];这里会根据传入的conditionvalue进行对比,如果不相等就会阻塞;如果相等才会执行。
  • [NSConditionLock unlockWithCondition:];这里会先改变value的值,然后进行广播,唤醒其他线程。

锁的归类

自旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短的时间场合是有效的。

互斥锁

是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。通过将代码切片成一个个的临界区而达成
这里属于互斥锁的有:

  • NSLock
  • pthread_mutex
  • @synchronized
条件锁

就是条件变量,当进程的某些资源要求不满足时,就进入休眠,也就是锁住了。当资源被分配到,满足条件时,条件锁打开,进程继续执行。

  • NSCondition
  • NSConditionLock
递归锁

就是同一个线程可以加锁N次,而不会引发死锁。

  • NSRecursiveLock
  • pthread_mutex(recursive)
信号量(semaphore)

是一种更高级的同步机制,互斥锁可以说是semaphore在取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不是单单的线程间互斥。
-dispatch_semaphore
其实基本的锁就包括三类:自旋锁、互斥锁、读写锁
其他的比如条件锁、递归锁、信号量都是上层的封装。

读写锁

读写锁实际上是一种特殊的自旋锁,它把对公共资源的访问者分为读者和写者,读者只对共享资源进行读访问,写者才需要对共享资源进行写操作。这种锁相对于自旋锁,可以提高并发性。因为在多处理器系统中,它允许有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

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

推荐阅读更多精彩内容