「Go」- golang源碼分析 - channel的底層實(shí)現(xiàn)

路徑為:./src/runtime/chan.go 文件中生真,先看channel結(jié)構(gòu)體:

type hchan struct {
    qcount   uint           // total data in the queue 當(dāng)前隊(duì)列中的數(shù)據(jù)的個(gè)數(shù)
    dataqsiz uint           // size of the circular queue   channel環(huán)形隊(duì)列的大小
    buf      unsafe.Pointer // points to an array of dataqsiz elements  存放數(shù)據(jù)的環(huán)形隊(duì)列的指針
    elemsize uint16     // channel 中存放的數(shù)據(jù)類(lèi)型的大小|即每個(gè)元素的大小
    closed   uint32     // channel 是否關(guān)閉的標(biāo)示
    elemtype *_type // element type channel中存放的元素的類(lèi)型
    sendx    uint   // send index   當(dāng)前發(fā)送元素指向channel環(huán)形隊(duì)列的下標(biāo)指針
    recvx    uint   // receive index 當(dāng)前接收元素指向channel環(huán)形隊(duì)列的下標(biāo)指針
    recvq    waitq  // list of recv waiters 等待接收元素的goroutine隊(duì)列
    sendq    waitq  // list of send waiters  等待發(fā)送元素的goroutine隊(duì)列

    // lock protects all fields in hchan, as well as several
    // fields in sudogs blocked on this channel.
    //
    // Do not change another G's status while holding this lock
    // (in particular, do not ready a G), as this can deadlock
    // with stack shrinking.
    // 保持此鎖定時(shí)不要更改另一個(gè)G的狀態(tài)(特別是脖咐,沒(méi)有準(zhǔn)備好G),因?yàn)檫@可能會(huì)因堆棧收縮而死鎖汇歹。
    lock mutex
}

以及waitq的結(jié)構(gòu)體:

//等待發(fā)送及接收的等待接收元素的goroutine隊(duì)列的結(jié)構(gòu)體
type waitq struct {
    first *sudog
    last  *sudog
}

等待發(fā)送或接受goroutine鏈表的結(jié)構(gòu)體sudog:

// sudog表示等待鏈表中的g屁擅,例如用于發(fā)送/接收在頻道上。
// 一個(gè)G可以出現(xiàn)在許多等待列表中产弹,因此一個(gè)G有許多sudog派歌;許多G可能在等待相同的結(jié)果,同步對(duì)象弯囊,因此一個(gè)對(duì)象可能有多個(gè)sudog。
// sudog是從一個(gè)特殊的池中分配的胶果。使用AcquireDog和
// 釋放sudog來(lái)分配和釋放它們匾嘱。

type sudog struct {
    // 以下字段受hchan.lock的保護(hù)
    g *g // 綁定的goroutine
    isSelect bool   // isSelect的布爾值表示該線程是否正在進(jìn)行操作channel
    next     *sudog // 指向下一個(gè)等待線程的指針地址
    prev     *sudog // 指向上一個(gè)等待線程的指針地址
    elem     unsafe.Pointer // data element (may point to stack) 數(shù)據(jù)對(duì)象(可能指向棧)
    // 當(dāng)進(jìn)行channel的send操作時(shí),elem代表將要保存進(jìn)channel的元素
    // 當(dāng)進(jìn)行channel的recv操作時(shí), elem代表從channel接受的元素
    // G1執(zhí)行ch<-task4的時(shí)候早抠,G1會(huì)創(chuàng)建一個(gè)sudog然后將elem保存進(jìn)入sendq隊(duì)列

    // 從不同場(chǎng)景訪問(wèn)以下字段霎烙。
    // 對(duì)于channel,WaitLink只能由G訪問(wèn)蕊连。
    // 對(duì)于信號(hào)量悬垃,所有字段(包括上面的字段)只有在持有semaroot鎖時(shí)才能訪問(wèn)。
    acquiretime int64 // 獲取時(shí)間
    releasetime int64 // 釋放時(shí)間
    ticket      uint32
    parent      *sudog // semaRoot binary tree
    waitlink    *sudog // g.waiting list or semaRoot
    waittail    *sudog // semaRoot
    c           *hchan // channel // 綁定channel
}

