Go Chan 源碼解析

本篇文章內(nèi)容基于go1.14.2分析

golang的chan是一個(gè)內(nèi)置類型咨油,作為csp編程的核心數(shù)據(jù)結(jié)構(gòu)析二,其底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)叫hchan的struct:

type hchan struct {
    qcount   uint           // 隊(duì)列中的元素?cái)?shù)量
    dataqsiz uint           // (環(huán)形)隊(duì)列的大小
    buf      unsafe.Pointer // 隊(duì)列的指針
    elemsize uint16 // 元素大小
    closed   uint32 // 是否已close
    elemtype *_type // 元素類型
    sendx    uint   // 環(huán)形隊(duì)列中惫周,send的位置
    recvx    uint   // 環(huán)形隊(duì)列中 recv的位置
    recvq    waitq  // 讀取等待隊(duì)列
    sendq    waitq  // 發(fā)送等待隊(duì)列
    lock mutex // 互斥鎖
}
image

如圖所示钥勋,chan最核心的部分由一個(gè)環(huán)形隊(duì)列和2個(gè)waitq組成,環(huán)形隊(duì)列用于存放數(shù)據(jù)(帶緩沖的情況下)科阎,waitq用于實(shí)現(xiàn)阻塞和恢復(fù)goroutine述吸。

chan的相關(guān)操作

對(duì)chan的操作有:make、讀锣笨、寫(xiě)蝌矛、close,當(dāng)然還有select错英,這里只討論前面四個(gè)操作入撒。

創(chuàng)建 chan

當(dāng)在代碼中使用make創(chuàng)建chan時(shí),編譯器會(huì)根據(jù)情況自動(dòng)替換成makechan64 或者makechan走趋,makechan64 其實(shí)還是調(diào)用了makechan函數(shù)衅金。

func makechan(t *chantype, size int) *hchan {
    elem := t.elem
    
  // 確保元素類型的size < 2^16噪伊,
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
  // 檢查內(nèi)存對(duì)齊
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

  // 計(jì)算緩沖區(qū)所需分配內(nèi)存大小
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

    var c *hchan
    switch {
    case mem == 0:
        // 即不帶緩沖區(qū)的情況簿煌,只需要調(diào)用mallocgc分配
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // 理解為空地址
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // 元素類型不包含指針的情況
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // 默認(rèn)情況下:包含指針
        c = new(hchan)
        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, "; dataqsiz=", size, "\n")
    }
    return c
}

chan 寫(xiě)操作

當(dāng)對(duì)chan進(jìn)行寫(xiě)入“ch <- interface{}” 時(shí),會(huì)被編譯器替換成chansend1函數(shù)的調(diào)用,最終還是調(diào)用了chansend函數(shù):

image
//elem 是待寫(xiě)入元素的地址
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

先看看chansend的函數(shù)簽名鉴吹,只需關(guān)注ep和block這個(gè)兩個(gè)參數(shù)即可姨伟,ep是要寫(xiě)入數(shù)據(jù)的地址,block表示是否阻塞式的調(diào)用

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

