GCD(四) dispatch_semaphore

本文是GCD多線程編程中dispatch_semaphore內容的小結,通過本文,你可以了解到:

  • 信號量的基本概念與基本使用
  • 信號量在線程同步與資源加鎖方面的應用
  • 信號量釋放時的小陷阱

今天我來講解一下dispatch_semaphore在我們平常開發中的一些基本概念與基本使用,dispatch_semaphore俗稱信號量,也稱為信號鎖,在多線程編程中主要用于控制多線程下訪問資源的數量,比如系統有兩個資源可以使用,但同時有三個線程要訪問,所以只能允許兩個線程訪問,第三個應當等待資源被釋放后再訪問,這時我們就可以使用dispatch_semaphore

dispatch_semaphore相關的共有3個方法,分別是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面我們逐一了解一下這三個方法。

測試代碼在這

semaphore的三個方法

dispatch_semaphore_create

/*!
 * @function dispatch_semaphore_create
 *
 * @abstract
 * Creates new counting semaphore with an initial value.
 *
 * @discussion
 * Passing zero for the value is useful for when two threads need to reconcile
 * the completion of a particular event. Passing a value greater than zero is
 * useful for managing a finite pool of resources, where the pool size is equal
 * to the value.
 *
 * @param value
 * The starting value for the semaphore. Passing a value less than zero will
 * cause NULL to be returned.
 *
 * @result
 * The newly created semaphore, or NULL on failure.
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(long value);

dispatch_semaphore_create方法用于創建一個帶有初始值的信號量dispatch_semaphore_t

對于這個方法的參數信號量的初始值,這里有2種情況:

  1. 信號量初始值為0時:這種情況主要用于兩個線程需要協調特定事件的完成時,即線程同步。
  2. 信號量初始值為大于0時:這種情況主要用于管理有限的資源池,其中池大小等于這個值,即資源加鎖。

上面的2種情況(線程同步、資源加鎖),我們在后續的使用篇中會詳細講解。

dispatch_semaphore_wait

/*!
 * @function dispatch_semaphore_wait
 *
 * @abstract
 * Wait (decrement) for a semaphore.
 *
 * @discussion
 * Decrement the counting semaphore. If the resulting value is less than zero,
 * this function waits for a signal to occur before returning.
 *
 * @param dsema
 * The semaphore. The result of passing NULL in this parameter is undefined.
 *
 * @param timeout
 * When to timeout (see dispatch_time). As a convenience, there are the
 * DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
 *
 * @result
 * Returns zero on success, or non-zero if the timeout occurred.
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

dispatch_semaphore_wait這個方法主要用于等待減少信號量,每次調用這個方法,信號量的值都會減一,然后根據減一后的信號量的值的大小,來決定這個方法的使用情況,所以這個方法的使用同樣也分為2種情況:

  1. 當減一后的值小于0時,這個方法會一直等待,即阻塞當前線程,直到信號量+1或者直到超時。
  2. 當減一后的值大于或等于0時,這個方法會直接返回,不會阻塞當前線程。

上面2種方式,放到我們日常的開發中就是下面2種使用情況:

  • 當我們只需要同步線程時,我們可以使用dispatch_semaphore_create(0)初始化信號量為0,然后使用dispatch_semaphore_wait方法讓信號量減一,這時就屬于第一種減一后小于0的情況,這時就會阻塞當前線程,直到另一個線程調用dispatch_semaphore_signal這個讓信號量加1的方法后,當前線程才會被喚醒,然后執行當前線程中的代碼,這時就起到一個線程同步的作用。

  • 當我們需要對資源加鎖,控制同時能訪問資源的最大數量(假設為n)時,我們就需要使用dispatch_semaphore_create(n)方法來初始化信號量為n,然后使用dispatch_semaphore_wait方法將信號量減一,然后訪問我們的資源,然后使用dispatch_semaphore_signal方法將信號量加一。如果有n個線程來訪問這個資源,當這n個資源訪問都還沒有結束時,就會阻塞當前線程,第n+1個線程的訪問就必須等待,直到前n個的某一個的資源訪問結束,這就是我們很常見的資源加鎖的情況。

dispatch_semaphore_signal

/*!
 * @function dispatch_semaphore_signal
 *
 * @abstract
 * Signal (increment) a semaphore.
 *
 * @discussion
 * Increment the counting semaphore. If the previous value was less than zero,
 * this function wakes a waiting thread before returning.
 *
 * @param dsema The counting semaphore.
 * The result of passing NULL in this parameter is undefined.
 *
 * @result
 * This function returns non-zero if a thread is woken. Otherwise, zero is
 * returned.
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphore_signal方法用于讓信號量的值加一,然后直接返回。如果先前信號量的值小于0,那么這個方法還會喚醒先前等待的線程。

semaphore使用篇

線程同步

這種情況在我們的開發中也是挺常見的,當主線程中有一個異步網絡任務,我們需要等這個網絡請求成功拿到數據后,才能繼續做后面的處理,這時我們就可以使用信號量這種方式來進行線程同步。

我們首先看看完整測試代碼:

- (IBAction)threadSyncTask:(UIButton *)sender {
    
    NSLog(@"threadSyncTask start --- thread:%@",[NSThread currentThread]);
    
    //1.創建一個初始值為0的信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    //2.定制一個異步任務
    //開啟一個異步網絡請求
    NSLog(@"開啟一個異步網絡請求");
    NSURLSession *session = [NSURLSession sharedSession];
    NSURL *url =
    [NSURL URLWithString:[@"https://www.baidu.com/" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"GET";
    
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
        }
        if (data) {
            NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            NSLog(@"%@", dict);
        }
        NSLog(@"異步網絡任務完成---%@",[NSThread currentThread]);
        //4.調用signal方法,讓信號量+1,然后喚醒先前被阻塞的線程
        NSLog(@"調用dispatch_semaphore_signal方法");
        dispatch_semaphore_signal(semaphore);
    }];
    [dataTask resume];
    
    //3.調用wait方法讓信號量-1,這時信號量小于0,這個方法會阻塞當前線程,直到信號量等于0時,喚醒當前線程
    NSLog(@"調用dispatch_semaphore_wait方法");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"threadSyncTask end --- thread:%@",[NSThread currentThread]);
}

運行之后的log如下:

2019-04-27 17:24:27.050077+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
2019-04-27 17:24:27.050227+0800 GCD(四) dispatch_semaphore[34482:6102243] 開啟一個異步網絡請求
2019-04-27 17:24:27.050571+0800 GCD(四) dispatch_semaphore[34482:6102243] 調用dispatch_semaphore_wait方法
2019-04-27 17:24:27.105069+0800 GCD(四) dispatch_semaphore[34482:6105851] (null)
2019-04-27 17:24:27.105262+0800 GCD(四) dispatch_semaphore[34482:6105851] 異步網絡任務完成---<NSThread: 0x6000028c6ec0>{number = 6, name = (null)}
2019-04-27 17:24:27.105401+0800 GCD(四) dispatch_semaphore[34482:6105851] 調用dispatch_semaphore_signal方法
2019-04-27 17:24:27.105550+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}

從log中我們可以看出,wait方法會阻塞主線程,直到異步任務完成調用signal方法,才會繼續回到主線程執行后面的任務。

資源加鎖

當一個資源可以被多個線程讀取修改時,就會很容易出現多線程訪問修改數據出現結果不一致甚至崩潰的問題。為了處理這個問題,我們通常使用的辦法,就是使用NSLock@synchronized給這個資源加鎖,讓它在同一時間只允許一個線程訪問資源。其實信號量也可以當做一個鎖來使用,而且比NSLock還有@synchronized代價更低一些,接下來我們來看看它的基本使用

第一步,定義2個宏,將waitsignal方法包起來,方便下面的使用

#ifndef ZED_LOCK
#define ZED_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif

#ifndef ZED_UNLOCK
#define ZED_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif

第二步,聲明與創建共享資源與信號鎖

/* 需要加鎖的資源 **/
@property (nonatomic, strong) NSMutableDictionary *dict;

