go语言并发编程(channel)
Posted 开源你我
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go语言并发编程(channel)相关的知识,希望对你有一定的参考价值。
一.channel简介
channel是go语言在语言级别提供的goroutine间的通信,我们可以使用channel在两个或者多个协程之间来传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如指针等。
channel是类型相关的,也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。可以将其认为是一种类型安全的管道。
channel案例:
package main
import "fmt"
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main(){
chs := make([]chan int , 10)
for i := 0; i < 10; i++{
chs[i] = make(chan int)
go Count( chs[i])
}
for _, ch := range(chs) {
<-ch
}
}
二.channel深入
1.channel的基本语法
channel的声明的基本语法形式:
var chanName chan ElementType
与一般的变量声明不同的地方仅仅是在类型之前加了一个chan关键字,ElementType指的是这个channel所能传递的数据类型。
var ch chan int //这个声明方式只能传递in型数据
或者,咱们声明一个map,元素是bool型的channel
var m map[string] chan bool //语句合法
定义一个channel也很简单,直接使用内置的make函数
ch := make(chan int)
在channel用法中,最常见的形式包含读出和读入,将一个数据写入(发送)到channel的语法:
ch <- value
向channel写入数据通常会导致阻塞,直到有其他的协程从channel中读取数据,从channel中读取数据的语法:
value := <-ch
当然,如果channel之前没有写入数据的话,那么从channel中读取数据也会导致程序阻塞,直到channel中有数据写入为止。通过一定方式也可以控制channel读和写。
2.Select
早在Unix时代,select机制就已经被引入,通过调用select函数来控制一系列文件句柄,一旦其中一个文件句柄发生了IO动作,该select()调用就会被返回,后来这个机制用来实现多路IO,在Socket中实现并发操作,经常会和其他的一些IO操作做比较,如poll和epoll。go语言也支持在语言级别的select关键字,用于处理异步IO。
select的用法与switch的用法类似,由select开始一个新的选择块,每个选择块由case语句描述。与switch语句可以选择任何使用相等比较的条件相比,select有比较的多的限制,其中最大的一条限制就是每一个case语句里必须是一个IO操作,大致的结构如下:
select (
case <- chan1:
//如果有成功读到数据,则进行该case处理语句
case chan2<- 1:
//如果有成功写入数据,则进行该case处理语句
default:
//如果上面的操作都没有执行,则执行默认操作
)
可以看得出,select并不像switch,后面并不带判断条件,而是直接去查case语句,每个case语句都必须是一个channel操作,比如上面的例子中,第一个case试图从chan1中读取一个数据并直接忽略读到的数据。而第二个case则写入一个1,如果这两者都没有成功,则执行default。
channel和select结合的简单案例:
ch := make(chan int, 1)
for {
select {
case ch <- 0;
case ch <-1;
}
i := <-ch;
fmt.Println("Value received:", i)
}
完整案例:
package main
import "fmt"func pass(left, right chan int){
left <- 1 + <- right
}
func main(){
const n = 50
leftmost := make(chan int)
right := leftmost
left := leftmost for i := 0; i< n; i++ {
right = make(chan int) // the chain is constructed from the end
go pass(left, right) // the first goroutine holds (leftmost, new chan)
left = right // the second and following goroutines hold (last right chan, new chan) }
go func(c chan int){ c <- 1}(right)
fmt.Println("sum:", <- leftmost)
}
这段代码产生了一个单向的管道环,每个节点对输入的值加了1,然后输出给下一个节点,最后到终点 leftmost。重点我认为有以下几个:
1.循环中的 goroutine ;
2.unbuffered channel 的连接和阻塞;
3goroutine 对 channel 的竞争;
3.channel缓冲机制
创建一个带缓冲的channel的语法:c := make(chan int, 1024)
go语言中,channel有缓冲与无缓冲是有重要区别的,那就是一个是同步的;一个是非同步的,如:
c1:=make(chan int) 无缓冲
c2:=make(chan int,1024) 有缓冲
c1<-1
无缓冲的不仅仅是向c1通道放1而是一直要有别的携程<-c1接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞;而c2<-1 则不会阻塞,因为缓冲大小是1024,只有当放第1024值的时候前面的值还没被人拿走,这时候才会阻塞。
有缓冲:
package main
import "fmt"
var c = make(chan int, 1)
func f() {
c <- 'c'
fmt.Println("在goroutine内")
}
func main() {
go f()
c <- 'c'
<-c
<-c
fmt.Println("外部调用")
}
无缓冲的案例代码:
package main
import (
"fmt"
)
func writeRoutine(test_chan chan int, value int) {
test_chan <- value
}
func readRoutine(test_chan chan int) {
<-test_chan
return
}
func main() {
c := make(chan int)
x := 100
//readRoutine(c)
//go writeRoutine(c, x)
//writeRoutine(c, x)
//go readRoutine(c)
//go readRoutine(c)
//writeRoutine(c, x)
go writeRoutine(c, x)
readRoutine(c)
fmt.Println(x)
}
4.channel的超时机制
在之前对channel的介绍中,完全没有提到错误的情况,而这个问题显然是不能被忽略的。在并发通信的过程中,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者channel试图读取数据时发现channel为空,如果不正确出处理这些情况,很可能会导致整个goroutine被锁死。
使用channel时需要小心,比如对于下面的用法:
i := <- ch
不出问题的话一切正常运行,但是如果出现一个错误的情况,即永远都没有人往ch里写数据,那么上述这个读取动作也将永远无法从ch中读到数据,导致整个goroutine永远阻塞并且没有挽留的机会,如果channel只是被同一个开发者使用,那么出问题的可能性要低一些,但是如果一旦对外公开,就必须考虑到最差的情况并且对程序进行保护。
go语言并没有提供直接的超时处理机制,但是我们可以使用select机制,虽然select机制不是专为超时二设置,其实Unix C里面很多时候也使用Select来做超时处理。select能够很方便地解决超时问题。因为select的特点是只要其中的一个case已经完成,程序就会往下执行,而不会考虑其他的case的情况。
示例代码:
timeout := make(chan bool, 1)
go func() {
time.Sleep(le9)
timeout <- true
}()
select {
case <- ch:
//从ch中读取数据
case <- timeout:
//一直没有从ch中读取到数据,但是从timeout中读到了数据
}
这样使用select机制可以避免永久等待问题,因为程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否处于等待状态,从而达到1秒超时的效果。
超时完整示例代码:
package main
import "time"
func main() {
ch := make(chan bool)
end := make(chan bool)
go func() {
defer func() { end <- true }()
select {
case <-ch:
print("OK\n")
case <-time.After(time.Second * 2):
}
}()
go func() {
time.Sleep(time.Second * 3)
ch <- true
}()
<-end
}
5.channel的传递
在go语言中channel本身也是一个原生类型,与map之类的地位一样,因为channel本身在定义后也可以通过channel来传递。
可以使用这个特性来实现Uinx上非常常见的管道特性,管道也是使用非常广泛的一种设计模式,比如在处理数据块的时候,我们可以采用管道设计,这样可以比较容易以插件的方式增加数据的处理流程。
使用channel来设计管道的示例代码:
首先定义数据结构:
type PipeData struct {
value int
handle func(int) int
next chan int
}
然后我们写一个常规的处理函数,只要定义一系列的PipeData的数据结构并一起传递给这个函数,就可以达到流式数据处理的目的:
func handle (queue chan *PipeData){
for data, _ : = range queue {
data.next <- data.handler(data.value)
}
}
其实这和其他的编程语言很类似
6.单向channel
我们默认创建的是双向通道,单向通道没有意义,但是我们却可以通过强制转换
将双向通道 转换成为单向通道 。
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
channel是一个原生类型,因此不仅 支持被传递,还支持类型转换。只有在介绍了单向channel的概念后,读者才会明白类型转换对于
channel的意义:就是在单向channel和双向channel之间进行转换。
示例如下:
ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
基于ch4,我们通过类型转换初始化了两个单向channel:单向读的ch5和单向写的ch6。
从设计的角度考虑,所有的代码应该都遵循“最小权限原则” ,
从而避免没必要地使用泛滥问题, 进而导致程序失控。 写过C++程序的读者肯定就会联想起const 指针的用法。非const指针具备const指针的所有功能,将一个指针设定为const就是明确告诉
函数实现者不要试图对该指针进行修改。单向channel也是起到这样的一种契约作用。
下面我们来看一下单向channel的用法:
func Parse(ch <-chan int) {
for value := range ch {
fmt.Println("Parsing value", value)
}
}
除非这个函数的实现者无耻地使用了类型转换,否则这个函数就不会因为各种原因而对ch 进行写,避免在ch中出现非期望的数据,从而很好地实践最小权限原则。
只读只写单向channel代码例子,遵循权限最小化的原则
package main
import "fmt"
import "time"
func sCh(ch <-chan int){
for val:= range ch {
fmt.Println(val)
}
}
func main(){
dch:=make(chan int,100)
for i:=0;i<100;i++{
dch<- i
}
go sCh(dch)
time.Sleep(1e9)
}
7.channel的关闭,以及判断channel的关闭
关闭channel非常简单,直接使用Go语言内置的close()函数即可:close(ch)
在介绍了如何关闭channel之后,我们就多了一个问题:如何判断一个channel是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:x, ok := <-ch
这个用法与map中的按键获取value的过程比较类似,只需要看第二个bool返回值即可,如果返回值是false则表示ch已经被关闭。
以上是关于go语言并发编程(channel)的主要内容,如果未能解决你的问题,请参考以下文章