一文读透GO语言的通道

channel是GO语言并发体系中的主推的通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。Go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
channel是一种特殊的类型是保证协程安全的,也就是在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。而且遵循先入先出(First In First Out)的规则,保证收发数据的顺序。这两个特性是channel可以产生共享内存功能的重要原因。。读完这一讲,下面我们就可以继续我们的例子,开始GO语言并发的实战了
一、通道的声明
1.经典方式声明
通过使用chan类型,其声明方式如下:
var name chan type
其中type表示通道内的数据类型;name:通道的变量名称,不过这样创建的通道只是空值 nil,一般来说都是通道都是通过make函数创建的。
2.make方式
make函数可以创建通道格式如下:
name := make(chan type)
3.创建带有缓冲的通道
后面会讲到缓冲通道的概念,这里先说他的定义方式
name := make(chan type, size)
其中type表示通道内的数据类型;name:通道的变量名称,size代表缓冲的长度。
二、通道的数据收发
1. 通道的数据发送
通道的发送的操作符<-,将数据通过通道发送的格式为:
chan <- value
注意,如果将数据发送至一个无缓冲的通道中,如果数据一直都没有接收,那么发送操作将持续阻塞。但是GO的编译器能够发现明显的错误,比如
ch := make(chan int) // 创建一个整型通道
ch <- 0// 尝试将0通过通道发送
编译时会报错:fatal error: all goroutines are asleep - deadlock!
2.通道的数据接收
通道接收数据的操作符也是<-,具体有以下几种方式
1) 阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
如需要忽略接收的数据,则将data变量省略,具体格式如下:
<-ch
2) 非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。一般只配合select语句配合定时器做超时检测时使用。
三、channel的超时检测
Go语言没有提供直接的超时处理机制,一般使用select关键字来设置超时。Select虽然不是专为超时而设计的,却能很方便的解决超时问题,因为select的特点是只要其中有一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。
但是与switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作,结构如下:

select {
    case <-chan1:
    // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行该case处理语句
    default:
    // 如果上面都没有成功,则进入default处理流程
}


比如在示例代码中,我们创建了一个用于传数据的channel,和一个用于超时退出的通道quit,并使用select来接收channel的数据,其中如果程序运行到3s时,退出通道会被置为true,这时<-quit不再阻塞,主goroutine退出。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)    //设置一个传送数据的channel
	quit := make(chan bool) //设置一个超时传送的channel
	//新开一个协程
	go func() {
		for {
			select {
			case num := <-ch: //如果收到传输能道ch中的值则打印.
				fmt.Println("num = ", num)
			case <-time.After(3 * time.Second): //如果达到3秒则认定超时
				fmt.Println("超时")
				quit <- true //将quit通道置为true
			}
		}
	}() //别忘了()
	for i := 0; i < 5; i++ {
		ch <- i
		time.Sleep(time.Second)
	}
	<-quit //当超时quit将不再阻塞,主协和将会退出
	fmt.Println("程序结束")
	/*运行结果为
		num =  0
	num =  1
	num =  2
	num =  3
	num =  4
	超时
	程序结束
	*/
}


四、通道数据收发的注意事项
1.通道的收发操作在不同的两个 goroutine 间进行。由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。
2.接收将持续阻塞直到发送方发送数据。如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
3.每次只接收一个元素。
第4节 深入理解GO语言中的channel
GO语言中的有关chan的代码位置在GOPATH\src\runtime\chan.go,阅读代码可以发现channel 内部就是一个带锁的队列。
1.基本数据结构

type hchan struct {
    qcount   uint           // 队列中数据个数
    dataqsiz uint           // channel 大小
    buf      unsafe.Pointer // 存放数据的环形数组
    elemsize uint16         // channel 中数据类型的大小
    closed   uint32         // 表示 channel 是否关闭
    elemtype *_type // 元素数据类型
    sendx    uint   // send 的数组索引
    recvx    uint   // recv 的数组索引
    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
}


如果是一个无缓冲的chan只需要用一个 lock来确保无竞争冲突。而带缓冲的chan其实就是一个环形队列,通过sendx 和 recvx 分别用来记录发送、接收的位置。
2.数据发送的实现
其实数据发送和接收的逻辑比类似,这里我们只举数据发送的例子。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {//正确性检查
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

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

	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
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	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)
		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
	}

	// Block on the channel. Some receiver will complete our operation for us.
	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
	c.sendq.enqueue(mysg)
	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
	releaseSudog(mysg)
	return true
}


我们看到在发送数据时,首先获取lock,然后进行竞争检查、指针检查等,然后更新读写位置,记录数据,并释放锁,如果,当前hchan.buf 无可用空间,则将操作阻塞。
所以从本质上讲channel就是一个基于锁的循环队列。
 

上一篇:Go语言 协程配合管道的综合案例-2


下一篇:基本输入输出系统BIOS---显示输出