/* 信號鎖 **/
@property (nonatomic, strong) dispatch_semaphore_t lock;
//創建共享資源
self.dict = [NSMutableDictionary dictionary];
//初始化信號量,設置初始值為1
self.lock = dispatch_semaphore_create(1);

第三步,在即將使用共享資源的地方添加ZED_LOCK宏,進行信號量減一操作,在共享資源使用完成的時候添加ZED_UNLOCK,進行信號量加一操作。

- (IBAction)resourceLockTask:(UIButton *)sender {
    
    NSLog(@"resourceLockTask start --- thread:%@",[NSThread currentThread]);
    
    //使用異步執行并發任務會開辟新的線程的特性,來模擬開辟多個線程訪問貢獻資源的場景
    
    for (int i = 0; i < 3; i++) {
        
        NSLog(@"異步添加任務:%d",i);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            
            ZED_LOCK(self.lock);
            //模擬對共享資源處理的耗時
            [NSThread sleepForTimeInterval:1];
            NSLog(@"i:%d --- thread:%@ --- 將要處理共享資源",i,[NSThread currentThread]);
            [self.dict setObject:@"semaphore" forKey:@"key"];
            NSLog(@"i:%d --- thread:%@ --- 共享資源處理完成",i,[NSThread currentThread]);
            ZED_UNLOCK(self.lock);
            
        });
    }
    
    NSLog(@"resourceLockTask end --- thread:%@",[NSThread currentThread]);
}

