golang的channel实现位于src/runtime/chan.go文件。golang中的channel对应的结构是:

// Invariants:
// At least one of c.sendq and c.recvq is empty,
// except for the case of an unbuffered channel with a single goroutine
// blocked on it for both sending and receiving using a select statement,
// in which case the length of c.sendq and c.recvq is limited only by the
// size of the select statement.
//
// For buffered channels, also:
// c.qcount > 0 implies that c.recvq is empty.
// c.qcount < c.dataqsiz implies that c.sendq is empty. type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters // 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
}

注:源码中的raceenabled表示是否启用race detector, 所以,如果你只需要了解channel的机制,可以忽略raceenabled的相关代码。当raceenabled为false时,相关函数位于runtime/race0.go文件,如果raceenabled为true,相关函数代码位于runtime/race.go文件。具体race detector的作用可以参考:

https://blog.golang.org/race-detector

对于这个结构中的成员,其中qcount表示当前buffer中的元素个数,这些元素可以被recv函数(x <- c) 立刻读取; dataqsiz表示buf可以存储的最大个数,这里的buf使用环形队列,dataqsiz就是make函数创建chan时传入的大小;buf指向环形队列,如果dataqsiz为0,或者元素大小为0,buf会象征性地指向一段地址;elemsize表示单个元素的大小;sendx表示send函数(c <- x)处理的buf位置;recvx标识recv函数处理到的buf位置;recvq表示等待recv的go程相关信息;sendq表示等待send的go程相关信息。 lock为保护结构体成员的锁。

下面给出waitq结构体的定义,以及其中成员sudog的定义,其中结构体g (type g struct)表示一个go程信息,用于go程的管理:

type waitq struct {
first *sudog
last *sudog
} // sudog represents a g in a wait list, such as for sending/receiving
// on a channel.
//
// sudogs are allocated from a special pool. Use acquireSudog and
// releaseSudog to allocate and free them.
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g // isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack) ... c *hchan // channel
}

在讲解函数之前,给出一些常量的说明:

  1. maxAlign 用于对齐,在内存中,结构体按照一定字节对齐,访问速度会更快,常见的结构体对齐要求是8字节对齐。
  2. hchanSize 表示,如果hchan以8字节对齐的话,那么chan所占的大小,源码中的:
const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))

等价于:

const hchanSize = unsafe.Sizeof(hchan{})+(maxAlign-1) - ((unsafe.Sizeof(hchan{})+(maxAlgn-1))%maxAlign

简单举例如下,如果unsafe.Sizeof(hchan{})为5,则hchanSize为8, 如果unsafe.Sizeof(hchan{})为8,则hchanSize为8,如果unsafe.Sizeof(hchan{})为10, 则unsafe.Sizeof(hchan{})为16,简单说,就是补足到8的倍数。

下面给出构建chan的函数:

func makechan(t *chantype, size int) *hchan {
  elem := t.elem   // check size and type information
  ...   switch {
  // 特殊情况,例如元素个数为0,元素大小为0等
  ...
  default:
    // Elements contain pointers.
    c = new(hchan)
    c.buf = mallocgc(uintptr(size)*elem.size, elem, true)
  }   c.elemsize = uint16(elem.size)
  c.elemtype = elem
  c.dataqsiz = uint(size)   return c
}

这个函数比较简单,就是构建hchan中的数据成员,在源码中,对于元素个数为0,元素大小为0,以及元素不为指针进行了特殊处理,如果想要理解chan的实现,这些细节可以暂时不关注。

下面先给出在send和recv中使用的等待和唤醒函数:

// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
// unlockf must not access this G’s stack, as it may be moved between
// the call to gopark and the call to unlockf.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
...
} // Puts the current goroutine into a waiting state and unlocks the lock.
// The goroutine can be made runnable again by calling goready(gp).
func goparkunlock(lock *mutex, reason string, traceEv byte, traceskip int) {
gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceSkip)
}

