深入学习golang(2)—channel

Channel

1. 概述

“网络,并发”是Go语言的两大feature。Go语言号称“互联网的C语言”,与使用传统的C语言相比,写一个Server所使用的代码更少,也更简单。写一个Server除了网络,另外就是并发,相对python等其它语言,Go对并发支持使得它有更好的性能。

Goroutine和channel是Go在“并发”方面两个核心feature。

Channel是goroutine之间进行通信的一种方式,它与Unix中的管道类似。

Channel声明:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

例如:

var ch chan int

var ch1 chan<- int  //ch1只能写

var ch2 <-chan int  //ch2只能读

channel是类型相关的,也就是一个channel只能传递一种类型。例如,上面的ch只能传递int。

在go语言中,有4种引用类型:slice,map,channel,interface。

Slice,map,channel一般都通过make进行初始化:

ci := make(chan int)            // unbuffered channel of integers

cj := make(chan int, 0)         // unbuffered channel of integers

cs := make(chan *os.File, 100)  // buffered channel of pointers to Files

创建channel时可以提供一个可选的整型参数,用于设置该channel的缓冲区大小。该值缺省为0,用来构建默认的“无缓冲channel”,也称为“同步channel”。

Channel作为goroutine间的一种通信机制,与操作系统的其它通信机制类似,一般有两个目的:同步,或者传递消息。

2. 同步

c := make(chan int)  // Allocate a channel.

// Start the sort in a goroutine; when it completes, signal on the channel.

go func() {

list.Sort()

c <- 1  // Send a signal; value does not matter.

}()

doSomethingForAWhile()

<-c   // Wait for sort to finish; discard sent value.

上面的示例中,在子goroutine中进行排序操作,主goroutine可以做一些别的事情,然后等待子goroutine完成排序。

接收方会一直阻塞直到有数据到来。如果channel是无缓冲的,发送方会一直阻塞直到接收方将数据取出。如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。

3. 消息传递

我们来模拟一下经典的生产者-消费者模型。

func Producer (queue chan<- int){

for i:= 0; i < 10; i++ {

queue <- i

}

}

func Consumer( queue <-chan int){

for i :=0; i < 10; i++{

v := <- queue

fmt.Println("receive:", v)

}

}

func main(){

queue := make(chan int, 1)

go Producer(queue)

go Consumer(queue)

time.Sleep(1e9) //让Producer与Consumer完成

}

上面的示例在Producer中生成数据,在Consumer中处理数据。

4. Server编程模型

在server编程,一种常用的模型:主线程接收请求,然后将请求分发给工作线程,工作线程完成请求处理。用go来实现,如下:

func handle(r *Request) {

process(r)  // May take a long time.

}

func Serve(queue chan *Request) {

for {

req := <-queue

go handle(req)  // Don‘t wait for handle to finish.

}

}

一般来说,server的处理能力不是无限的,所以,有必要限制线程(或者goroutine)的数量。在C/C++编程中,我们一般通过信号量来实现,在go中,我们可以通过channel达到同样的效果:

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {

sem <- 1    // Wait for active queue to drain.

process(r)  // May take a long time.

<-sem       // Done; enable next request to run.

}

func Serve(queue chan *Request) {

for {

req := <-queue

go handle(req)  // Don‘t wait for handle to finish.

}

}

我们通过引入sem channel,限制了同时最多只有MaxOutstanding个goroutine运行。但是,上面的做法,只是限制了运行的goroutine的数量,并没有限制goroutine的生成数量。如果请求到来的速度过快,会导致产生大量的goroutine,这会导致系统资源消耗完全。

为此,我们有必要限制goroutine的创建数量:

func Serve(queue chan *Request) {

for req := range queue {

sem <- 1

go func() {

process(req) // Buggy; see explanation below.

<-sem

}()

}

}

上面的代码看似简单清晰,但在go中,却有一个问题。Go语言中的循环变量每次迭代中是重用的,更直接的说就是req在所有的子goroutine中是共享的,从变量的作用域角度来说,变量req对于所有的goroutine,是全局的。

这个问题属于语言实现的范畴,在C语言中,你不应该将一个局部变量传递给另外一个线程去处理。有很多解决方法,这里有一个讨论。从个人角度来说,我更倾向下面这种方式:

func Serve(queue chan *Request) {

for req := range queue {

sem <- 1

go func(r *Request) {

process(r)

<-sem

}(req)

}

}

至少,这样的代码不会让一个go的初学者不会迷糊,另外,从变量的作用域角度,也更符合常理一些。

在实际的C/C++编程中,我们倾向于工作线程在一开始就创建好,而且线程的数量也是固定的。在go中,我们也可以这样做:

func handle(queue chan *Request) {

for r := range queue {

process(r)

}

}

func Serve(clientRequests chan *Request, quit chan bool) {

// Start handlers

for i := 0; i < MaxOutstanding; i++ {

go handle(clientRequests)

}

<-quit  // Wait to be told to exit.

}

开始就启动固定数量的handle goroutine,每个goroutine都直接从channel中读取请求。这种写法比较简单,但是不知道有没有“惊群”问题?有待后续分析goroutine的实现。

5. 传递channel的channel

channel作为go语言的一种原生类型,自然可以通过channel进行传递。通过channel传递channel,可以非常简单优美的解决一些实际中的问题。

在上一节中,我们主goroutine通过channel将请求传递给工作goroutine。同样,我们也可以通过channel将处理结果返回给主goroutine。

主goroutine:

type Request struct {

args        []int

resultChan  chan int

}

request := &Request{[]int{3, 4, 5}, make(chan int)}

// Send request

clientRequests <- request

// Wait for response.