在這一步中,我們使用異步執行并發任務會開辟新的線程的特性,來模擬開辟多個線程訪問貢獻資源的場景,同時使用了線程休眠的API來模擬對共享資源處理的耗時。這里我們開辟了3個線程來并發訪問這個共享資源,代碼運行的log如下:

2019-04-27 18:36:25.275060+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask start --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:25.275312+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:0
2019-04-27 18:36:25.275508+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:1
2019-04-27 18:36:25.275680+0800 GCD(四) dispatch_semaphore[35944:6315957] 異步添加任務:2
2019-04-27 18:36:25.275891+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask end --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:26.276757+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:26.277004+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 共享資源處理完成
2019-04-27 18:36:27.282099+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:27.282357+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 共享資源處理完成
2019-04-27 18:36:28.283769+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 將要處理共享資源
2019-04-27 18:36:28.284041+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 共享資源處理完成

從多次log中我們可以看出:

添加信號鎖之后,每個線程對于共享資源的操作都是有序的,并不會出現2個線程同時訪問鎖中的代碼區域。

我把上面的實現代碼簡化一下,方便分析這種鎖的實現原理:

    //step_1
    ZED_LOCK(self.lock);
    //step_2
    NSLog(@"執行任務");
    //step_3
    ZED_UNLOCK(self.lock);
  • 信號量初始化的值為1,當一個線程過來執行step_1的代碼時,會調用信號量的值減一的方法,這時,信號量的值為0,它會直接返回,然后執行step_2的代碼去完成去共享資源的訪問,然后再使用step_3中的signal方法讓信號量加一,信號量的值又會回歸到初始值1。這就是一個線程過來訪問的調用流程。
  • 當線程1過來執行到step_2的時候,這時又有一個線程2它也從step_1處來調用這段代碼,由于線程1已經調用過step_1的wait方法將信號量的值減一,這時信號量的值為0。同時線程2進入然后調用了step_1的wait方法又將信號量的值減一,這時的信號量的值為-1,由于信號量的值小于0時會阻塞當前線程(線程2),所以,線程2就會一直等待,直到線程1執行完step_3中的方法,將信號量加一,才會喚醒線程2,繼續執行下面的代碼。這就是為什么信號量可以對共享資源加鎖的原因,如果我們可以允許n個線程同時訪問,我們就需要在初始化這個信號量時把信號量的值設為n,這樣就限制了訪問共享資源的線程數。

