深入golang之---goroutine并發(fā)控制與通信

開發(fā)go程序的時候移层,時常需要使用goroutine并發(fā)處理任務(wù),有時候這些goroutine是相互獨立的赫粥,而有的時候观话,多個goroutine之間常常是需要同步與通信的。另一種情況越平,主goroutine需要控制它所屬的子goroutine频蛔,總結(jié)起來,實現(xiàn)多個goroutine間的同步與通信大致有:

  • 全局共享變量
  • channel通信(CSP模型)
  • Context包

本文章通過goroutine同步與通信的一個典型場景-通知子goroutine退出運行秦叛,來深入講解下golang的控制并發(fā)晦溪。

通知多個子goroutine退出運行

goroutine作為go語言的并發(fā)利器,不僅性能強勁而且使用方便:只需要一個關(guān)鍵字go即可將普通函數(shù)并發(fā)執(zhí)行挣跋,且goroutine占用內(nèi)存極腥病(一個goroutine只占2KB的內(nèi)存),所以開發(fā)go程序的時候很多開發(fā)者常常會使用這個并發(fā)工具,獨立的并發(fā)任務(wù)比較簡單舟肉,只需要用go關(guān)鍵字修飾函數(shù)就可以啟用一個goroutine直接運行修噪;但是,實際的并發(fā)場景常常是需要進行協(xié)程間的同步與通信路媚,以及精確控制子goroutine開始和結(jié)束黄琼,其中一個典型場景就是主進程通知名下所有子goroutine優(yōu)雅退出運行。

由于goroutine的退出機制設(shè)計是整慎,goroutine退出只能由本身控制脏款,不允許從外部強制結(jié)束該goroutine啡专。只有兩種情況例外轻绞,那就是main函數(shù)結(jié)束或者程序崩潰結(jié)束運行;所以得问,要實現(xiàn)主進程控制子goroutine的開始和結(jié)束丈氓,必須借助其它工具來實現(xiàn)强法。

控制并發(fā)的方法

實現(xiàn)控制并發(fā)的方式,大致可分成以下三類:

  • 全局共享變量
  • channel通信
  • Context包

全局共享變量

這是最簡單的實現(xiàn)控制并發(fā)的方式饮怯,實現(xiàn)步驟是:

  1. 聲明一個全局變量闰歪;
  2. 所有子goroutine共享這個變量蓖墅,并不斷輪詢這個變量檢查是否有更新;
  3. 在主進程中變更該全局變量论矾;
  4. 子goroutine檢測到全局變量更新教翩,執(zhí)行相應(yīng)的邏輯。

示例如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    running := true
    f := func() {
        for running {
            fmt.Println("sub proc running...")
            time.Sleep(1 * time.Second)
        }
        fmt.Println("sub proc exit")
    }
    go f()
    go f()
    go f()
    time.Sleep(2 * time.Second)
    running = false
    time.Sleep(3 * time.Second)
    fmt.Println("main proc exit")
}

全局變量的優(yōu)勢是簡單方便饱亿,不需要過多繁雜的操作闰靴,通過一個變量就可以控制所有子goroutine的開始和結(jié)束;缺點是功能有限配猫,由于架構(gòu)所致杏死,該全局變量只能是多讀一寫佳遣,否則會出現(xiàn)數(shù)據(jù)同步問題凡伊,當然也可以通過給全局變量加鎖來解決這個問題,但那就增加了復雜度诵盼,另外這種方式不適合用于子goroutine間的通信银还,因為全局變量可以傳遞的信息很杏挤琛;還有就是主進程無法等待所有子goroutine退出饮寞,因為這種方式只能是單向通知列吼,所以這種方法只適用于非常簡單的邏輯且并發(fā)量不太大的場景幽崩,一旦邏輯稍微復雜一點寞钥,這種方法就有點捉襟見肘。

channel通信

另一種更為通用且靈活的實現(xiàn)控制并發(fā)的方式是使用channel進行通信蹄溉。
首先您炉,我們先來了解下什么是golang中的channel:Channel是Go中的一個核心類型邻吭,你可以把它看成一個管道宴霸,通過它并發(fā)核心單元就可以發(fā)送或者接收數(shù)據(jù)進行通訊(communication)。
要想理解 channel 要先知道 CSP 模型:

CSP 是 Communicating Sequential Process 的簡稱畸写,中文可以叫做通信順序進程氓扛,是一種并發(fā)編程模型,由 Tony Hoare 于 1977 年提出千所。簡單來說淫痰,CSP 模型由并發(fā)執(zhí)行的實體(線程或者進程)所組成,實體之間通過發(fā)送消息進行通信籽孙,這里發(fā)送消息時使用的就是通道火俄,或者叫 channel瓜客。CSP 模型的關(guān)鍵是關(guān)注 channel,而不關(guān)注發(fā)送消息的實體犹菇。Go 語言實現(xiàn)了 CSP 部分理論芽卿,goroutine 對應(yīng) CSP 中并發(fā)執(zhí)行的實體卸例,channel 也就對應(yīng)著 CSP 中的 channel。
也就是說筷转,CSP 描述這樣一種并發(fā)模型:多個Process 使用一個 Channel 進行通信, 這個 Channel 連結(jié)的 Process 通常是匿名的呜舒,消息傳遞通常是同步的(有別于 Actor Model)。

先來看示例代碼:

package main
import (
    "fmt"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)
func consumer(stop <-chan bool) {
    for {
        select {
        case <-stop:
            fmt.Println("exit sub goroutine")
            return
        default:
            fmt.Println("running...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}
func main() {
        stop := make(chan bool)
        var wg sync.WaitGroup
        // Spawn example consumers
        for i := 0; i < 3; i++ {
            wg.Add(1)
            go func(stop <-chan bool) {
                defer wg.Done()
                consumer(stop)
            }(stop)
        }
        waitForSignal()
        close(stop)
        fmt.Println("stopping all jobs!")
        wg.Wait()
}
func waitForSignal() {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, os.Interrupt)
    signal.Notify(sigs, syscall.SIGTERM)
    <-sigs
}

這里可以實現(xiàn)優(yōu)雅等待所有子goroutine完全結(jié)束之后主進程才結(jié)束退出,借助了標準庫sync里的Waitgroup到腥,這是一種控制并發(fā)的方式乡范,可以實現(xiàn)對多goroutine的等待啤咽,官方文檔是這樣描述的:

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for.
Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

簡單來講宇整,它的源碼里實現(xiàn)了一個類似計數(shù)器的結(jié)構(gòu)芋膘,記錄每一個在它那里注冊過的協(xié)程,然后每一個協(xié)程完成任務(wù)之后需要到它那里注銷盼玄,然后在主進程那里可以等待直至所有協(xié)程完成任務(wù)退出潜腻。
使用步驟:

  1. 創(chuàng)建一個Waitgroup的實例wg;
  2. 在每個goroutine啟動的時候童番,調(diào)用wg.Add(1)注冊威鹿;
  3. 在每個goroutine完成任務(wù)后退出之前,調(diào)用wg.Done()注銷幼东。
  4. 在等待所有g(shù)oroutine的地方調(diào)用wg.Wait()阻塞進程科雳,知道所有g(shù)oroutine都完成任務(wù)調(diào)用wg.Done()注銷之后糟秘,Wait()方法會返回。

該示例程序是一種golang的select+channel的典型用法散庶,我們來稍微深入一點分析一下這種典型用法:

channel

首先了解下channel凌净,可以理解為管道泻蚊,它的主要功能點是:

  1. 隊列存儲數(shù)據(jù)
  2. 阻塞和喚醒goroutine

channel 實現(xiàn)集中在文件 runtime/chan.go 中,channel底層數(shù)據(jù)結(jié)構(gòu)是這樣的:

type hchan struct {
    qcount   uint           // 隊列中數(shù)據(jù)個數(shù)
    dataqsiz uint           // channel 大小
    buf      unsafe.Pointer // 存放數(shù)據(jù)的環(huán)形數(shù)組
    elemsize uint16         // channel 中數(shù)據(jù)類型的大小
    closed   uint32         // 表示 channel 是否關(guān)閉
    elemtype *_type // 元素數(shù)據(jù)類型
    sendx    uint   // send 的數(shù)組索引
    recvx    uint   // recv 的數(shù)組索引
    recvq    waitq  // 由 recv 行為(也就是 <-ch)阻塞在 channel 上的 goroutine 隊列
    sendq    waitq  // 由 send 行為 (也就是 ch<-) 阻塞在 channel 上的 goroutine 隊列

    // 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.
    lock mutex
}

從源碼可以看出它其實就是一個隊列加一個鎖(輕量)没卸,代碼本身不復雜约计,但涉及到上下文很多細節(jié)迁筛,故而不易通讀细卧,有興趣的同學可以去看一下,我的建議是蜘犁,從上面總結(jié)的兩個功能點出發(fā)止邮,一個是 ring buffer,用于存數(shù)據(jù)导披; 一個是存放操作(讀寫)該channel的goroutine 的隊列撩匕。

  • buf是一個通用指針,用于存儲數(shù)據(jù)并村,看源碼時重點關(guān)注對這個變量的讀寫
  • recvq 是讀操作阻塞在 channel 的 goroutine 列表滓技,sendq 是寫操作阻塞在 channel 的 goroutine 列表。列表的實現(xiàn)是 sudog膝昆,其實就是一個對 g 的結(jié)構(gòu)的封裝荚孵,看源碼時重點關(guān)注纬朝,是怎樣通過這兩個變量阻塞和喚醒goroutine的

由于涉及源碼較多共苛,這里就不再深入蜓萄。

select

然后是select機制澄峰,golang 的 select 機制可以理解為是在語言層面實現(xiàn)了和 select, poll, epoll 相似的功能:監(jiān)聽多個描述符的讀/寫等事件俏竞,一旦某個描述符就緒(一般是讀或者寫事件發(fā)生了),就能夠?qū)l(fā)生的事件通知給關(guān)心的應(yīng)用程序去處理該事件玻佩。 golang 的 select 機制是席楚,監(jiān)聽多個channel酣胀,每一個 case 是一個事件,可以是讀事件也可以是寫事件甚脉,隨機選擇一個執(zhí)行铆农,可以設(shè)置default墩剖,它的作用是:當監(jiān)聽的多個事件都阻塞住會執(zhí)行default的邏輯。

select的源碼在runtime/select.go 郊霎,看的時候建議是重點關(guān)注 pollorder 和 lockorder

  • pollorder保存的是scase的序號爷绘,亂序是為了之后執(zhí)行時的隨機性土至。
  • lockorder保存了所有case中channel的地址,這里按照地址大小堆排了一下lockorder對應(yīng)的這片連續(xù)內(nèi)存骡苞。對chan排序是為了去重,保證之后對所有channel上鎖時不會重復上鎖贴见。

因為我對這部分源碼研究得也不是很深蝇刀,故而點到為止即可徘溢,有興趣的可以去看看源碼啦然爆!

具體到demo代碼:consumer為協(xié)程的具體代碼黍图,里面是只有一個不斷輪詢channel變量stop的循環(huán),所以主進程是通過stop來通知子協(xié)程何時該結(jié)束運行的剖张,在main方法中搔弄,close掉stop之后丰滑,讀取已關(guān)閉的channel會立刻返回該channel數(shù)據(jù)類型的零值褒墨,因此子goroutine里的<-stop操作會馬上返回,然后退出運行浑玛。

事實上噩咪,通過channel控制子goroutine的方法可以總結(jié)為:循環(huán)監(jiān)聽一個channel剧腻,一般來說是for循環(huán)里放一個select監(jiān)聽channel以達到通知子goroutine的效果。再借助Waitgroup灰伟,主進程可以等待所有協(xié)程優(yōu)雅退出后再結(jié)束自己的運行栏账,這就通過channel實現(xiàn)了優(yōu)雅控制goroutine并發(fā)的開始和結(jié)束。

channel通信控制基于CSP模型竖般,相比于傳統(tǒng)的線程與鎖并發(fā)模型茶鹃,避免了大量的加鎖解鎖的性能消耗闭翩,而又比Actor模型更加靈活,使用Actor模型時兑障,負責通訊的媒介與執(zhí)行單元是緊耦合的–每個Actor都有一個信箱流译。而使用CSP模型者疤,channel是第一對象,可以被獨立地創(chuàng)建竞漾,寫入和讀出數(shù)據(jù)业岁,更容易進行擴展寇蚊。

殺器Context

Context通常被譯作上下文仗岸,它是一個比較抽象的概念。在討論鏈式調(diào)用技術(shù)時也經(jīng)常會提到上下文较锡。一般理解為程序單元的一個運行狀態(tài)蚂蕴、現(xiàn)場、快照熔号,而翻譯中上下又很好地詮釋了其本質(zhì)鸟整,上下則是存在上下層的傳遞篮条,上會把內(nèi)容傳遞給下。在Go語言中亮瓷,程序單元也就指的是Goroutine。

每個Goroutine在執(zhí)行之前蚓胸,都要先知道程序當前的執(zhí)行狀態(tài)沛膳,通常將這些執(zhí)行狀態(tài)封裝在一個Context變量中锹安,傳遞給要執(zhí)行的Goroutine中。上下文則幾乎已經(jīng)成為傳遞與請求同生存周期變量的標準方法忍宋。在網(wǎng)絡(luò)編程下风罩,當接收到一個網(wǎng)絡(luò)請求Request超升,在處理這個Request的goroutine中,可能需要在當前gorutine繼續(xù)開啟多個新的Goroutine來獲取數(shù)據(jù)與邏輯處理(例如訪問數(shù)據(jù)庫乾闰、RPC服務(wù)等)涯肩,即一個請求Request,會需要多個Goroutine中處理谣膳。而這些Goroutine可能需要共享Request的一些信息继谚;同時當Request被取消或者超時的時候阵幸,所有從這個Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束挚赊。

context在go1.7之后被引入到標準庫中,1.7之前的go版本使用context需要安裝golang.org/x/net/context包妹卿,關(guān)于golang context的更詳細說明夺克,可參考官方文檔:context

Context初試

Context的創(chuàng)建和調(diào)用關(guān)系是層層遞進的铺纽,也就是我們通常所說的鏈式調(diào)用哟忍,類似數(shù)據(jù)結(jié)構(gòu)里的樹锅很,從根節(jié)點開始,每一次調(diào)用就衍生一個葉子節(jié)點尝偎。首先致扯,生成根節(jié)點当辐,使用context.Background方法生成缘揪,而后可以進行鏈式調(diào)用使用context包里的各類方法,context包里的所有方法:

  • func Background() Context
  • func TODO() Context
  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  • func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
  • func WithValue(parent Context, key, val interface{}) Context

這里僅以WithCancel和WithValue方法為例來實現(xiàn)控制并發(fā)和通信:
話不多說慷吊,上碼:

package main

import (
    "context"
    "crypto/md5"
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"
)

type favContextKey string

func main() {
    wg := &sync.WaitGroup{}
    values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
    ctx, cancel := context.WithCancel(context.Background())

    for _, url := range values {
        wg.Add(1)
        subCtx := context.WithValue(ctx, favContextKey("url"), url)
        go reqURL(subCtx, wg)
    }

    go func() {
        time.Sleep(time.Second * 3)
        cancel()
    }()

    wg.Wait()
    fmt.Println("exit main goroutine")
}

func reqURL(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    url, _ := ctx.Value(favContextKey("url")).(string)
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("stop getting url:%s\n", url)
            return
        default:
            r, err := http.Get(url)
            if r.StatusCode == http.StatusOK && err == nil {
                body, _ := ioutil.ReadAll(r.Body)
                subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
                wg.Add(1)
                go showResp(subCtx, wg)
            }
            r.Body.Close()
            //啟動子goroutine是為了不阻塞當前goroutine溉瓶,這里在實際場景中可以去執(zhí)行其他邏輯堰酿,這里為了方便直接sleep一秒
            // doSometing()
            time.Sleep(time.Second * 1)
        }
    }
}