chansend有以下幾種處理流程:

  1. 當(dāng)對(duì)一個(gè)nil chan進(jìn)行寫(xiě)操作時(shí)豆励,如果是非阻塞調(diào)用夺荒,直接返回瞒渠;否則將當(dāng)前協(xié)程掛起

    // chansend 對(duì)一個(gè) nil chan發(fā)送數(shù)據(jù)時(shí),如果是非阻塞則直接返回技扼,否則將當(dāng)前協(xié)程掛起
    if c == nil {
         if !block {
             return false
         }
         gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
         throw("unreachable")
     }
    
  2. 非阻塞模式且chan未close伍玖,沒(méi)有緩沖區(qū)且沒(méi)有等待接收或者緩沖區(qū)滿的情況下,直接return false剿吻。

    // 1. 非阻塞模式且chan未close
      // 2. 沒(méi)有緩沖區(qū)且沒(méi)有等待接收 或者 緩沖區(qū)滿的情況下
      // 滿足以上條件直接return false
    if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
         (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
         return false
     }
    
  3. c.recvq中有等待讀的接收者窍箍,將其出隊(duì),將數(shù)據(jù)直接copy給接收者丽旅,并喚醒接收者椰棘。

    // 有等待的接收的goroutine
     // 出隊(duì),傳遞數(shù)據(jù)
     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
     }
    

    recvq是一個(gè)雙向鏈表榄笙,每個(gè)sudog會(huì)關(guān)聯(lián)上一個(gè)reader(被阻塞的g)

    image

    當(dāng)sudog出隊(duì)后邪狞,會(huì)調(diào)用send方法,通過(guò)sendDirect 實(shí)現(xiàn)數(shù)據(jù)在兩個(gè)地址之間拷貝茅撞,最后調(diào)用goready喚醒reader(被阻塞的g)

    func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
     // ... 剔除無(wú)關(guān)代碼
     if sg.elem != nil {
         // 直接將數(shù)據(jù)拷貝到變量ep所在的地址
         sendDirect(c.elemtype, sg, ep)
         sg.elem = nil
     }
     gp := sg.g
     unlockf()
     gp.param = unsafe.Pointer(sg)
     if sg.releasetime != 0 {
         sg.releasetime = cputicks()
     }
     //將reader的goroutine喚起
     goready(gp, skip+1)
    }
    
    
  4. 緩沖區(qū)未滿的情況下帆卓,數(shù)據(jù)放入環(huán)形緩沖區(qū)即可。

     // 緩沖區(qū)未滿
     // 將數(shù)據(jù)放到緩沖區(qū)
     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
     }
    
  1. 緩沖區(qū)已滿米丘,阻塞模式下關(guān)聯(lián)一個(gè)sudog數(shù)據(jù)結(jié)構(gòu)并進(jìn)入c.sendq隊(duì)列鳞疲,掛起當(dāng)前協(xié)程。

     // 阻塞的情況
     gp := getg() //拿到當(dāng)前g
     mysg := acquireSudog() // 獲取一個(gè)sudog
     mysg.releasetime = 0
     if t0 != 0 {
         mysg.releasetime = -1
     
     mysg.elem = ep //關(guān)聯(lián)ep蠕蚜,即待寫(xiě)入的數(shù)據(jù)地址
     mysg.waitlink = nil
     mysg.g = gp
     mysg.isSelect = false
     mysg.c = c
     gp.waiting = mysg
     gp.param = nil
     c.sendq.enqueue(mysg) // 入隊(duì)
     // Signal to anyone trying to shrink our stack that we're about
     // to park on a channel. The window between when this G's status
     // changes and when we set gp.activeStackChans is not safe for
     // stack shrinking.
     atomic.Store8(&gp.parkingOnChan, 1)
     // 將g休眠尚洽,讓出cpu
      // gopark后,需等待reader來(lái)喚醒它
     gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
     // 喚醒過(guò)后
     // 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.
     // 保持?jǐn)?shù)據(jù)不被回收
     KeepAlive(ep)
    
     // someone woke us up.
     if mysg != gp.waiting {
         throw("G waiting list is corrupted")
     }
     gp.waiting = nil
     gp.activeStackChans = false
     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
     releaseSudog(mysg)
     return true
    

chan 讀操作

當(dāng)對(duì)chan進(jìn)行讀操作時(shí)靶累,編譯器會(huì)替換成 chanrecv1或者chanrecv2函數(shù)腺毫,最終會(huì)調(diào)用chanrecv函數(shù)處理讀取

image
// v := <- ch
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}
// v, ok := <- ch
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

和chansend一樣,chanrecv也是支持非阻塞式的調(diào)用

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) 

