fasthttp剖析

先說點(diǎn)題外話迈倍,最近在開發(fā)公司級(jí)的網(wǎng)關(guān)伤靠,雖然沒有明說,但是對(duì)于我們大家來說Nginx就是我們對(duì)標(biāo)的對(duì)象。但是說實(shí)話宴合,想要對(duì)標(biāo)Nginx的性能焕梅,用Go開發(fā)基本上是不可能的,人家沒有scheduler調(diào)度這一項(xiàng)就可以吊打Go了卦洽,更別說Go還有GC了贞言。跑Benchmark的時(shí)候就能很明顯地看到,隨著并發(fā)請(qǐng)求的增多阀蒂,Ngx的響應(yīng)時(shí)間幾乎就是一條完美的直線该窗。而我們用Go開發(fā)的網(wǎng)關(guān),在并發(fā)數(shù)小于等于閾值X的時(shí)候還能跟Ngx不相上下蚤霞,雖然有周期性GC帶來的毛刺酗失,但是總體影響不大,毛刺主要影響的是99分位的響應(yīng)時(shí)間昧绣。但是规肴,一旦并發(fā)數(shù)超過閾值X,Go網(wǎng)關(guān)的響應(yīng)時(shí)間便指數(shù)級(jí)地上升了夜畴。
這個(gè)現(xiàn)象拖刃,直接通過pprof觀察可以發(fā)現(xiàn),在并發(fā)數(shù)小于X時(shí)贪绘,總Goroutine數(shù)量基本保持穩(wěn)定兑牡,但是一旦超過閾值X,goroutine數(shù)量則快速飆升上去税灌。為什么呢发绢?簡(jiǎn)單來說就是,由于Go的runtime需要調(diào)度goroutine(sleep哪個(gè)喚醒哪個(gè)搶占哪個(gè))垄琐,在Goroutine數(shù)量巨大的時(shí)候這個(gè)調(diào)度的開銷非常大,每個(gè)goroutine被喚起的周期變得很長(zhǎng)经柴,因此會(huì)導(dǎo)致響應(yīng)變長(zhǎng)狸窘。同時(shí),由于大量goroutine并沒有完成其任務(wù)坯认,導(dǎo)致無法回收翻擒,新到的請(qǐng)求就只能new goroutine,導(dǎo)致goroutine的數(shù)量進(jìn)一步增加牛哺,使得響應(yīng)時(shí)間進(jìn)一步惡化…然后基本上服務(wù)就不可用了陋气,pprof能夠看到,絕大部分時(shí)間都在執(zhí)行runtime.findRunableG引润。
這個(gè)現(xiàn)象讓我想起了以前大學(xué)時(shí)代學(xué)習(xí)的二極管的雪崩擊穿巩趁。雖然,Go號(hào)稱goroutine非常輕量級(jí)淳附,可以輕松地開到十萬百萬級(jí)议慰,但是這話是省略了很多上下文和限制條件的蠢古。它只告訴你可以有millions of goroutine,但是沒告訴你后果是啥别凹,怎樣才能開到millions草讶,輕松開millions of goroutines是相對(duì)誰來說輕松…總之一句話就是,太美的承諾都不能信炉菲。

說回正題堕战,fasthttp!為什么我進(jìn)入主題之前說這么多題外話拍霜,本質(zhì)上的目的就是想表明嘱丢,對(duì)處理高并發(fā)場(chǎng)景的應(yīng)用,goroutine的代價(jià)其實(shí)是不可忽視的沉御,一定要省著用屿讽!fasthttp為什么比標(biāo)準(zhǔn)庫net/http快,就是因?yàn)樗⒉皇莵硪粋€(gè)請(qǐng)求就開一個(gè)goroutine吠裆,而是維護(hù)了一個(gè)workerPool伐谈,盡可能復(fù)用goroutine。當(dāng)然還有很多別的優(yōu)化试疙,比如盡量減少數(shù)據(jù)copy诵棵,這些在fasthttp的API里就有很直觀的體現(xiàn)。

先來簡(jiǎn)單看看fasthttp的大框架結(jié)構(gòu):