從以上三個(gè)結(jié)構(gòu)體我們即可看出channel其實(shí)就是由一個(gè)環(huán)形數(shù)組實(shí)現(xiàn)的隊(duì)列用于在確定大小的連續(xù)內(nèi)存塊進(jìn)行數(shù)據(jù)元素的存儲(chǔ)甘苍,用waitq以及鏈表sudog共同實(shí)現(xiàn)goroutine的等待隊(duì)列尝蠕,并在每個(gè)鏈表元素中存儲(chǔ)待從channel中取出或拷貝進(jìn)channel的數(shù)據(jù)元素,可以理解為每個(gè)等待線程都是channel的搬運(yùn)工,負(fù)責(zé)運(yùn)送數(shù)據(jù).
其中hchan中的lock是 recvq 是讀操作阻塞在 channel 的 goroutine 列表载庭,sendq 是寫(xiě)操作阻塞在 channel 的 goroutine 列表看彼。
qcount 和 dataqsiz 分別描述了該channel的當(dāng)前使用量和最大容量。

接下來(lái)進(jìn)行channel的每一個(gè)函數(shù)方法進(jìn)行分析:

makechan:

func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    // 判斷定義的channel存儲(chǔ)的每個(gè)元素大小是否在范圍內(nèi)
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }

    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    // 計(jì)算channel所需要分配的內(nèi)存大小
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    // 判斷內(nèi)存大小是否超過(guò)限制
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

    var c *hchan
    switch {
    // 當(dāng)計(jì)算channel的內(nèi)存大小為0時(shí)創(chuàng)建不帶buffer的channel
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    // elem類(lèi)型非指針
    // 當(dāng)計(jì)算channel的內(nèi)存大小為0時(shí)創(chuàng)建帶buffer的channel
    // 分配連續(xù)的內(nèi)存 (連續(xù)內(nèi)存有利于提高內(nèi)存使用效率)
    // 直接從棧中分配內(nèi)存
    case elem.kind&kindNoPointers != 0:
        // 分配內(nèi)存
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    // 當(dāng)channel元素類(lèi)型包含指針時(shí)分配離散的內(nèi)存
    default:
        // Elements contain pointers.
        c = new(hchan)
        // 分配內(nèi)存
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

    if debugChan {
        print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "\n")
    }
    return c
}

函數(shù)接收兩個(gè)參數(shù)囚聚,一個(gè)是channel里面保存的元素的數(shù)據(jù)類(lèi)型靖榕,一個(gè)是緩沖的容量(如果為0表示是非緩沖buffer),創(chuàng)建流程如下:

根據(jù)傳遞的緩沖大小size是否為零顽铸,分別創(chuàng)建不帶buffer的channel或則帶size大小的緩沖channel:
對(duì)于不帶緩沖channel茁计,申請(qǐng)一個(gè)hchan數(shù)據(jù)結(jié)構(gòu)的內(nèi)存大小跋破;
對(duì)于帶緩沖channel,new一個(gè)hchan對(duì)象瓶蝴,并初始化buffer內(nèi)存毒返;
對(duì)于包含指針帶緩存的channel同樣申請(qǐng)一個(gè)hchan數(shù)據(jù)結(jié)構(gòu)的內(nèi)存大小;
以及設(shè)置channel的屬性。
帶指針以及不帶指針帶內(nèi)存申請(qǐng)區(qū)別可以看內(nèi)存管理相關(guān)源碼舷手。

chanbuf:

 //chanbuf(c, i) is pointer to the i'th slot in the buffer.