chanrecv有以下幾種處理流程:

  1. 讀nil chan挣柬,如果是非阻塞潮酒,直接返回;如果是阻塞式邪蛔,將當(dāng)前協(xié)程掛起急黎。

     // 讀阻塞
     if c == nil {
         if !block {
             return
         }
         gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
         throw("unreachable")
     }
    
  2. 非阻塞模式下,沒(méi)有緩沖區(qū)且沒(méi)有等待寫(xiě)的writer或者緩沖區(qū)沒(méi)數(shù)據(jù)侧到,直接返回勃教。

     if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
         c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
         atomic.Load(&c.closed) == 0 {
         return
     }
    
  3. chan已經(jīng)被close,并且隊(duì)列中沒(méi)有數(shù)據(jù)時(shí)匠抗,會(huì)將存放值的變量清零故源,然后返回。

     // c已經(jīng)被close 并且 沒(méi)有數(shù)據(jù)
     // 清除ep指針
     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
     }
    
  4. sendq中有等待的writer汞贸,writer出隊(duì)绳军,并調(diào)用recv函數(shù)

    // 從sendq中取出sender
     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).
         // 從sender中讀取數(shù)據(jù)
         recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
         return true, true
     }
    
    image

    recv在這分兩種處理:如果ch不帶緩沖區(qū)的話印机,直接將writer的sg.elem數(shù)據(jù)拷貝到ep;如果帶緩沖區(qū)的話门驾,此時(shí)緩沖區(qū)肯定滿了射赛,那么就從緩沖區(qū)隊(duì)列頭部取出數(shù)據(jù)拷貝至ep,然后將writer的sg.elem數(shù)據(jù)拷貝到緩沖區(qū)中奶是,最后喚醒writer(g)

    func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
       // 不帶緩沖區(qū)的情況
       // 直接copy from sender
       if c.dataqsiz == 0 {
          if raceenabled {
             racesync(c, sg)
          }
          if ep != nil {
             // copy data from sender
             recvDirect(c.elemtype, sg, ep)
          }
       } else {
          // Queue is full. Take the item at the
          // head of the queue. Make the sender enqueue
          // its item at the tail of the queue. Since the
          // queue is full, those are both the same slot.
          // 隊(duì)列已滿
          // 隊(duì)列元素出隊(duì)
          qp := chanbuf(c, c.recvx)
          if raceenabled {
             raceacquire(qp)
             racerelease(qp)
             raceacquireg(sg.g, qp)
             racereleaseg(sg.g, qp)
          }
          // copy data from queue to receiver
          // 數(shù)據(jù)拷貝給ep
          if ep != nil {
             typedmemmove(c.elemtype, ep, qp)
          }
          // copy data from sender to queue
          // 將sender的數(shù)據(jù)拷貝到這個(gè)槽中
          typedmemmove(c.elemtype, qp, sg.elem)
          c.recvx++
          if c.recvx == c.dataqsiz {
             c.recvx = 0
          }
          c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
       }
       // 置空
       sg.elem = nil
       gp := sg.g
       unlockf()
       gp.param = unsafe.Pointer(sg)
       if sg.releasetime != 0 {
          sg.releasetime = cputicks()
       }
       // 喚醒sender協(xié)程
       goready(gp, skip+1)
    }
    
  5. 直接從緩沖隊(duì)列中讀數(shù)咒劲。

     // 帶緩沖區(qū)
     if c.qcount > 0 {
         // Receive directly from queue
         // 直接buf中取
         qp := chanbuf(c, c.recvx)
         if raceenabled {
             raceacquire(qp)
             racerelease(qp)
         }
         // 拷貝數(shù)據(jù)到ep指針
         if ep != nil {
             typedmemmove(c.elemtype, ep, qp)
         }
         // 清除qp
         typedmemclr(c.elemtype, qp)
         c.recvx++
         if c.recvx == c.dataqsiz {
             c.recvx = 0
         }
         c.qcount--
         unlock(&c.lock)
         return true, true
     }
    
  6. 阻塞的情況,緩沖區(qū)沒(méi)有數(shù)據(jù)诫隅,且沒(méi)有writer

    
     // 阻塞
     gp := getg() //拿到當(dāng)前的goroutine
     mysg := acquireSudog() // 獲取一個(gè)sudog
     mysg.releasetime = 0
     if t0 != 0 {
         mysg.releasetime = -1
     }
     
     //sudog 關(guān)聯(lián)
     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) //入隊(duì)
     // Signal to anyone trying to shrink our stack that we're about
     // to park on a channel. The window between when this G's status
     // changes and when we set gp.activeStackChans is not safe for
     // stack shrinking.
     atomic.Store8(&gp.parkingOnChan, 1)
      // 掛起當(dāng)前goroutine腐魂,等待writer喚醒
     gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
    
     // 喚醒后
     if mysg != gp.waiting {
         throw("G waiting list is corrupted")
     }
     gp.waiting = nil
     gp.activeStackChans = false
     if mysg.releasetime > 0 {
         blockevent(mysg.releasetime-t0, 2)
     }
     closed := gp.param == nil
     gp.param = nil
     // sudog解除關(guān)聯(lián)
     mysg.c = nil
      // 釋放sudog
     releaseSudog(mysg)
    
    