func (s *Server) Serve(ln net.Listener) error {
    var lastOverflowErrorTime time.Time
    var lastPerIPErrorTime time.Time
    var c net.Conn
    var err error
    // 略

    maxWorkersCount := s.getConcurrency()
    s.concurrencyCh = make(chan struct{}, maxWorkersCount)
    wp := &workerPool{
        WorkerFunc:      s.serveConn,
        MaxWorkersCount: maxWorkersCount,
        LogAllErrors:    s.LogAllErrors,
        Logger:          s.logger(),
        connState:       s.setState,
    }
    wp.Start()

    atomic.AddInt32(&s.open, 1)
    defer atomic.AddInt32(&s.open, -1)

    for {
        if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil {
      //略
        }
        s.setState(c, StateNew)
        atomic.AddInt32(&s.open, 1)
        if !wp.Serve(c) {
    // 略
        }
        c = nil
    }
}

上面是一個(gè)經(jīng)過刪減提煉后的代碼祝旷,從上面可以看到履澳,fasthttp在啟動(dòng)時(shí)先實(shí)例化并啟動(dòng)了一個(gè)workerPool,然后進(jìn)入到了一個(gè)大循環(huán)中怀跛,也是每當(dāng)accept一個(gè)連接之后就教給workerPool去Serve距贷。
可以簡(jiǎn)單地對(duì)比以下標(biāo)準(zhǔn)庫net/http對(duì)應(yīng)邏輯的偽代碼:

for {
    c,err := accept(s)
    go s.serve(c)
}

可以看到最大的區(qū)別就是fasthttp是wp.serve(c)而標(biāo)準(zhǔn)庫是直接起一個(gè)goroutinego s.serve(c)。為什么不像標(biāo)準(zhǔn)庫直接啟動(dòng)一個(gè)goroutine去處理呢吻谋?當(dāng)然是為了優(yōu)化爸一取!前面也說了漓拾,高并發(fā)下goroutine代價(jià)也是很高的阁最,盡量復(fù)用goroutine。

接著我們來講講workerPool骇两。和別的語言中線程池的實(shí)現(xiàn)思路基本一致速种,偽代碼如下:


func init() {
    for i:=0 i<N; i++ {
      signalReceiver := make(chan net.Conn)
      ready = append(ready, signalReceiver)
      go job(signalReceiver)
    }
}

// 每個(gè)worker都有一個(gè)自己的channel,通過從channel中接收消息來獲得執(zhí)行權(quán)
func job(ch chan net.Conn) {
    for c := range ch {
        doSomething(c)
        //當(dāng)完成任務(wù)后低千,把自己的channel放到ready隊(duì)列里配阵,表示自己是空閑狀態(tài)
        lock()
        ready = append(ready, ch)
        unlock()
    }
}
var ready = []chan net.Conn{}

// 每次從ready隊(duì)列里取一個(gè)空閑的channel,然后通知該job來執(zhí)行任務(wù)
func serve(c net.Conn) {
  lock()
  readyJob := ready[len(ready)-1]
  ready = ready[:len(ready)-1]
  unlock()
  readyJob <- c
}

以上便是一個(gè)最簡(jiǎn)單的(問題多多)的workerPool。我們一開始啟動(dòng)了N個(gè)worker闸餐,每個(gè)worker都由一個(gè)自己的channel用于接收數(shù)據(jù)饱亮,然后一開始把所有worker的channel都放到ready隊(duì)列里,表示所有的worker都處于空閑狀態(tài)舍沙。每次接收到一個(gè)請(qǐng)求時(shí)近上,serve就通過ready去看哪個(gè)worker是空閑的,然后向那個(gè)worker的channel發(fā)消息拂铡,從而讓該worker執(zhí)行當(dāng)前任務(wù)壹无。

確保你完全明白上面的workerPool的實(shí)現(xiàn)思路,我們?cè)倮^續(xù)看fasthttp的實(shí)現(xiàn)感帅。
fasthttp的serve和我們上述基本一致:

func (wp *workerPool) Serve(c net.Conn) bool {
    ch := wp.getCh()
    if ch == nil {
        return false
    }
    ch.ch <- c
    return true
}

從wp里取一個(gè)channel斗锭,然后向該channel發(fā)消息,讓對(duì)應(yīng)的worker執(zhí)行任務(wù)失球。我們這個(gè)具體看看fasthttp是怎么找到空閑worker的:

func (wp *workerPool) getCh() *workerChan {
    var ch *workerChan
    createWorker := false

    wp.lock.Lock()
    ready := wp.ready
    n := len(ready) - 1
    if n < 0 {
        if wp.workersCount < wp.MaxWorkersCount {
            createWorker = true
            wp.workersCount++
        }
    } else {
        ch = ready[n]
        ready[n] = nil
        wp.ready = ready[:n]
    }
    wp.lock.Unlock()

    if ch == nil {
        if !createWorker {
            return nil
        }
        vch := wp.workerChanPool.Get()
        if vch == nil {
            vch = &workerChan{
                ch: make(chan net.Conn, workerChanCap),
            }
        }
        ch = vch.(*workerChan)
        go func() {
            wp.workerFunc(ch)
            wp.workerChanPool.Put(vch)
        }()
    }
    return ch
}

其實(shí)也是岖是,一開始嘗試從ready隊(duì)列里取,如果ready隊(duì)列里沒有实苞,但是當(dāng)前worker數(shù)量還沒有達(dá)到用戶配置的MaxWorkersCount豺撑,那么就新起一個(gè)worker,否則就直接返回nil黔牵。這里新建worker還用到了臨時(shí)對(duì)象池sync.Pool也就是代碼中的wp.workerChanPool聪轿,能在兩次gc之間復(fù)用對(duì)象,減少內(nèi)存分配的開銷猾浦。不過從這里也能看出陆错,fasthttp的workerPool是lazyLoading的,并不是像我們之前的實(shí)現(xiàn)那樣一開始就創(chuàng)建N個(gè)worker金赦。這么做當(dāng)然就是省內(nèi)存啦音瓷,大部分業(yè)務(wù)大時(shí)間服務(wù)器都不會(huì)有這么高的并發(fā)壓力,因此fasthttp作為通用框架夹抗,lazyLoading肯定是一個(gè)正確的選擇外莲!

這里的wp.workerFunc其實(shí)就是我們之前偽代碼中的job函數(shù),在里面監(jiān)聽channel消息兔朦,然后執(zhí)行業(yè)務(wù)邏輯。我們可以具體看看:

func (wp *workerPool) workerFunc(ch *workerChan) {
    var c net.Conn

    var err error
    for c = range ch.ch {
        if c == nil {
            break
        }

        if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
        // 省略錯(cuò)誤處理
        }
        if err == errHijacked {
            wp.connState(c, StateHijacked)
        } else {
            c.Close()
            wp.connState(c, StateClosed)
        }
        c = nil
        // 把ch放到ready隊(duì)列里
        if !wp.release(ch) {
            break
        }
    }

    wp.lock.Lock()
    wp.workersCount--
    wp.lock.Unlock()
}

從上面代碼我們能夠看到磨确,每個(gè)job確實(shí)也是不斷地監(jiān)聽channel沽甥,如果收到消息且不是nil,那就執(zhí)行真正的業(yè)務(wù)邏輯乏奥。成功執(zhí)行完之后(省略掉一些錯(cuò)誤處理分支)摆舟,通過wp.release把channel放到ready隊(duì)列里。正常情況下,workerFunc會(huì)一直執(zhí)行恨诱,直到收到一個(gè)nil或者執(zhí)行出錯(cuò)媳瞪,然后把workerPool的workersCount-1并退出,之后就等著runtime來回收或者釋放goroutine了照宝。

以上就是fasthttp的主要邏輯蛇受,沒有什么特別的設(shè)計(jì),和其它線程池的設(shè)計(jì)幾乎是一模一樣的厕鹃。當(dāng)然fasthttp的workerPool還有些需要注意的性質(zhì)兢仰,從上面可以看出,每次release到ready隊(duì)列時(shí)剂碴,直接放到隊(duì)尾把将,每次取也是從隊(duì)尾取。因此fasthttp的worker隊(duì)列是FILO的忆矛,即先進(jìn)后出察蹲。這會(huì)導(dǎo)致在并發(fā)小的情況下很多先入隊(duì)的worker會(huì)一直空閑。因此fasthttp也支持設(shè)置IdleDuration參數(shù)催训,定期清理空閑的worker減少資源占用洽议。這部分代碼:

  func (wp *workerPool) Start() {
    if wp.stopCh != nil {
        panic("BUG: workerPool already started")
    }
    wp.stopCh = make(chan struct{})
    stopCh := wp.stopCh
    go func() {
        var scratch []*workerChan
        for {
            wp.clean(&scratch)
            select {
            case <-stopCh:
                return
            default:
                time.Sleep(wp.getMaxIdleWorkerDuration())
            }
        }
    }()
}