func chanbuf(c *hchan, i uint) unsafe.Pointer {
    return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

chanbuf的實(shí)現(xiàn)很簡(jiǎn)單拧簸,主要就是根據(jù)下標(biāo)(sendx或recvx)以及每一個(gè)元素的大小還有環(huán)形隊(duì)列的指針計(jì)算出該下標(biāo)槽點(diǎn)內(nèi)存地址并返回

chansend:


// 通用單通道發(fā)送/接收
// 如果阻塞不是nil,則將不會(huì)休眠男窟,但如果無(wú)法完成則返回盆赤。
// 當(dāng)睡眠中涉及的通道關(guān)閉時(shí),睡眠可以通過(guò)g.param == nil喚醒歉眷。 最簡(jiǎn)單的循環(huán)和重新運(yùn)行操作; 我們會(huì) 
// 看到它現(xiàn)在已經(jīng)關(guān)閉了牺六。   

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {

    // 當(dāng) channel 未初始化或?yàn)?nil 時(shí),向其中發(fā)送數(shù)據(jù)將會(huì)永久阻塞
    if c == nil {
        if !block {
            return false
        }
        // gopark 會(huì)使當(dāng)前 goroutine 休眠汗捡,并通過(guò) unlockf 喚醒淑际,但是此時(shí)傳入的 unlockf 為 nil, 因此,goroutine 會(huì)一直休眠
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    if debugChan {
        print("chansend: chan=", c, "\n")
    }

    // 如果開(kāi)啟了競(jìng)爭(zhēng)檢測(cè)
    if raceenabled {
        racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
    }

    // Fast path: check for failed non-blocking operation without acquiring the lock.
    //
    // After observing that the channel is not closed, we observe that the channel is
    // not ready for sending. Each of these observations is a single word-sized read
    // (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
    // Because a closed channel cannot transition from 'ready for sending' to
    // 'not ready for sending', even if the channel is closed between the two observations,
    // they imply a moment between the two when the channel was both not yet closed
    // and not ready for sending. We behave as if we observed the channel at that moment,
    // and report that the send cannot proceed.
    //
    // It is okay if the reads are reordered here: if we observe that the channel is not
    // ready for sending and then observe that it is not closed, that implies that the
    // channel wasn't closed during the first observation.
    if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
        (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
        return false
    }

    var t0 int64
    //計(jì)時(shí)器
    if blockprofilerate > 0 {
        t0 = cputicks()
    }

    // 獲取同步鎖
    lock(&c.lock)


    // 向已經(jīng)關(guān)閉的 channel 發(fā)送消息會(huì)產(chǎn)生 panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

    // CASE1: 當(dāng)有 goroutine 在 recv 隊(duì)列上等待時(shí),跳過(guò)緩存隊(duì)列春缕,將消息直接發(fā)給 reciever goroutine
    // dequeue 從等待接受的線程隊(duì)列鏈表獲取一個(gè)sudog
    if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

     // CASE2: 緩存隊(duì)列未滿盗胀,則將消息復(fù)制到緩存隊(duì)列上并移動(dòng)sendx下標(biāo)
    if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }

    if !block {
        unlock(&c.lock)
        return false
    }
     // CASE3: 緩存隊(duì)列已滿,將goroutine 加入 send 隊(duì)列
     // 創(chuàng)建 sudo
    // Block on the channel. Some receiver will complete our operation for us.
    //獲取當(dāng)前線程并綁定到sudog
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    // 講當(dāng)前sudog放入等待發(fā)送的線程隊(duì)列
    c.sendq.enqueue(mysg)
    // 休眠線程(即阻塞)
    // 通過(guò)調(diào)用goready(gp)锄贼,goroutine可以再次運(yùn)行票灰。
    goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
    // Ensure the value being sent is kept alive until the
    // receiver copies it out. The sudog has a pointer to the
    // stack object, but sudogs aren't considered as roots of the
    // stack tracer.
    KeepAlive(ep)

    // someone woke us up.
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if gp.param == nil {
        if c.closed == 0 {
            throw("chansend: spurious wakeup")
        }
        panic(plainError("send on closed channel"))
    }
    gp.param = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    mysg.c = nil

    //釋放sudog
    releaseSudog(mysg)
    return true
}

// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked.  send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if raceenabled {
        if c.dataqsiz == 0 {
            racesync(c, sg)
        } else {
            // Pretend we go through the buffer, even though
            // we copy directly. Note that we need to increment
            // the head/tail locations only when raceenabled.
            qp := chanbuf(c, c.recvx)
            raceacquire(qp)
            racerelease(qp)
            raceacquireg(sg.g, qp)
            racereleaseg(sg.g, qp)
            c.recvx++
            if c.recvx == c.dataqsiz {
                c.recvx = 0
            }
            c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
        }
    }
    if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

send 有以下四種情況:【都是對(duì)不為nil的chan的情況】
1.向已經(jīng)close的chan寫(xiě)數(shù)據(jù),拋panic宅荤。

2.有 goroutine 阻塞在 channel recv 隊(duì)列上屑迂,此時(shí)緩存隊(duì)列( hchan.buf)為空(即緩沖區(qū)內(nèi)無(wú)元素),直接將消息發(fā)送給 reciever goroutine,只產(chǎn)生一次復(fù)制,從當(dāng)前 channel 的等待隊(duì)列中取出等待的 goroutine膘侮,然后調(diào)用 send屈糊。goready 負(fù)責(zé)喚醒 goroutine。

3.當(dāng) channel 緩存隊(duì)列( hchan.buf )有剩余空間時(shí)琼了,將數(shù)據(jù)放到隊(duì)列里逻锐,等待接收,接收后總共產(chǎn)生兩次復(fù)制

4.當(dāng) channel 緩存隊(duì)列( hchan.buf )已滿時(shí)雕薪,將當(dāng)前 goroutine 加入 send 隊(duì)列并阻塞

receive:

// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // raceenabled: don't need to check ep, as it is always on the stack
    // or is new memory allocated by reflect.

    if debugChan {
        print("chanrecv: chan=", c, "\n")
    }

    // 從 nil 的 channel 中接收消息昧诱,永久阻塞
    if c == nil {
        if !block {
            return
        }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    // Fast path: check for failed non-blocking operation without acquiring the lock.
    //
    // After observing that the channel is not ready for receiving, we observe that the
    // channel is not closed. Each of these observations is a single word-sized read
    // (first c.sendq.first or c.qcount, and second c.closed).
    // Because a channel cannot be reopened, the later observation of the channel
    // being not closed implies that it was also not closed at the moment of the
    // first observation. We behave as if we observed the channel at that moment
    // and report that the receive cannot proceed.
    //
    // The order of operations is important here: reversing the operations can lead to
    // incorrect behavior when racing with a close.
    if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
        c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
        atomic.Load(&c.closed) == 0 {
        return
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }

    lock(&c.lock)

     // CASE1: 從已經(jīng) close 且為空的 channel recv 數(shù)據(jù)蝌以,返回空值
    if c.closed != 0 && c.qcount == 0 {
        if raceenabled {
            raceacquire(c.raceaddr())
        }
        unlock(&c.lock)
        if ep != nil {
            typedmemclr(c.elemtype, ep)
        }
        return true, false
    }

    // CASE2: send 隊(duì)列不為空,直接從channel隊(duì)列中獲取
    // sg是sends 線程隊(duì)列
    // 從sends 線程隊(duì)列獲取一個(gè)sudog并喚醒讓其將元素推入channel
    if sg := c.sendq.dequeue(); sg != nil {
        // Found a waiting sender. If buffer is size 0, receive value
        // directly from sender. Otherwise, receive from head of queue
        // and add sender's value to the tail of the queue (both map to
        // the same buffer slot because the queue is full).
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }
    // CASE3: 緩存隊(duì)列不為空敬尺,此時(shí)只有可能是緩存隊(duì)列已滿笼踩,從隊(duì)列頭取出元素阵子,
    if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        //移動(dòng)channel的recvx下標(biāo)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    if !block {
        unlock(&c.lock)
        return false, false
    }


    // CASE4: 緩存隊(duì)列為空荞胡,將 goroutine 加入 recv 隊(duì)列惑惶,并阻塞
    // no sender available: block on this channel.
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)
    goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)

    // someone woke us up
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    closed := gp.param == nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, !closed
}

從代碼上可以很明顯的看出
receive和send的四種情況相互配合相互對(duì)應(yīng)實(shí)現(xiàn)一存一拿的執(zhí)行順序
close channel 的工作

整個(gè)channel的流程結(jié)構(gòu):


WechatIMG1513.jpeg

15141548494179_.pic_hd.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒸眠,一起剝皮案震驚了整個(gè)濱河市修赞,隨后出現(xiàn)的幾起案子前翎,更是在濱河造成了極大的恐慌稚配,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件港华,死亡現(xiàn)場(chǎng)離奇詭異道川,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)立宜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)冒萄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人橙数,你說(shuō)我怎么就攤上這事尊流。” “怎么了灯帮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵奠旺,是天一觀的道長(zhǎng)蜘澜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)响疚,這世上最難降的妖魔是什么鄙信? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮忿晕,結(jié)果婚禮上装诡,老公的妹妹穿的比我還像新娘。我一直安慰自己践盼,他們只是感情好鸦采,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著咕幻,像睡著了一般渔伯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肄程,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天锣吼,我揣著相機(jī)與錄音,去河邊找鬼蓝厌。 笑死玄叠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拓提。 我是一名探鬼主播读恃,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼代态!你這毒婦竟也來(lái)了寺惫?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹦疑,失蹤者是張志新(化名)和其女友劉穎西雀,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體必尼,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒋搜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年篡撵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了判莉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡育谬,死狀恐怖券盅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膛檀,我是刑警寧澤锰镀,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布娘侍,位于F島的核電站,受9級(jí)特大地震影響泳炉,放射性物質(zhì)發(fā)生泄漏憾筏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一花鹅、第九天 我趴在偏房一處隱蔽的房頂上張望氧腰。 院中可真熱鬧,春花似錦刨肃、人聲如沸古拴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)黄痪。三九已至,卻和暖如春盔然,著一層夾襖步出監(jiān)牢的瞬間桅打,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工轻纪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留油额,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓刻帚,卻偏偏與公主長(zhǎng)得像潦嘶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子崇众,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

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