func showResp(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stop showing resp")
            return
        default:
            //子goroutine里一般會處理一些IO任務(wù)触创,如讀寫數(shù)據(jù)庫或者rpc調(diào)用为牍,這里為了方便直接把數(shù)據(jù)打印
            fmt.Println("printing ", ctx.Value(favContextKey("resp")))
            time.Sleep(time.Second * 1)
        }
    }
}

前面我們說過Context就是設(shè)計用來解決那種多個goroutine處理一個Request且這多個goroutine需要共享Request的一些信息的場景吵聪,以上是一個簡單模擬上述過程的demo吟逝。

首先調(diào)用context.Background()生成根節(jié)點赦肋,然后調(diào)用withCancel方法佃乘,傳入根節(jié)點,得到新的子Context以及根節(jié)點的cancel方法(通知所有子節(jié)點結(jié)束運行)庞呕,這里要注意:該方法也返回了一個Context住练,這是一個新的子節(jié)點愁拭,與初始傳入的根節(jié)點不是同一個實例了岭埠,但是每一個子節(jié)點里會保存從最初的根節(jié)點到本節(jié)點的鏈路信息 蔚鸥,才能實現(xiàn)鏈式止喷。

程序的reqURL方法接收一個url启盛,然后通過http請求該url獲得response技羔,然后在當前goroutine里再啟動一個子groutine把response打印出來藤滥,然后從ReqURL開始Context樹往下衍生葉子節(jié)點(每一個鏈式調(diào)用新產(chǎn)生的ctx),中間每個ctx都可以通過WithValue方式傳值(實現(xiàn)通信)拙绊,而每一個子goroutine都能通過Value方法從父goroutine取值,實現(xiàn)協(xié)程間的通信榄攀,每個子ctx可以調(diào)用Done方法檢測是否有父節(jié)點調(diào)用cancel方法通知子節(jié)點退出運行,根節(jié)點的cancel調(diào)用會沿著鏈路通知到每一個子節(jié)點金句,因此實現(xiàn)了強并發(fā)控制檩赢,流程如圖:


Context調(diào)用鏈路

該demo結(jié)合前面說的WaitGroup實現(xiàn)了優(yōu)雅并發(fā)控制和通信,關(guān)于WaitGroup的原理和使用前文已做解析违寞,這里便不再贅述贞瞒,當然,實際的應(yīng)用場景不會這么簡單趁曼,處理Request的goroutine啟動多個子goroutine大多是處理IO密集的任務(wù)如讀寫數(shù)據(jù)庫或rpc調(diào)用军浆,然后在主goroutine中繼續(xù)執(zhí)行其他邏輯,這里為了方便講解做了最簡單的處理乒融。

Context作為golang中并發(fā)控制和通信的大殺器,被廣泛應(yīng)用摄悯,一些使用go開發(fā)http服務(wù)的同學如果閱讀過這些很多 web framework的源碼就知道赞季,Context在web framework隨處可見,因為http請求處理就是一個典型的鏈式過程以及并發(fā)場景射众,所以很多web framework都會借助Context實現(xiàn)鏈式調(diào)用的邏輯碟摆。有興趣可以讀一下context包的源碼,會發(fā)現(xiàn)Context的實現(xiàn)其實是結(jié)合了Mutex鎖和channel而實現(xiàn)的叨橱,其實并發(fā)典蜕、同步的很多高級組件萬變不離其宗断盛,都是通過最底層的數(shù)據(jù)結(jié)構(gòu)組裝起來的,只要知曉了最基礎(chǔ)的概念愉舔,上游的架構(gòu)也可以一目了然钢猛。