fmt.Printf("answer: %d\n", <-request.resultChan)

主goroutine将请求发给request channel,然后等待result channel。子goroutine完成处理后,将结果写到result channel。

func handle(queue chan *Request) {

for req := range queue {

result := do_something()

req.resultChan <- result

}

}

6. 多个channel

在实际编程中,经常会遇到在一个goroutine中处理多个channel的情况。我们不可能阻塞在两个channel,这时就该select场了。与C语言中的select可以监控多个fd一样,go语言中select可以等待多个channel。

c1 := make(chan string)

c2 := make(chan string)

go func() {

time.Sleep(time.Second * 1)

c1 <- "one"

}()

go func() {

time.Sleep(time.Second * 2)

c2 <- "two"

}()

for i := 0; i < 2; i++ {

select {

case msg1 := <-c1:

fmt.Println("received", msg1)

case msg2 := <-c2:

fmt.Println("received", msg2)

}

}

在C中,我们一般都会传一个超时时间给select函数,go语言中的select没有该参数,相当于超时时间为0。

主要参考

https://golang.org/doc/effective_go.html

作者:YY哥 
出处:http://www.cnblogs.com/hustcat/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

时间: 2024-11-05 11:46:06

深入学习golang(2)—channel的相关文章

golang控制channel的出入口

golang控制channel的出入口 我们常常使用channel来在多个goroutine之间做数据通讯,但是chan作为函数的入参我们应该怎么写呢?也许有人觉得这个问题比较傻,不过这个还真的是我今天才知道的. 首先我们看看下面的代码: func main() { c := make(chan int) go in(c) go out(c) time.Sleep(time.Second) } func in(c chan int) { for i := 0; i < 10; i++ { c <

学习Golang语言(6):类型--切片

学习Golang语言(1): Hello World 学习Golang语言(2): 变量 学习Golang语言(3):类型--布尔型和数值类型 学习Golang语言(4):类型--字符串 学习Golang语言(5):类型--数组 学习Golang语言(6):类型--切片 在很多应用场景中,数组不能够满足我们的需求.在初始定义数组时,我们并不知道数组所需的长度.因此,我们需要一个大小可以动态变化的数组(动态数组) 在Go语言中,这种"动态数组"成为slice(切片). 但是实际上slic

NIO学习:使用Channel、Buffer写入文件

NIO的效率要高于标准IO,因为NIO将最耗时的IO操作(填充和提取缓冲区)转移会操作系统.NIO以块为单位传输数据,相比标准IO的以字节为单位效率要高很多. 通道和缓冲时NIO的核心对象,每个NIO操作都要使用到它们. 通道是对流的模拟,但与流不同,通道的传输是双向的,一个通道可以同时用于读和写. 缓冲区是一个容器,它包含将要写入或者刚读出的数据.使用通道进行读写时都要经过缓冲区. 使用NIO写入文件,可以通过文件流获取通道 FileOutputStream outputStream=new

【One by one系列】一步步学习Golang web框架Gin

一步步学习Golang web框架Gin 建立项目 go mod 管理依赖 cd $gopath\src\github.com\carfield go mod init 就可以看到在src\github.com\carfield 生成了go.mod文件 module github.com/carfield go 1.13 下载gin包 go get -u github.com/gin-gonic/gin ps:由于众所周知的原因,大概率是下不动,所以请修改代理 修改代理 go env -w GO

学习golang的历程——持续更新

学习一门语言首先hello world 1,首先建立一个目录,新建一个hello.go的文件 package main import ( "fmt" ) func main() { fmt.Println("hello world") } 2,go run hello.go [email protected] study % go run hello.go hello world 上面我们就完成了我们的基础使命.下面我们正式开始入门 在正式进入写代码之前,推荐大家使

Golang的channel使用以及并发同步技巧

在学习<The Go Programming Language>第八章并发单元的时候还是遭遇了不少问题,和值得总结思考和记录的地方. 做一个类似于unix du命令的工具.但是阉割了一些功能,这里应该只实现-c(统计total大小) 和-h(以human比较容易辨识的显示出来)的功能. 首先我们需要构造一个 能够返回FileInfo信息数组的函数,我们把它取名为dirEntries: func dirEntries(dir string) []os.FileInfo { entries, er

golang中channel的超时处理

并发中超时处理是必不可少的,golang没有提供直接的超时处理机制,但可以利用select机制来解决超时问题. func timeoutFunc() { //首先,实现并执行一个匿名的超时等待函数 timeout := make(chan bool, 1) go func() { time.Sleep(1e9) //等待1秒钟 timeout <- true }() //然后,我们把timeout这个channel利用起来 select { case <- ch: //从ch中读到数据 cas

golang goroutine channel [fmt.Println=&gt;String]

初识golang,这部分也不是很了解,百度了一下,做个小记录 goroutine是golang用来做并发处理的,使用起来很简单  go func(){...}(),就是看起来随便用一般而容易go的滥用,所以使用时要仔细斟酌才好. channel 大约是用来在线程间传递数据的,主线程开通一个channel,goroutine往channel中存入内容 1.channel只能用make创建 c := make(chan int) 2.channel中存入数据 c<-2  //把2存入到channel

如何优雅的关闭golang的channel

How to Gracefully Close Channels,这篇博客讲了如何优雅的关闭channel的技巧,好好研读,收获良多. 众所周知,在golang中,关闭或者向已关闭的channel发送数据都会引发panic. 谨遵优雅关闭channel的原则 不要在接受一端关闭channel 不要在有多个并发的senders中关闭channel.反过来说,如果只有一个协程充当sender,那么我们可以在这个sender协程内关闭掉channel. 一个简单的方法 SafeClose type M