細(xì)說@synchronized和dispatch_once

工欲善其事,必先利其器妄痪。

通常我們在實(shí)現(xiàn)單例時(shí)候都會(huì)使用synchronized或者dispatch_once方法臣疑,初始化往往是下面的樣子:
使用synchronized方法實(shí)現(xiàn):

static id obj = nil;
+(instancetype)shareInstance
{
    @synchronized(self) {
        if (!obj) {
            obj = [[SingletonObj alloc] init];
        }
    }
    return obj;
}

使用dispatch_once方法實(shí)現(xiàn):

static id obj = nil;
+(instancetype)shareInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        obj = [[SingletonObj alloc] init];
    });
    return obj;
}
性能差異

上面的這些寫法大家應(yīng)該都很熟悉硬爆,既然兩種方式都能實(shí)現(xiàn)呻拌,我們來看看兩者的性能差異膀藐,這里簡單寫了個(gè)測試的demo萌朱,使用兩個(gè)方法分單線程跟多線程(采用dispatch_apply方式宴树,性能相對較高)去訪問一個(gè)單例對象一百萬次,對比這期間的耗時(shí)晶疼,從iPod跟5s測試得到如下的結(jié)果

    //ipod酒贬,主線程
    SingletonTest[4285:446820] synchronized time cost:2.202945s
    SingletonTest[4285:446820] dispatch_once time cost:0.761034s
    
    //5s,主線程
    SingletonTest[5372:2394430] synchronized time cost:0.466293s
    SingletonTest[5372:2394430] dispatch_once time cost:0.070822s

    //ipod翠霍,多線程
    SingletonTest[4315:448499] synchronized time cost:3.385109s
    SingletonTest[4315:448499] dispatch_once time cost:0.908009s
    
    //5s锭吨,多線程
    SingletonTest[5391:2399069] synchronized time cost:0.507504s
    SingletonTest[5391:2399069] dispatch_once time cost:0.169934s

可以發(fā)現(xiàn)dispatch_once方法的性能要明顯優(yōu)于synchronized方法(多線程不采用dispathc_apply方式差距更明顯),所以在實(shí)際的應(yīng)用中我們可以多采用dispatch_once方式來實(shí)現(xiàn)單例寒匙。通常使用的時(shí)候了解這些就夠了零如,不過想知道兩者的具體差異就需要我們再邁進(jìn)一步。

深入@synchronized(object)

翻看蘋果的文檔可以發(fā)現(xiàn) @synchronized指令內(nèi)部使用鎖來實(shí)現(xiàn)多線程的安全訪問锄弱,并且隱式添加了一個(gè)異常處理的handler考蕾,當(dāng)異常發(fā)生時(shí)會(huì)自動(dòng)釋放鎖。在stackoverflow上看到@synchronized指令其實(shí)可以轉(zhuǎn)換成objc_sync_enter跟objc_sync_exit会宪,可以在<objc/objc-sync.h>頭文件中找到這兩個(gè)函數(shù):

//Allocates recursive pthread_mutex associated with 'obj' if needed
int objc_sync_enter(id obj)

//End synchronizing on 'obj'
int objc_sync_exit(id obj)

根據(jù)注釋文檔肖卧,objc_sync_enter會(huì)根據(jù)需要給每個(gè)傳進(jìn)來的對象創(chuàng)建一個(gè)互斥鎖并lock,然后objc_sync_exit的時(shí)候unlock狈谊,這樣就可以通過這個(gè)鎖來實(shí)現(xiàn)多線程的安全訪問喜命,所以結(jié)合蘋果文檔可以認(rèn)為

@synchronized(self) {
    //thread safe code
}

等價(jià)于

@try {
    objc_sync_enter(self);
    // thread safe code
} @finally {
    objc_sync_exit(self);    
}

慶幸的是蘋果已經(jīng)將objc-runtime這部分開源沟沙,所以我們可以更進(jìn)一步了解內(nèi)部的實(shí)現(xiàn)河劝,源碼在這里,有興趣也可以自己去查閱矛紫,這里簡單介紹一下赎瞎。
讓我們先來看看幾個(gè)數(shù)據(jù)結(jié)構(gòu),其中有些涉及到緩存颊咬,我們就不去考慮了:

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

struct SyncList {
    SyncData *data;
    spinlock_t lock;
};

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