func (wp *workerPool) clean(scratch *[]*workerChan) {
    maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration()

    // Clean least recently used workers if they didn't serve connections
    // for more than maxIdleWorkerDuration.
    currentTime := time.Now()

    wp.lock.Lock()
    ready := wp.ready
    n := len(ready)
    i := 0
    for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration {
        i++
    }
    *scratch = append((*scratch)[:0], ready[:i]...)
    if i > 0 {
        m := copy(ready, ready[i:])
        for i = m; i < n; i++ {
            ready[i] = nil
        }
        wp.ready = ready[:m]
    }
    wp.lock.Unlock()

    // Notify obsolete workers to stop.
    // This notification must be outside the wp.lock, since ch.ch
    // may be blocking and may consume a lot of time if many workers
    // are located on non-local CPUs.
    tmp := *scratch
    for i, ch := range tmp {
        ch.ch <- nil
        tmp[i] = nil
    }
}

wp.Start中啟動(dòng)一個(gè)goroutine,定期執(zhí)行wp.clean操作瞳腌。wp.clean其實(shí)就是從頭遍歷ready隊(duì)列绞铃,把空閑時(shí)間超過maxIdleWorkerDuration的都清理掉。這里清理也很簡(jiǎn)單嫂侍,直接向該channel發(fā)送一個(gè)nil就行了儿捧。別忘了之前workFunc中,當(dāng)收到一個(gè)nil之后就直接break出大循環(huán)挑宠,做些收尾工作然后退出函數(shù)菲盾,整個(gè)goroutine也就可以被runtime回收了。

不過這還不算完各淀。我們?cè)倏纯磜p.WorkerFunc吧懒鉴,這是一個(gè)很長(zhǎng)的函數(shù),其實(shí)就是s.serveConn(在初始化時(shí)把s.serveConn賦值給了wp.WorkerFunc)碎浇,以下也是簡(jiǎn)化過的代碼临谱,我們只關(guān)注其hotpath:

func (s *Server) serveConn(c net.Conn) error {
  ctx := s.acquireCtx(c)
  var connRequestNum int
  for {
    connRequestNum++
    var br *bufio.Reader = acquireReader(ctx)
    err = ctx.Request.readLimitBody(br, maxRequestSize)
    err = s.Handler(ctx)
    var wr *bufio.Writer = acquireWriter(ctx)
    err = wr.Flush()
    if err != nil {
      return      
    }
    if s.MaxRequestNumPerConn {
      break;
    }
  }
// 省略
}

這里有個(gè)比較奇怪的地方,為什么要用一個(gè)無限循環(huán)呢奴璃?難道是接收網(wǎng)絡(luò)包的分組之類的悉默?NoNoNo,不要把概念搞混了苟穆!分組這些都是協(xié)議棧處理的內(nèi)容抄课,到Go這塊直接就是應(yīng)用層了唱星。那為什么要無限循環(huán)呢?在我這篇文章里說過跟磨,只有通過三次握手新建的連接间聊,才用Accept去取。建立好連接后抵拘,后續(xù)數(shù)據(jù)的收發(fā)都是基于該socket對(duì)象哎榴,也即net.Conn對(duì)象。

也就是說仑濒,只有新建的連接才會(huì)從Serve中的acceptConn函數(shù)開始叹话,然后執(zhí)行上述的邏輯。已經(jīng)建立好的連接墩瞳,后續(xù)的請(qǐng)求都在serveConn中循環(huán)處理驼壶。換句話說,如果一個(gè)HTTP請(qǐng)求是KeepAlive的(HTTP 1.1默認(rèn)行為)喉酌,那么worker就會(huì)一直處理此連接热凹,無限循環(huán)地從該連接上讀取數(shù)據(jù)(也就是下一個(gè)請(qǐng)求),然后進(jìn)行業(yè)務(wù)邏輯泪电。除非遇到connRequestNum >= MaxRequestNumPerConn或者其它錯(cuò)誤了般妙,才會(huì)關(guān)閉該連接,然后把自己設(shè)置為空閑相速。