通過上面的分析,我們可以知道,如果我們使用信號量來進行線程同步時,我們需要把信號量的初始值設為0,如果要對資源加鎖,限制同時只有n個線程可以訪問的時候,我們就需要把信號量的初始值設為n。

semaphore的釋放

在我們平常的開發過程中,如果對semaphore使用不當,就會在它釋放的時候遇到奔潰問題。

首先我們來看2個例子:

- (IBAction)crashScene1:(UIButton *)sender {
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    //在使用過程中將semaphore置為nil
    semaphore = nil;
}
- (IBAction)crashScene2:(UIButton *)sender {
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    //在使用過程中對semaphore進行重新賦值
    semaphore = dispatch_semaphore_create(3);
}

我們打開測試代碼,找到semaphore對應的target,然后運行一下代碼,然后點擊后面2個按鈕調用一下上面的代碼,然后我們可以發現,代碼在運行到semaphore = nil;semaphore = dispatch_semaphore_create(3);時奔潰了。然后我們使用lldbbt命令查看一下調用棧。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x0000000111c31309 libdispatch.dylib`_dispatch_semaphore_dispose + 59
    frame #1: 0x0000000111c2fb06 libdispatch.dylib`_dispatch_dispose + 97
  * frame #2: 0x000000010efb113b GCD(四) dispatch_semaphore`-[ZEDDispatchSemaphoreViewController crashScene1:](self=0x00007fdcfdf0add0, _cmd="crashScene1:", sender=0x00007fdcfdd0a3d0) at ZEDDispatchSemaphoreViewController.m:117
    frame #3: 0x0000000113198ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
    frame #4: 0x0000000112bd40bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
    frame #5: 0x0000000112bd43da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
    frame #6: 0x0000000112bd331e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
    frame #7: 0x00000001131d40a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
    frame #8: 0x00000001131d57a0 UIKitCore`-[UIWindow sendEvent:] + 4080
    frame #9: 0x00000001131b3394 UIKitCore`-[UIApplication sendEvent:] + 352
    frame #10: 0x00000001132885a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
    frame #11: 0x000000011328b1cb UIKitCore`__handleEventQueueInternal + 5948
    frame #12: 0x0000000110297721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #13: 0x0000000110296f93 CoreFoundation`__CFRunLoopDoSources0 + 243
    frame #14: 0x000000011029163f CoreFoundation`__CFRunLoopRun + 1263
    frame #15: 0x0000000110290e11 CoreFoundation`CFRunLoopRunSpecific + 625
    frame #16: 0x00000001189281dd GraphicsServices`GSEventRunModal + 62
    frame #17: 0x000000011319781d UIKitCore`UIApplicationMain + 140
    frame #18: 0x000000010efb06a0 GCD(四) dispatch_semaphore`main(argc=1, argv=0x00007ffee0c4efc8) at main.m:14
    frame #19: 0x0000000111ca6575 libdyld.dylib`start + 1
    frame #20: 0x0000000111ca6575 libdyld.dylib`start + 1
(lldb) 

從上面的調用棧我們可以看出,奔潰的地方都處于libdispatch庫調用dispatch_semaphore_dispose方法釋放信號量的時候,為什么在信號量使用過程中對信號量進行重新賦值或置空操作會crash呢,這個我們就需要從GCD的源碼層面來分析了,GCD的源碼庫libdispatch在蘋果的開源代碼庫可以下載,我在自己的Github也放了一份libdispatch-187.10版本的,下面的源碼分析都是基于這個版本的。

首先我們來看一下dispatch_semaphore_t的結構體dispatch_semaphore_s的結構體定義

struct dispatch_semaphore_s {
    DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
    long dsema_value; //當前的信號值
    long dsema_orig;  //初始化的信號值
    size_t dsema_sent_ksignals;
#if USE_MACH_SEM && USE_POSIX_SEM
#error "Too many supported semaphore types"
#elif USE_MACH_SEM
    semaphore_t dsema_port; //當前mach_port_t信號
    semaphore_t dsema_waiter_port; //休眠時mach_port_t信號
#elif USE_POSIX_SEM
    sem_t dsema_sem;
#else
#error "No supported semaphore type"
#endif
    size_t dsema_group_waiters;
    struct dispatch_sema_notify_s *dsema_notify_head;//鏈表頭部
    struct dispatch_sema_notify_s *dsema_notify_tail;//鏈表尾部
};