下面给出chansend函数的实现 (c <- x):

// block 用来表示是否阻塞, ep是要处理的元素(elemenet pointer)
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  if c == nil {
    if !block {
      return false
    }
    gopark(nil, nil, “chan send (nil chan)”, traceEvGoStop, 2)
    throw(“unreachable”)
  }
  ...
  // Fast path: check for failed non-blocking operation without acquiring the lock.
  if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
    return false
  }
  ...
  lock(&c.lock)   if c.closed != 0 {
    unlock(&c.lock)
    panic(plainError(“send on closed channel”)
  }   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
  }   if c.qcount < c.dataqsiz {
    // Space is available in the channel buffer. Enqueue the element to send.
    qp := chanbuf(c, c.sendx)
    ...
    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
  }   // Block on the channel. Some receiver will complete our operation for us.
  mysg := acquireSudog()
  ...
  c.sendq.enqueue(mysg)
  goparkunlock(&c.lock, “chan send”, traceEvGoBlockSend, 3)   // someone woke us up
  ...
  releaseSudog(mysg)
  return true
}

由上面的代码可以看出,对于send函数,调用顺序如下:

(1)如果channel为nil,在block为false的情况下,则返回false,否则调用gopark函数。

(2)如果当前队列没有空间,而且没有等待recv的go程,在block为false的情况下,返回false,否则进入下面的流程。

(3)如果有等待recv的go程(说明buf中没有可以recv的值),那么从等待recv的列表中获取第一个go程信息,然后将ep直接发送给那个go程。这里的队列为先入先出队列。

(4)如果buf中有recv的值(说明没有recv的go程在等待),那么将ep写入到buf中,这里也是先写入,先读取。

(5)在block为false的情况下,返回false,否则进入等待,等待recv go程的出现。

上述说明,没有说明如果channel为关闭的情况,如果channel关闭,正常触发panic的异常。

下面给出send函数的实现,根据send函数,可以简单了解go程是如何被唤醒的:

// 在上面的函数调用中 unlockf: func() { unlock(&c.lock) }
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  ...
  if sg.elem != nil {
    sendDirect(c.elemtype, sg, ep)
    sg.elem = nil
  }
  gp := sg.g
  unlockf()
  gp.param = unsafe.Pointer(sg)
  ...
  goready(gp, skip+1)
}

其中上面的goready用来唤醒go程,其中gp的含义大致为go pointer,表示一个go程信息的指针。

下面给出recv函数的定义:

// entry points for <- c from compiled code
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
} func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
} func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
if c == nil {
if !block {
return
}
gopark(nil, nil, “chan receive (nil chan)”, traceEvGoStop, 2)
throw(“unreachable”)
} // Fast path: check for failed non-blocking operation without acquiring the lock.
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
...
lock(&c.lock) if c.closed != 0 && c.qcount == 0 {
...
unlock(&c.lock)
if ep != nil {
  // memory clear
      typedmemclr(c.elemtype, ep)
    }
    return true, false
  }   if sg := c.sendq.dequeue(); sg != nil {
    // Found a waiting sender. If buffer is zero, 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
  }   if c.qcount > 0 {
    // Receive directly from queue
    qp := chanbuf(c, c.recvx)
      ...
    if ep != nil {
      typedmemmove(c.elemtype, ep, qp)
    }
    typedmemclr(c.elemtype, qp)
    c.recv++
    if c.recvx == c.dataqsiz {
      c.recvx = 0
    }
    c.qcount--
    unlock(&c.lock)
    return true, true
  }   if !block {
    unlock(&c.lock)
    return false, false
  }   // no sender available: block on this channel.
  mysg := acquireSudog()
  ...
  c.recvq.enqueue(mysg)
  goparkunlock(&c.lock, “chan receive”, traceEvGoBlockRecv, 3)   // someone woke us up
  ...
  releaseSudog(mysg)
  return true, !closed
}