首先看看SyncData這個(gè)數(shù)據(jù)結(jié)構(gòu)务甥,包含一個(gè)指向object的指針,這個(gè)object對象就是我們@synchronized時(shí)傳進(jìn)來的對象喳篇,也包含一個(gè)跟object關(guān)聯(lián)的遞歸互斥鎖recursive_mutex_t敞临,該鎖用來互斥訪問object對象;同時(shí)還包含一個(gè)指向下一個(gè)SyncData的指針nextData麸澜,可以看出SyncData是一個(gè)鏈表中的節(jié)點(diǎn)挺尿;至于threadCount,這個(gè)值標(biāo)示有幾個(gè)線程正在訪問這個(gè)對象,當(dāng)threadCount==0的時(shí)候编矾,會(huì)重用該SyncData對象熟史,這是為了節(jié)省內(nèi)存。
??接下來看看SyncList窄俏,SyncList其實(shí)就是一個(gè)鏈表蹂匹,data指向第一個(gè)SyncData節(jié)點(diǎn),lock則是為了多線程安全訪問該鏈表凹蜈。
??最后看下sDataLists靜態(tài)哈希表對象限寞,它以obj的指針為key,對應(yīng)的value為SyncList鏈表。
??了解上面之后仰坦,我們就可以看看objc_sync_enter跟objc_sync_exit的具體實(shí)現(xiàn)(摘取部分代碼)


//根據(jù)object對象去查詢相應(yīng)的SyncData對象昆烁,如果沒有則創(chuàng)建一個(gè)新的
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    //lock,多線程安全訪問SyncList
    lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            //找到object對象對應(yīng)的SyncData對象缎岗,增加其threadCount計(jì)數(shù)静尼,然后返回
            if ( p->object == object ) {
                result = p;
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //當(dāng)threadCount == 0時(shí),設(shè)置當(dāng)前SyncData為可重用
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
        // 如果有可重用的節(jié)點(diǎn)传泊,則使用當(dāng)前SyncData節(jié)點(diǎn)鼠渺,SyncData的object指針指向新的object對象
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    //如果沒有可重用的節(jié)點(diǎn),則創(chuàng)建一個(gè)新的SyncData節(jié)點(diǎn)
    result = (SyncData*)calloc(sizeof(SyncData), 1);

    //將新的SyncData節(jié)點(diǎn)的object指針指向傳進(jìn)來的object對象
    result->object = (objc_object *)object;
    result->threadCount = 1;

    //創(chuàng)建一個(gè)新的與該object關(guān)聯(lián)的遞歸互斥鎖
    new (&result->mutex) recursive_mutex_t();
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    return result;
}

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        //根據(jù)obj指針的哈希值查找對應(yīng)的SyncData,threadcount計(jì)數(shù)加一
        SyncData* data = id2data(obj, ACQUIRE);

        //使用SyncData的互斥鎖上鎖
        data->mutex.lock();
    } else {
        // @synchronized(nil) 傳入nil時(shí)什么也不處理
    }
    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        //根據(jù)obj指針的哈希值查找對應(yīng)的SyncData眷细,threadcount計(jì)數(shù)減一
        SyncData* data = id2data(obj, RELEASE);

        //使用SyncData的互斥鎖解鎖 
        bool okay = data->mutex.tryUnlock();
        if (!okay) {
           result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        }
    } else {
        // @synchronized(nil) 傳入nil時(shí)什么也不處理
    }
    return result;
}

簡單來說拦盹,調(diào)用objc_sync_enter(obj)時(shí),會(huì)根據(jù)obj指針在哈希表sDataLists對應(yīng)的鏈表SyncList溪椎,然后在鏈表中查詢對應(yīng)obj的SyncData對象普舆,如果查詢不到則創(chuàng)建一個(gè)新的SyncData對象(包含創(chuàng)建跟obj相關(guān)的遞歸互斥鎖)并添加到鏈表中,然后使用SyncData對象上鎖校读;調(diào)用objc_sync_exit(obj)時(shí)沼侣,使用SyncData對象解鎖,因此通過這個(gè)鎖便可確保@synchronized之間的代碼線程安全歉秫。

sDataLists
深入dispatch_once

探討了synchronized之后蛾洛,我們再來說說dispatch_once。

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