這里我們需要關注2個值的變化,dsema_valuedsema_orig,它們分別代表當前的信號值與初始化時的信號值。

當我們調用dispatch_semaphore_create方法創建信號量時,這個方法內部會把傳入的參數存儲到dsema_value(當前的value)和dsema_orig(初始value)中,條件是value的值必須大于或等于0。

dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
    dispatch_semaphore_t dsema;

    // If the internal value is negative, then the absolute of the value is
    // equal to the number of waiting threads. Therefore it is bogus to
    // initialize the semaphore with a negative value.
    if (value < 0) {//初始值不能小于0
        return NULL;
    }

    dsema = calloc(1, sizeof(struct dispatch_semaphore_s));//申請信號量的內存

    if (fastpath(dsema)) {//信號量初始化賦值
        dsema->do_vtable = &_dispatch_semaphore_vtable;
        dsema->do_next = DISPATCH_OBJECT_LISTLESS;
        dsema->do_ref_cnt = 1;
        dsema->do_xref_cnt = 1;
        dsema->do_targetq = dispatch_get_global_queue(
                DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dsema->dsema_value = value;//當前的值
        dsema->dsema_orig = value;//初始值
#if USE_POSIX_SEM
        int ret = sem_init(&dsema->dsema_sem, 0, 0);//內存空間映射
        DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
    }

    return dsema;
}

然后調用dispatch_semaphore_waitdispatch_semaphore_signal時會對dsema_value做加一或減一操作。當我們對信號量置空或者重新賦值操作時,會調用dispatch_semaphore_dispose釋放信號量,我們來看看對應的源碼

static void
_dispatch_semaphore_dispose(dispatch_semaphore_t dsema)
{
    if (dsema->dsema_value < dsema->dsema_orig) {//當前的信號值如果小于初始值就會crash
        DISPATCH_CLIENT_CRASH(
                "Semaphore/group object deallocated while in use");
    }

#if USE_MACH_SEM
    kern_return_t kr;
    if (dsema->dsema_port) {
        kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
    if (dsema->dsema_waiter_port) {
        kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
#elif USE_POSIX_SEM
    int ret = sem_destroy(&dsema->dsema_sem);
    DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif

    _dispatch_dispose(dsema);
}

從源碼中我們可以看出,當dsema_value小于dsema_orig時,即信號量還在使用時,會直接調用DISPATCH_CLIENT_CRASH讓APP奔潰。

所以,我們在使用信號量的時候,不能在它還在使用的時候,進行賦值或者置空的操作。

如果文中有錯誤的地方,或者與你的想法相悖的地方,請在評論區告知我,我會繼續改進,如果你覺得這個篇文章總結的還不錯,麻煩動動小手,給我的文章與Git代碼樣例點個?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容

  • iOS GCD (一) 任務+隊列 基礎組合iOS GCD (二 ) dispatch_group 隊列組iO...
    奔跑吧小螞蟻閱讀 8,986評論 0 34
  • 1.NSTimer不準時的原因:(1).RunLoop循環處理時間,每次循環是固定時間,只有在這段時間才會去查看N...
    稻春閱讀 1,261評論 0 3
  • 很久前的總結,今天貼出來。適合看了就用,很少講解,純粹用法。 目錄 Dispatch Queue dispatch...
    和女神經常玩閱讀 685評論 0 3
  • 作者:張暄敏 那是我上小學四年級時,發生的一件事,那個老太太的慈祥的笑容,一直到今天,仍舊像一...
    為理想追夢閱讀 411評論 0 0
  • 輕輕的,他走了。向過去的親人揮揮手,不帶走一片云彩,一絲遺憾。 第一次看見他的面龐,熟悉中透露著陌生,他神情木然,...
    廠習十閱讀 429評論 0 3