由上面的代码可以看出,对于recv函数,调用顺序如下:

(1)如果channel为nil,在block为false的情况下,返回(false, false),否则调用gopark函数。

(2)如果buf为空,并且没有等待send的go程,channel没有关闭,在block为false的情况下,返回(false, false), 否则进入下面的流程。

(3)如果当前channel已经关闭,并且buf中没有值,则返回(true, false)。

(4)如果当前有等待send的go程,分为如下两种情况:

  1)buf为空,则直接将send go程发送的信息,发送给调用recv函数的go程

  2)将buf中最先写入的值发送给recv函数的go程,然后将send队列表头的go程中的值写入到buf中

(5)如果当前没有等待send的go程,而且buf中有值,则将第一个写入的值发送给recv函数的go程。

(6)如果当前buf没有值,在block为false的情况下,返回(false, false),否则等待send go程的出现。

从上面的chansend和chanrecv函数的实现来看,可以得出下面的结论:

(1)先调用chansend的go程会第一个被唤醒。

(2)先调用chanrecv的go程会被第一个被唤醒。

(3)先调动chansend的go程写入管道(channel)中的值,会最先从buf中取出。

下面给出close函数的实现:

func closechan(c *hchan) {
  ...
  lock(&c.lock)
  if c.closed != 0 {
    unlock(&c.lock)
    panic(plainError(“close of closed channel”))
  }   c.closed = 1
  var glist *g   // release all readers
  for {
    sg := c.recvq.dequeue()
    ...
    gp.schedlink.set(glist)
    glist = gp
  }   // release all writers (they will panic)
  for {
    sg := c.sendq.dequeue()
    ...
    gp.schedlink.set(glist)
    glist = gp
  }
  unlock(&c.lock)   // Ready all Gs now that we’ve dropped the channel lock
  for glist != nil {
    gp := glist
    glist = glist.schedlink.ptr()
    gp.schedlink = 0
    goready(gp, 3)
  }
}

从closechan的实现来看,closechan会唤醒所有等待向这个channel send和向这个channel recv的go程,但是不会清空buf中的内容。

下面给出一个compiler建立的代码与函数的映射:

// compiler implements
//
// select {
// case c <- v:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if selectnbsend(c, v) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
} // compiler implements
//
// select {
// case v = <-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if selectnbrecv(&v, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
} // compiler implements
//
// select {
// case v, ok = <-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if c != nil && selectnbrecv2(&v, &ok, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}

下面给出操作recvq和sendq队列的函数实现,实际上就是使用链表实现的先入先出队列的实现:

func (q *waitq) enqueue(sgp *sudog) {
sgp.next = nil
x := q.last
if x == nil {
sgp.prev = nil
q.first = sgp
q.last = sgp
return
}
sgp.prev = x
x.next = sgp
q.last = sgp
} func (q *waitq) dequeue() *sudog {
for {
sgp := q.first
if sgp == nil {
return nil
}
y := sgp.next
if y == nil {
q.first = nil
q.last = nil
} else {
y.prev = nil
q.first = y
sgp.next = nil // mark as removed (see dequeueSudog)
} ... return sgp
}
}

关于channel实现的简单讲解就到这里了。如果有什么建议或者提议,欢迎提出。