close 關(guān)閉操作

當(dāng)close一個(gè)chan時(shí),編譯器會(huì)替換成對(duì)closechan函數(shù)的調(diào)用逐纬,將closed字段置為1蛔屹,并將recvq和sendq中的goroutine釋放喚醒,對(duì)sendq中未寫(xiě)入的數(shù)據(jù)做清除豁生,且writer會(huì)發(fā)生panic異常兔毒。

func closechan(c *hchan) {
    if c == nil {
        panic(plainError("close of nil channel"))
    }
    
  // 加鎖
    lock(&c.lock)
  // 不可重復(fù)close
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    if raceenabled {
        callerpc := getcallerpc()
        racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
        racerelease(c.raceaddr())
    }

    c.closed = 1

    var glist gList

    // 釋放所有的
    for {
        // 出隊(duì)
        sg := c.recvq.dequeue()
        if sg == nil {
            break
        }
        // 清零
        if sg.elem != nil {
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }

    // 釋放所有writer
    for {
        // 出隊(duì)
        sg := c.sendq.dequeue()
        if sg == nil {
            break
        }
        // 丟棄數(shù)據(jù)
        sg.elem = nil
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
    unlock(&c.lock)

    // 喚醒所有g(shù)
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3)
    }
}

chan使用小技巧

  1. 避免read、write一個(gè)nil chan

    func main() {
     ch := make(chan int,1)
    
     go func() {
         time.Sleep(1*time.Second)
         ch = nil
     }()
    
     ch<-1 
     ch<-1 // 協(xié)程直接掛起
    }
    
  2. 從chan中read時(shí)甸箱,使用帶指示的訪問(wèn)方式育叁,讀取的時(shí)候無(wú)法感知到close的關(guān)閉

    func main() {
     ch := make(chan int)
    
     go func() {
         ch <- 10
         close(ch)
     }()
    
     for {
         select {
          // case i, ok := <-ch:
          // if ok {
          //  break
          //}
             case i := <-ch:
                 fmt.Println(i)
                 time.Sleep(100 * time.Millisecond)
         }
     }
    }
    
  3. 從chan中read時(shí),不要使用已存在變量接收, chan close之后芍殖,緩沖區(qū)沒(méi)有數(shù)據(jù)的話豪嗽,使用存在變量讀取時(shí),會(huì)將變量清零

    func main() {
     a := 10
     ch := make(chan int,1)
    
     fmt.Println("before close a is: ", a) // a is 10
     close(ch)
     a = <-ch 
     fmt.Println("after close a is: ", a) // a is 0
    }
    
  1. 使用select+default可以實(shí)現(xiàn) chan的無(wú)阻塞讀取

    // 使用select反射包實(shí)現(xiàn)無(wú)阻塞讀寫(xiě)
    func tryRead(ch chan int) (int, bool) {
     var cases []reflect.SelectCase
     caseRead := reflect.SelectCase{
         Dir:  reflect.SelectRecv,
         Chan: reflect.ValueOf(ch),
     }
    
     cases = append(cases, caseRead)
     cases = append(cases, reflect.SelectCase{
         Dir: reflect.SelectDefault,
     })
    
     _, v, ok := reflect.Select(cases)
    
     if ok {
    
         return (v.Interface()).(int), ok
     }
    
     return 0, ok
    }
    
    func tryWrite(ch chan int, data int) bool {
     var cases []reflect.SelectCase
     caseWrite := reflect.SelectCase{
         Dir:  reflect.SelectSend,
         Chan: reflect.ValueOf(ch),
         Send: reflect.ValueOf(data),
     }
    
     cases = append(cases, caseWrite)
     cases = append(cases, reflect.SelectCase{
         Dir: reflect.SelectDefault,
     })
     chosen, _, _ := reflect.Select(cases)
    
     return chosen == 0
    }
    
    // 使用select + default實(shí)現(xiàn)無(wú)阻塞讀寫(xiě)
    func tryRead2(ch chan int) (int, bool) {
     select {
     case v, ok := <-ch:
         return v, ok
     default:
         return 0, false
     }
    }
    
    func tryWrite2(ch chan int, data int) bool {
     select {
     case ch <- data:
         return true
     default:
         return false
     }
    }
    
    

    原因是如果select的case中存在default豌骏,對(duì)chan的讀寫(xiě)會(huì)使用無(wú)阻塞的方法

    func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
     return chansend(c, elem, false, getcallerpc())
    }
    
    func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
     selected, _ = chanrecv(c, elem, false)
     return
    }
    
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末龟梦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窃躲,更是在濱河造成了極大的恐慌计贰,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒂窒,死亡現(xiàn)場(chǎng)離奇詭異躁倒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)洒琢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)秧秉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人纬凤,你說(shuō)我怎么就攤上這事福贞×媒溃” “怎么了停士?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵挖帘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我恋技,道長(zhǎng)拇舀,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任蜻底,我火速辦了婚禮骄崩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘薄辅。我一直安慰自己要拂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布站楚。 她就那樣靜靜地躺著脱惰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窿春。 梳的紋絲不亂的頭發(fā)上拉一,一...
    開(kāi)封第一講書(shū)人閱讀 50,096評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音旧乞,去河邊找鬼蔚润。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尺栖,可吹牛的內(nèi)容都是我干的嫡纠。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼延赌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼货徙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起皮胡,我...
    開(kāi)封第一講書(shū)人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤痴颊,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后屡贺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蠢棱,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年甩栈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泻仙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡量没,死狀恐怖玉转,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情殴蹄,我是刑警寧澤究抓,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布猾担,位于F島的核電站,受9級(jí)特大地震影響刺下,放射性物質(zhì)發(fā)生泄漏绑嘹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一橘茉、第九天 我趴在偏房一處隱蔽的房頂上張望工腋。 院中可真熱鬧,春花似錦畅卓、人聲如沸擅腰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惕鼓。三九已至,卻和暖如春唐础,著一層夾襖步出監(jiān)牢的瞬間箱歧,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工一膨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呀邢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓豹绪,卻偏偏與公主長(zhǎng)得像价淌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞒津,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

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

  • 簡(jiǎn)介 熟悉Go的人都知道蝉衣,它提倡著不要通過(guò)共享內(nèi)存來(lái)通訊,而要通過(guò)通訊來(lái)共享內(nèi)存巷蚪。Go提供了一種獨(dú)特的并發(fā)同步技術(shù)...
    marsjhe閱讀 2,633評(píng)論 0 2
  • 設(shè)計(jì)原理 目前的 Channel 收發(fā)操作均遵循了先進(jìn)先出的設(shè)計(jì)病毡,具體規(guī)則如下: 先從 Channel 讀取數(shù)據(jù)的...
    Xuenqlve閱讀 1,597評(píng)論 0 0
  • channel是golang中特有的一種數(shù)據(jù)結(jié)構(gòu),通常與goroutine一起使用屁柏,下面我們就介紹一下這種數(shù)據(jù)結(jié)構(gòu)...
    cfanbo閱讀 284評(píng)論 0 0
  • 前言 Golang在并發(fā)編程上有兩大利器啦膜,分別是channel和goroutine,這篇文章我們先聊聊channe...
    即將禿頭的Java程序員閱讀 1,092評(píng)論 0 2
  • 簡(jiǎn)介(js) 通道(channel) 是Go實(shí)現(xiàn)CSP并發(fā)模型的關(guān)鍵淌喻, 鼓勵(lì)用通信來(lái)實(shí)現(xiàn)數(shù)據(jù)共享僧家。 Dont' c...
    darcyaf閱讀 273評(píng)論 0 0