這里還有個(gè)問題你可能會(huì)疑惑:由于并不知道下一次請(qǐng)求啥時(shí)候會(huì)發(fā)過來碟渺,這里只有一個(gè)ctx.Request.readLimitBody(br, maxRequestSize),并沒有看到“不斷嘗試去讀”這種邏輯呢突诬?

這是一個(gè)好問題I慌摹!

其實(shí)這里的答案就是Go提供的一種強(qiáng)大的抽象net.Conn旺隙。它不僅僅是代表一個(gè)socket绒极,同時(shí)它還被封裝成了netPoller對(duì)象。netPoller是Go runtime的一個(gè)數(shù)據(jù)結(jié)構(gòu)蔬捷,也許你早已知道了linux的epoll垄提,netPoller就是對(duì)epoll的一種封裝。Go把socket注冊(cè)到epoll里周拐,后續(xù)當(dāng)用戶在net.Conn對(duì)象上調(diào)用Read時(shí)铡俐,實(shí)際上是這樣的:

// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
    if err := fd.readLock(); err != nil {
        return 0, err
    }
    defer fd.readUnlock()
    if len(p) == 0 {
        // If the caller wanted a zero byte read, return immediately
        // without trying (but after acquiring the readLock).
        // Otherwise syscall.Read returns 0, nil which looks like
        // io.EOF.
        // TODO(bradfitz): make it wait for readability? (Issue 15735)
        return 0, nil
    }
    if err := fd.pd.prepareRead(fd.isFile); err != nil {
        return 0, err
    }
    if fd.IsStream && len(p) > maxRW {
        p = p[:maxRW]
    }
    for {
        n, err := syscall.Read(fd.Sysfd, p)
        if err != nil {
            n = 0
            if err == syscall.EAGAIN && fd.pd.pollable() {
                if err = fd.pd.waitRead(fd.isFile); err == nil {
                    continue
                }
            }

            // On MacOS we can see EINTR here if the user
            // pressed ^Z.  See issue #22838.
            if runtime.GOOS == "darwin" && err == syscall.EINTR {
                continue
            }
        }
        err = fd.eofError(n, err)
        return n, err
    }
}

也就是先會(huì)進(jìn)行一次syscall.Read,但是如果沒有數(shù)據(jù)妥粟,此時(shí)會(huì)得到一個(gè)錯(cuò)誤syscall.EAGAIN高蜂。這時(shí),會(huì)執(zhí)行fd.pd.waitRead罕容,這個(gè)函數(shù)會(huì)一直阻塞直到epoll通知socket有數(shù)據(jù)就緒备恤。這里的阻塞和syscall的阻塞調(diào)用不一樣,這里的阻塞相當(dāng)于主動(dòng)讓出時(shí)間片(park)锦秒,當(dāng)前線程可以去執(zhí)行別的goroutine露泊,然后等待適當(dāng)?shù)臅r(shí)間(epoll event fire)被runtime喚醒。從用戶的角度來看旅择,這就像是阻塞的惭笑。

那么這是怎么做到的呢?這就涉及到runtime的調(diào)度了生真,具體可以參見這篇文章沉噩。順帶提一句,查看太深入到runtime的代碼一定要用dlv柱蟀、lldb或者gdb川蒙,用IDE會(huì)跳到錯(cuò)誤的位置,因?yàn)楹芏鄏untime的代碼直接和平臺(tái)有關(guān)了长已,不同平臺(tái)對(duì)應(yīng)實(shí)現(xiàn)也不一樣畜眨,然后鏈接器也會(huì)搞一些事情導(dǎo)致符號(hào)表在源代碼層面不能正確跳轉(zhuǎn),所以一定要用單步調(diào)試去看代碼术瓮。

以上便是對(duì)fasthttp源碼結(jié)構(gòu)的一個(gè)剖析康聂,接下來讓我們思考一個(gè)問題吧:

假設(shè)有N個(gè)客戶端都使用長(zhǎng)連接(http keepalive)發(fā)送請(qǐng)求,同時(shí)假設(shè)每個(gè)客戶端每秒發(fā)送M個(gè)請(qǐng)求胞四。那么此時(shí)fasthttp和net/http的性能會(huì)如何呢恬汁?

