深入学习golang(2)—channel
本文内容纲要:
-Channel
-1.概述
-2.同步
-3.消息传递
-4.Server编程模型
-5.传递channel的channel
-6.多个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.
例如:
varchchanint
varch1chan<-int//ch1只能写
varch2<-chanint//ch2只能读
channel是类型相关的,也就是一个channel只能传递一种类型。例如,上面的ch只能传递int。
在go语言中,有4种引用类型:slice,map,channel,interface。
Slice,map,channel一般都通过make进行初始化:
ci:=make(chanint)//unbufferedchannelofintegers
cj:=make(chanint,0)//unbufferedchannelofintegers
cs:=make(chan*os.File,100)//bufferedchannelofpointerstoFiles
创建channel时可以提供一个可选的整型参数,用于设置该channel的缓冲区大小。该值缺省为0,用来构建默认的“无缓冲channel”,也称为“同步channel”。
Channel作为goroutine间的一种通信机制,与操作系统的其它通信机制类似,一般有两个目的:同步,或者传递消息。
2.同步
c:=make(chanint)//Allocateachannel.
//Startthesortinagoroutine;whenitcompletes,signalonthechannel.
gofunc(){
list.Sort()
c<-1//Sendasignal;valuedoesnotmatter.
}()
doSomethingForAWhile()
<-c//Waitforsorttofinish;discardsentvalue.
上面的示例中,在子goroutine中进行排序操作,主goroutine可以做一些别的事情,然后等待子goroutine完成排序。
接收方会一直阻塞直到有数据到来。如果channel是无缓冲的,发送方会一直阻塞直到接收方将数据取出。如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。
3.消息传递
我们来模拟一下经典的生产者-消费者模型。
funcProducer(queuechan<-int){
fori:=0;i<10;i++{
queue<-i
}
}
funcConsumer(queue<-chanint){
fori:=0;i<10;i++{
v:=<-queue
fmt.Println("receive:",v)
}
}
funcmain(){
queue:=make(chanint,1)
goProducer(queue)
goConsumer(queue)
time.Sleep(1e9)//让Producer与Consumer完成
}
上面的示例在Producer中生成数据,在Consumer中处理数据。
4.Server编程模型
在server编程,一种常用的模型:主线程接收请求,然后将请求分发给工作线程,工作线程完成请求处理。用go来实现,如下:
funchandle(r*Request){
process(r)//Maytakealongtime.
}
funcServe(queuechan*Request){
for{
req:=<-queue
gohandle(req)//Don'twaitforhandletofinish.
}
}
一般来说,server的处理能力不是无限的,所以,有必要限制线程(或者goroutine)的数量。在C/C++编程中,我们一般通过信号量来实现,在go中,我们可以通过channel达到同样的效果:
varsem=make(chanint,MaxOutstanding)
funchandle(r*Request){
sem<-1//Waitforactivequeuetodrain.
process(r)//Maytakealongtime.
<-sem//Done;enablenextrequesttorun.
}
funcServe(queuechan*Request){
for{
req:=<-queue
gohandle(req)//Don'twaitforhandletofinish.
}
}
我们通过引入semchannel,限制了同时最多只有MaxOutstanding个goroutine运行。但是,上面的做法,只是限制了运行的goroutine的数量,并没有限制goroutine的生成数量。如果请求到来的速度过快,会导致产生大量的goroutine,这会导致系统资源消耗完全。
为此,我们有必要限制goroutine的创建数量:
funcServe(queuechan*Request){
forreq:=rangequeue{
sem<-1
gofunc(){
process(req)//Buggy;seeexplanationbelow.
<-sem
}()
}
}
上面的代码看似简单清晰,但在go中,却有一个问题。Go语言中的循环变量每次迭代中是重用的,更直接的说就是req在所有的子goroutine中是共享的,从变量的作用域角度来说,变量req对于所有的goroutine,是全局的。
这个问题属于语言实现的范畴,在C语言中,你不应该将一个局部变量传递给另外一个线程去处理。有很多解决方法,这里有一个讨论。从个人角度来说,我更倾向下面这种方式:
funcServe(queuechan*Request){
forreq:=rangequeue{
sem<-1
gofunc(r*Request){
process(r)
<-sem
}(req)
}
}
至少,这样的代码不会让一个go的初学者不会迷糊,另外,从变量的作用域角度,也更符合常理一些。
在实际的C/C++编程中,我们倾向于工作线程在一开始就创建好,而且线程的数量也是固定的。在go中,我们也可以这样做:
funchandle(queuechan*Request){
forr:=rangequeue{
process(r)
}
}
funcServe(clientRequestschan*Request,quitchanbool){
//Starthandlers
fori:=0;i<MaxOutstanding;i++{
gohandle(clientRequests)
}
<-quit//Waittobetoldtoexit.
}
开始就启动固定数量的handlegoroutine,每个goroutine都直接从channel中读取请求。这种写法比较简单,但是不知道有没有“惊群”问题?有待后续分析goroutine的实现。
5.传递channel的channel
channel作为go语言的一种原生类型,自然可以通过channel进行传递。通过channel传递channel,可以非常简单优美的解决一些实际中的问题。
在上一节中,我们主goroutine通过channel将请求传递给工作goroutine。同样,我们也可以通过channel将处理结果返回给主goroutine。
主goroutine:
typeRequeststruct{
args[]int
resultChanchanint
}
request:=&Request{[]int{3,4,5},make(chanint)}
//Sendrequest
clientRequests<-request
//Waitforresponse.
fmt.Printf("answer:%d\n",<-request.resultChan)
主goroutine将请求发给requestchannel,然后等待resultchannel。子goroutine完成处理后,将结果写到resultchannel。
funchandle(queuechan*Request){
forreq:=rangequeue{
result:=do_something()
req.resultChan<-result
}
}
6.多个channel
在实际编程中,经常会遇到在一个goroutine中处理多个channel的情况。我们不可能阻塞在两个channel,这时就该select场了。与C语言中的select可以监控多个fd一样,go语言中select可以等待多个channel。
c1:=make(chanstring)
c2:=make(chanstring)
gofunc(){
time.Sleep(time.Second*1)
c1<-"one"
}()
gofunc(){
time.Sleep(time.Second*2)
c2<-"two"
}()
fori:=0;i<2;i++{
select{
casemsg1:=<-c1:
fmt.Println("received",msg1)
casemsg2:=<-c2:
fmt.Println("received",msg2)
}
}
在C中,我们一般都会传一个超时时间给select函数,go语言中的select没有该参数,相当于超时时间为0。
主要参考
https://golang.org/doc/effective_go.html
作者:YY哥
出处:http://www.cnblogs.com/hustcat/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
本文内容总结:Channel,1.概述,2.同步,3.消息传递,4.Server编程模型,5.传递channel的channel,6.多个channel,
原文链接:https://www.cnblogs.com/hustcat/p/4003729.html