golang的channel实现的更多相关文章

  1. golang的Channel

    golang的Channel Channel 是 golang 一个非常重要的概念,如果你是刚开始使用 golang 的开发者,你可能还没有真正接触这一概念,本篇我们将分析 golang 的Chann ...

  2. c#实现golang 的channel

    使用.NET的 BlockingCollection<T>来包装一个ConcurrentQueue<T>来实现golang的channel. 代码如下: public clas ...

  3. 【GoLang】golang context channel 详解

    代码示例: package main import ( "fmt" "time" "golang.org/x/net/context" ) ...

  4. 深入学习golang(2)—channel

    Channel 1. 概述 “网络,并发”是Go语言的两大feature.Go语言号称“互联网的C语言”,与使用传统的C语言相比,写一个Server所使用的代码更少,也更简单.写一个Server除了网 ...

  5. golang中channel的超时处理

    并发中超时处理是必不可少的,golang没有提供直接的超时处理机制,但可以利用select机制来解决超时问题. func timeoutFunc() { //首先,实现并执行一个匿名的超时等待函数 t ...

  6. 如何优雅的关闭golang的channel

    How to Gracefully Close Channels,这篇博客讲了如何优雅的关闭channel的技巧,好好研读,收获良多. 众所周知,在golang中,关闭或者向已关闭的channel发送 ...

  7. golang 中 channel 的非阻塞访问方法

    在golang中,基本的channel读写操作都是阻塞的,如果你想要非阻塞的,可以使用如下示例: 即只要在select中加入default,阻塞立即变成非阻塞: package main import ...

  8. golang查看channel缓冲区的长度

    golang提供内建函数cap用于查看channel缓冲区长度. cap的定义如下: func cap(v Type) int The cap built-in function returns th ...

  9. Golang 入门 : channel(通道)

    笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...

随机推荐

  1. Python原生调试工具pdb实践小结

    使用python -m pdb xxx.py进入单步调试模式,默认会在脚本的第一行可执行命令处停止.此时,通过 b function设置之后的函数断点会提示出错,从出错异常栈中可以看出,pdb是将fu ...

  2. Transaction-Mybatis源码

    github地址:https://github.com/dchack/Mybatis-source-code-learn (欢迎star) TransactionFactory 官方文档: 在 MyB ...

  3. ["Visual Studio快捷键" ,"Vs","IDEA快捷键"]

    描述说明 描述 说明 ↑ 方向键.上 ↓ 方向键.下 ← 方向键.左 → 方向键.右 快捷键大比拼 描述 Visual Studio 快捷键 IDEA快捷键 VisualStudio学名 IDEA学名 ...

  4. EasyDSS高性能RTMP、HLS(m3u8)、HTTP-FLV、RTSP流媒体服务器软件二次开发接口对接说明示列

    EasyDSS相关功能 EasyDSS流媒体服务器软件,提供一站式的转码.点播.直播.时移回放服务,极大地简化了开发和集成的工作.其中,点播版本主要包含:上传.转码.分发.直播版本主要包含:直播.录像 ...

  5. FinalShell—一体化服务器管理软件(SSH客户端)

    下面附上一些截图和官方连接: 官网:http://www.hostbuf.com/ FinalShell是一体化的的服务器,网络管理软件,不仅是ssh客户端,还是功能强大的开发,运维工具,充分满足开发 ...

  6. MQTT的Res接口发布消息

    MQTT(这里采用的V2版本)发布消息的常见方法: 1.通过MQTT客户端连接MQTT服务器,建立长连接,通过接口发布消息 最常见的客户端: <dependency> <groupI ...

  7. SQL Server 使用文件组备份降低备份文件占用的存储空间

    对于DBA来说,备份和刷新简历是最重要的两项工作,如果发生故障后,发现备份也不可用,那么刷新简历的重要性就显现出来,哇咔咔!当然备份是DBA最重要的事情(没有之一),在有条件的情况下,我们应该在多个服 ...

  8. day07——数据类型补充、坑、二次编码

    day07 数据类型补充 str 首字母大写:capitalize() name = 'alex' name1 = name.capitalize() print(name1) 每个单词首字母大写:t ...

  9. day52——jquery引入与下载、标签查找、操作标签

    day52 jquery引入 下载链接:jQuery官网 https://jquery.com/ 中文文档:jQuery AP中文文档 http://jquery.cuishifeng.cn/ < ...

  10. Stack实现

    栈的三种操作算法很简单 STACK-EMPTY(S) 1 if S.top == 0 2    return TRUE 3 else return FALSE PUSH(S, x) 1 S.top = ...