根據(jù)官方文檔,dispatch_once可以用來初始化一些全局的數(shù)據(jù)雁芙,它能夠確保block代碼在app的生命周期內(nèi)僅被運(yùn)行一次轧膘,而且還是線程安全的,不需要額外加鎖兔甘;predicate必須指向一個(gè)全局或者靜態(tài)的變量谎碍,不過使用predicate的話結(jié)果是未定義的,不過predicate有啥作用洞焙,如何實(shí)現(xiàn)block在整個(gè)生命周期執(zhí)行一次蟆淀?那我們只能從源碼查找(源碼地址:once)太援。
不過在這之前先簡要介紹一下:

  • bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
    提供原子的比較和交換操作,如果當(dāng)前值 *ptr == oldval扳碍,就將newval寫入ptr提岔,當(dāng)比較賦值操作成功后返回true

  • *__sync_synchronize (...)
    調(diào)用這個(gè)函數(shù)會(huì)產(chǎn)生一個(gè)full memory barrier ,用于保證CPU按照我們代碼編寫的順序來執(zhí)行代碼笋敞,比如:

doJob1();
 doJob2();
 __sync_synchronize();  //Job3會(huì)在Job1跟Job2完成后才執(zhí)行
doJob3();
  • type __sync_swap(type *ptr, type value, ...)
    提供原子交換操作的函數(shù)碱蒙,交換第一個(gè)跟第二個(gè)參數(shù)的值,然后返回交換前第一個(gè)參數(shù)的舊值夯巷。
  • _dispatch_hardware_pause()
    調(diào)用這個(gè)函數(shù)主要是暗示處理器不要做額外的優(yōu)化處理等赛惩,提高性能,節(jié)省CPU時(shí)間趁餐,可以查看這里了解更多
  • 信號(hào)量
    信號(hào)量是一個(gè)非負(fù)整數(shù)喷兼,定義了兩種原子操作:wait跟signal來進(jìn)行訪,信號(hào)量主要用于線程同步后雷。當(dāng)一個(gè)線程調(diào)用wait操作時(shí)季惯,如果信號(hào)量的值大于0,則獲得資源并將信號(hào)量值減一臀突,如果等于0線程睡眠直到信號(hào)量值大于0或者超時(shí)勉抓;singal將信號(hào)量的值加1,如果這時(shí)候有正在等待的線程候学,喚醒該線程藕筋。
// 創(chuàng)建一個(gè)信號(hào)量,其值為0        
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
    //操作完成后梳码,調(diào)用signal信號(hào)量+1
    dispatch_semaphore_signal(sema);
});
//等待dispatch_semaphore_signal將信號(hào)量值加1后才繼續(xù)運(yùn)行
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

接下來看看具體代碼隐圾,當(dāng)我們調(diào)用dispatch_once時(shí)候,內(nèi)部是調(diào)用dispatch_once_f函數(shù)掰茶,其中val就是外部傳入的predicate值暇藏,ctxt為Block的指針,func則是Block內(nèi)部具體實(shí)現(xiàn)的函數(shù)指針符匾,由于源碼比較短叨咖,所以我直接把源碼貼出來(為了方便查看瘩例,有些不使用宏定義)啊胶。

struct _dispatch_once_waiter_s {
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    _dispatch_thread_semaphore_t dow_sema;
};
#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)
void dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
    struct Block_basic *bb = (void *)block;
    dispatch_once_f(val, block, (void *)bb->Block_invoke);
}

