go之chan
更新日期:
go语言中的chan非常关键,通过它go线程才能方面的进行通讯和数据交换。chan很像linux系统中的pipe管道,多线程可以同时向里面写信息和读取信息,而且不用加锁(向pipe中一次性写入数据大于一定量时还是要加锁的,而chan不需要)。
使用chan也很方便:1
2
3ch:=make(chan int,5) //创建一个缓冲等于5的chan
ch<-1//向chan中写入1
n,ok<-ch//从chan中读取一个int到n,ok表示ch是否关闭
注意:
1.已关闭的chan不会阻塞,向关闭的chan写入会panic,从关闭的chan读取会直接返回
2.等于nil的chan永远阻塞(读写操作都会阻塞),但是select操作会忽略==nil的chan
可以把对chan的读写放在多个不同的线程里执行,如果没有数据可以读或者写线程则阻塞,直到收到可读写信号开始继续执行。
这里有个问题:线程如何知道chan是否已经关闭了,从而要结束自己的读写工作?
对于读线程很简单,就如上面的语句n,ok<-ch ok==false表示线程关闭
但是写线程应该怎么做呢?在go里,如果向已经关闭的chan里写数据直接会造成panic错误,简单的讲就是宕机了。而我们又不能向读取那样去判断chan是否关闭,因为这样做的话很可能直接从chan中把内容读取了出来,但是我们是写线程,不期望在chan中取内容。所以不禁要问go为什么不提供向读取那样的接口,如果向chan中写内容不成功的话直接返回失败,这样就省了很多事了。但是没有这样的接口,于是只有曲线救国了。
1.首先想到的就是在chan关闭的时候记一个变量,每次在写chan的时候判断下这个变量就好了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25ch:=make(chan int)
isclose:=false
go func(ch chan int){
for{
//lock
if isclose{
break
}
ch<-1
//unlock
}
}
go func(ch chan int){
for{
_,ok:=ch<-ch
if !ok{
break
}
}
}
time.Sleep(10*time.Second)
//lock
isclose=true
close(ch)
//unlock
仔细想想会发现这样也是有问题的,因为判断chan是否关闭和向chan写入是两条语句,在多线程并发时就又可能发生:判断时chan还没关闭,但是开始写入的时候chan已经被其他线程关闭了,这样还是避免不了宕机。所以这样的话还必须在chan关闭时 还有(判断+写入)时加锁,上例里面要在写入线程的for循环里加锁和最后两句加锁。
2.使用waitgroup
在线程写入前waitgroup.add(1) 写入后waitgroup.done(),同时在isclose=true和close(ch)之间调用waitgroup.wait()
这样也能实现chan的安全读写,但本质上也是加锁。
3.使用chan和select1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24ch:=make(chan int)
isclose:=make(chan int)
go func(ch chan int){
for{
select {
case <-isclose:
return
case ch<-1:
continue
}
}
}
go func(ch chan int){
for{
select {
case <-isclose:
return
case n<-ch:
fmt.Println(n)
}
}
}
time.Sleep(10*time.Second)
close(isclose)
上面通过另外一个chan和select实现了对chan的安全读写,但是chan本身并没有被关闭,要等待gc回收……
其实select和chan操作本身也都是有锁的,只是锁的粒度不同
4.利用nil和select