context使用規(guī)范

最后,Context雖然是神器轩缤,但開發(fā)者使用也要遵循基本法命迈,以下是一些Context使用的規(guī)范:

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一個結(jié)構(gòu)體當中火的,顯式地傳入函數(shù)壶愤。Context變量需要作為第一個參數(shù)使用,一般命名為ctx馏鹤;

  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use征椒;即使方法允許,也不要傳入一個nil的Context湃累,如果你不確定你要用什么Context的時候傳一個context.TODO勃救;

  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請求相關(guān)的元數(shù)據(jù)治力,不要用它來傳遞一些可選的參數(shù)蒙秒;

  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中宵统,Context在多個goroutine中是安全的晕讲;

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市榜田,隨后出現(xiàn)的幾起案子益兄,更是在濱河造成了極大的恐慌,老刑警劉巖箭券,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疑枯,居然都是意外死亡辩块,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門荆永,熙熙樓的掌柜王于貴愁眉苦臉地迎上來废亭,“玉大人,你說我怎么就攤上這事具钥《勾澹” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵骂删,是天一觀的道長掌动。 經(jīng)常有香客問我四啰,道長,這世上最難降的妖魔是什么粗恢? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任柑晒,我火速辦了婚禮,結(jié)果婚禮上眷射,老公的妹妹穿的比我還像新娘匙赞。我一直安慰自己,他們只是感情好妖碉,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布涌庭。 她就那樣靜靜地躺著,像睡著了一般欧宜。 火紅的嫁衣襯著肌膚如雪坐榆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天鱼鸠,我揣著相機與錄音猛拴,去河邊找鬼。 笑死蚀狰,一個胖子當著我的面吹牛愉昆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播麻蹋,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼跛溉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扮授?” 一聲冷哼從身側(cè)響起芳室,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刹勃,沒想到半個月后堪侯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡荔仁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年伍宦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乏梁。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡次洼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遇骑,到底是詐尸還是另有隱情卖毁,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布落萎,位于F島的核電站亥啦,受9級特大地震影響炭剪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜禁悠,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一念祭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碍侦,春花似錦粱坤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至濒旦,卻和暖如春株旷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尔邓。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工晾剖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梯嗽。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓齿尽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灯节。 傳聞我的和親對象是個殘疾皇子循头,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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