由于有N個(gè)客戶端,因此fashttp和net/http的Server都會(huì)Accept N次辜伟。標(biāo)準(zhǔn)庫會(huì)啟動(dòng)N個(gè)goroutine氓侧,而對(duì)于fasthttp來說,由于每個(gè)連接都是長(zhǎng)連接游昼,每個(gè)worker會(huì)一直處理該連接直到連接關(guān)閉或者次數(shù)到了限制甘苍,因此ready隊(duì)列一直是空的,所以也會(huì)啟動(dòng)N個(gè)goroutine烘豌。即使N很大载庭,這種case下fasthttp和標(biāo)準(zhǔn)庫所使用的goroutine數(shù)量的持平的。
但是由于fasthttp大量使用了sync.Pool復(fù)用對(duì)象減少內(nèi)存分配的開銷廊佩,而標(biāo)準(zhǔn)庫每個(gè)請(qǐng)求都會(huì)new一個(gè)request和response對(duì)象囚聚。同時(shí)fasthttp中大部分存的是[]byte而標(biāo)準(zhǔn)庫中多是string,因此fasthttp還相比標(biāo)準(zhǔn)庫減少了很多內(nèi)存復(fù)制的開銷标锄。
總體而言顽铸,fasthttp性能在各種場(chǎng)景下應(yīng)該都比標(biāo)準(zhǔn)庫好很多。

當(dāng)然還有個(gè)tips料皇,作為網(wǎng)關(guān)谓松,外網(wǎng)和內(nèi)網(wǎng)的一道門星压,為了防止惡意請(qǐng)求,還是應(yīng)該在response后主動(dòng)close連接鬼譬,也就是說應(yīng)該使用短連接娜膘,這樣才安全。

That's all优质!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竣贪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子巩螃,更是在濱河造成了極大的恐慌演怎,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件避乏,死亡現(xiàn)場(chǎng)離奇詭異爷耀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)淑际,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門畏纲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人春缕,你說我怎么就攤上這事盗胀。” “怎么了锄贼?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵票灰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我宅荤,道長(zhǎng)屑迂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任冯键,我火速辦了婚禮惹盼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惫确。我一直安慰自己手报,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布改化。 她就那樣靜靜地躺著掩蛤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陈肛。 梳的紋絲不亂的頭發(fā)上揍鸟,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音句旱,去河邊找鬼阳藻。 笑死晰奖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的腥泥。 我是一名探鬼主播畅涂,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼道川!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起立宜,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤冒萄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后橙数,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尊流,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年灯帮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崖技。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钟哥,死狀恐怖迎献,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腻贰,我是刑警寧澤吁恍,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站播演,受9級(jí)特大地震影響冀瓦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜写烤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一翼闽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洲炊,春花似錦感局、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至古徒,卻和暖如春拓提,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隧膘。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國打工代态, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寺惫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓蹦疑,卻偏偏與公主長(zhǎng)得像西雀,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歉摧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

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

  • Chapter 8 Goroutines and Channels Go enable two styles of...
    SongLiang閱讀 1,599評(píng)論 0 3
  • 開發(fā)go程序的時(shí)候艇肴,時(shí)常需要使用goroutine并發(fā)處理任務(wù),有時(shí)候這些goroutine是相互獨(dú)立的叁温,而有的時(shí)...
    駐馬聽雪閱讀 2,441評(píng)論 0 21
  • 由淺入深剖析 go channel channel 是 golang 中最核心的 feature 之一再悼,因此理解 ...
    不智魚閱讀 59,753評(píng)論 4 83
  • 并發(fā)編程 1跟束、并行和并發(fā) 并行(parallel): 指同一時(shí)刻莺奸,有多條指令在多個(gè)處理器上執(zhí)行 并發(fā)(concur...
    Pauley閱讀 6,132評(píng)論 0 12
  • 人生中有6年的時(shí)間,是最美好的冀宴。那時(shí)的我們最單純灭贷,最天真,最善良花鹅。沒有心機(jī)氧腰,只有一顆“愛”的心。我多么希望時(shí)光可以...
    Yimaid閱讀 194評(píng)論 0 0