void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    //volatile,標(biāo)示該變量隨時(shí)可能改變垛贤,編譯器不會(huì)對訪問該變量的代碼進(jìn)行優(yōu)化焰坪,每次都從內(nèi)存去讀取,而不使用寄存器里的值
    struct _dispatch_once_waiter_s * volatile *vval =
            (struct _dispatch_once_waiter_s**)val;
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;

    //第一次執(zhí)行的時(shí)候聘惦,predicate的值為0某饰,所以vval=NULL,原子比較交換函數(shù)返回true
    //然后vval指向dow(dispatch_once_waiter_s,信號(hào)量的值為0黔漂,即等待中)
    if (__sync_bool_compare_and_swap(vval, NULL, &dow)) {

        //空的宏定義诫尽,啥也不做
        dispatch_atomic_acquire_barrier();

        //執(zhí)行dispatch_once傳進(jìn)來的block
        _dispatch_client_callout(ctxt, func);
        
        //后面解釋
        dispatch_atomic_maximally_synchronizing_barrier();
        
        //執(zhí)行完block之后,將vval的值設(shè)為DISPATCH_ONCE_DONE(即predicate設(shè)為~0l)
        tmp = __sync_swap(vval, DISPATCH_ONCE_DONE);  
        tail = &dow;

        //1.如果在block的執(zhí)行過程中炬守,沒有其線程調(diào)用該函數(shù)等待牧嫉,tmp的值也為&dow,tail==tmp,循環(huán)的條件不滿足减途,函數(shù)執(zhí)行完畢
        //2.如果在block的執(zhí)行過程中酣藻,有其線程調(diào)用該函數(shù)等待,歷遍信號(hào)量鏈表鳍置,逐個(gè)喚醒線程繼續(xù)運(yùn)行
        while (tail != tmp) {
            //如果中途有其它線程將vval賦值&dow辽剧,這期間dow_next值為NULL,需要等待税产,參見else分支的__sync_bool_compare_and_swap調(diào)用
            while (!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    }
    else 
    {   
        //如果vval不等NULL怕轿,走這個(gè)分支,非第一次調(diào)用dispatch_once辟拷,其它線程調(diào)用
        //獲取信號(hào)量撤卢,如果有信號(hào)量則返回該信號(hào)量,如果沒有則在當(dāng)前線程創(chuàng)建一個(gè)新的信號(hào)量
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for (;;) {
            tmp = *vval;

            //vval已經(jīng)被賦值為~0l梧兼,證明block已經(jīng)被執(zhí)行了放吩,退出然后調(diào)用_dispatch_put_thread_semaphore銷毀信號(hào)量
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            //空的宏定義,啥也不做
            dispatch_atomic_store_barrier();

            //將當(dāng)前信號(hào)量加入到信號(hào)鏈表中羽杰,然后線程等待渡紫,
            if (__sync_bool_compare_and_swap(vval, tmp, &dow)) {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }

            //如果vval的指向值不再是tmp,可能其它線程同時(shí)進(jìn)入該分支考赛,然后調(diào)用__sync_bool_compare_and_swap原子操作將vval指向了新的節(jié)點(diǎn)惕澎,
            //則重新開始for循環(huán)
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}

讓我們來看看dispatch_once是如何確保block只執(zhí)行一次。簡單來說颜骤,當(dāng)線程A在調(diào)用執(zhí)行block并設(shè)置predicate為DISPATCH_ONCE_DONE(~0l)期間唧喉,如果有其他線程也在調(diào)用disptach_once,則這些線程會(huì)等待忍抽,各線程對應(yīng)的信號(hào)量會(huì)加入到信號(hào)量鏈表中八孝,等predicate設(shè)置為DISPATCH_ONCE_DONE后,也就是block執(zhí)行完了鸠项,會(huì)根據(jù)信號(hào)量鏈表喚醒各個(gè)線程使其繼續(xù)執(zhí)行干跛。


信號(hào)量鏈表.png

??不過有一種臨界情況,假如線程A在執(zhí)行block祟绊,但是創(chuàng)建單例對象obj還未完成楼入,這時(shí)候線程B獲取該obj對象哥捕,此時(shí)obj=nil,而線程B在線程A將predicate設(shè)為DISPATCH_ONCE_DONE之后讀取predicate嘉熊,這是線程B會(huì)認(rèn)為單例對象已經(jīng)初始化完成遥赚,然后使用空的obj對象,這就會(huì)導(dǎo)致錯(cuò)誤發(fā)生阐肤。因此dispatch_once會(huì)在執(zhí)行完block之后會(huì)執(zhí)行dispatch_atomic_maximally_synchronizing_barrier()調(diào)用鸽捻,這個(gè)調(diào)用會(huì)執(zhí)行一些cpuid指令,確保線程A創(chuàng)建單例對象obj以及置predicate為DISPATCH_ONCE_DONE的時(shí)間TimeA大于線程B進(jìn)入block并讀取predicate值的時(shí)間TimeB泽腮。

#define dispatch_atomic_maximally_synchronizing_barrier() \
    do { unsigned long _clbr; __asm__ __volatile__( \
    "cpuid" \
    : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory" \
    ); } while(0)

除此之外御蒲,每次調(diào)用dispatch_once的時(shí)候,都會(huì)先判斷predicate的值是否是~0l(也就是DISPATCH_ONCE_DONE)诊赊,如果是則意味著block已經(jīng)執(zhí)行過了厚满,便不再執(zhí)行,代碼如下:

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
#ifdef __GNUC__
#define dispatch_once(x, ...) do { if (__builtin_expect(*(x), ~0l) != ~0l) dispatch_once((x), (__VA_ARGS__)); } while (0)
#endif

讓我們看看這里面的__builtin_expect((x), (v))碧磅,這又是一個(gè)優(yōu)化的地方碘箍。。。

__builtin_expect()目的是將“分支轉(zhuǎn)移”的信息提供給編譯器,這樣編譯器可以對代碼進(jìn)行優(yōu)化诫咱,
以減少指令跳轉(zhuǎn)帶來的性能下降柳畔。
__builtin_expect((x),1) 表示 x 的值為真的可能性更大绢彤; 
__builtin_expect((x),0) 表示 x 的值為假的可能性更大。  

由于dispatch_once的只執(zhí)行block一次,所以我們更期望的是已經(jīng)block已經(jīng)執(zhí)行完了,也就是predict的值為~0l的可能性更大盗蟆。
??現(xiàn)在我們清楚dispatch_once是如何確保block只執(zhí)行一次了,關(guān)鍵就在predict這個(gè)值舒裤,通過比較這個(gè)值等于0或者~0l來判斷block是否執(zhí)行過喳资,這也就是為啥我們需要將這個(gè)值設(shè)為static或者全局的緣故,因?yàn)楦鱾€(gè)線程都要去訪問這個(gè)predict腾供,有興趣的可以試試把predicate的初始值設(shè)為非0或者非靜態(tài)全局變量會(huì)發(fā)生什么~~

總結(jié)

通過上面的分析仆邓,我們知道@synchronized采用的是遞歸互斥鎖來實(shí)現(xiàn)線程安全,而dispatch_once的內(nèi)部則使用了很多原子操作來替代鎖伴鳖,以及通過信號(hào)量來實(shí)現(xiàn)線程同步节值,而且有很多針對處理器優(yōu)化的地方,甚至在if判斷語句上也做了優(yōu)化(逼格有點(diǎn)高)黎侈,使得其效率有很大的提升察署,雖然其源碼很短,但里面包含的東西卻很多峻汉,所以蘋果也推薦使用dispatch_once來創(chuàng)建單例贴汪。通過這個(gè)簡短的dispatch_once,你也可以清楚為什么GCD的性能會(huì)這么高了休吠,感興趣可以再去看看libdispatch的其它源碼扳埂。。

參考

objc-sync
synchronized
dispatch_once
Built-in functions for atomic memory access
__builtin_expect

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瘤礁,一起剝皮案震驚了整個(gè)濱河市阳懂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌柜思,老刑警劉巖岩调,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赡盘,居然都是意外死亡号枕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門陨享,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葱淳,“玉大人,你說我怎么就攤上這事抛姑≡薏蓿” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵定硝,是天一觀的道長皿桑。 經(jīng)常有香客問我,道長蔬啡,這世上最難降的妖魔是什么唁毒? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮星爪,結(jié)果婚禮上浆西,老公的妹妹穿的比我還像新娘。我一直安慰自己顽腾,他們只是感情好近零,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抄肖,像睡著了一般久信。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上漓摩,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天裙士,我揣著相機(jī)與錄音,去河邊找鬼管毙。 笑死腿椎,一個(gè)胖子當(dāng)著我的面吹牛桌硫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播啃炸,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼铆隘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了南用?” 一聲冷哼從身側(cè)響起膀钠,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎裹虫,沒想到半個(gè)月后肿嘲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡筑公,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年雳窟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片十酣。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涩拙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耸采,到底是詐尸還是另有隱情兴泥,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布虾宇,位于F島的核電站搓彻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嘱朽。R本人自食惡果不足惜旭贬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搪泳。 院中可真熱鬧稀轨,春花似錦、人聲如沸岸军。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艰赞。三九已至佣谐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間方妖,已是汗流浹背狭魂。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雌澄。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓斋泄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掷伙。 傳聞我的和親對象是個(gè)殘疾皇子是己,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容