go语言的并发
并发模式
根据之前的内容,可以写一个简单的生成器,生成channel,不断发送数据。
使用生成器生成多个channel,从channel中接收数据,就是简单的并发
//生成器
func createChan()chan int{
out := make(chan int)
go func(){
for{
time.Sleep(time.Second)
out <- 0
}
}()
return out
}
//main函数中创建两个channel,接收数据
func main(){
c1,c2 := createChan(),createChan()
for {
fmt.Println(<-c1)
fmt.Println(<-c2)
}
}
上述就是一个简单的并发模式,但不是正经的并发。因为每次都要先从c1接收数据,再从c2接收数据,某一个channel没接收到还要等着,才能接收另一个channel
要实现真正的并发,可以将从多个channel中接收的数据,只要接收到就发送给新的一个channel,main函数中只从这一个channel中接收数据。
多个channel的数据发送给一个channel有两种实现方式,一种是为每一个channel创建一个goroutine,这样就不会互相影响了,另一种方式是使用select调度器,哪个channel先有数据,就接收它,只用开一个goroutine。
两种方式各有优势,适用情况不同
方式一
适用情况
当channel个数不确定时使用,参数设为可变参数,然后在函数内遍历
简单形式
func processChan(c1,c2 chan int) chan int{
out := make(chan int)
go func(){
for{
out <- <-c1
}
}()
go func(){
for{
out <- <-c1
}
}()
return out
}
完全体
//这里...代表可变参数,一个或多个chan int
func processChan(ch ...chan int) chan int{
out := make(chan int)
for c := range ch{
//这里传递了参数,而不是直接使用外部的变量
//之前提过开goroutine尽量不要直接使用外部变量
//这里如果直接使用外部的c,结果会只从最后一个channel中接收数据
//因为开了goroutine并不会立即执行,而是当接收到数据后才会向out发送
//而c只有一个,循环一遍开了多个goroutine,这是c就已经是最后一个channel了,所有的goroutine再使用c的话就全是最后一个channel了
//所以c作为参数传递给goroutine,因为是值传递,每个goroutine使用的都是对应的channel了
go func(c chan int){
for{
out <- <-c1
}
}(c)
}
return out
}
方式二
使用情况
当channel的个数确定时,优点在于只需要开一个goroutine
例子
func processChanWithSelect(c1,c2 chan int) chan int{
out := make(chan int)
for {
select{
case n := <- c1:
out <- n
case n := <- c2:
out <- n
}
}
return out
}
并发机制
根据之前的知识,我们可以实现一些并发机制,如:非阻塞机制、超时机制、中断退出、优雅退出
非阻塞机制
传入一个channel,如果接收到数据,就返回接收到的数据以及true,如果没有接收到数据,则返回自定义数据以及false
func noBlock(c chan int) (int,bool){
select{
case n := <- c:
return n,true
default:
return -1,false
}
}
//这样在接收数据时可以使用该函数,然后判断,如果为false则跳过接收数据
超时机制
使用time.Tick来实现,每次等待一段时间,当收到了time.Tick上的channel传来的数据,则返回timeout提示
func isTimeOut(c chan int)(int,bool){
select{
case n := <- c:
return n,true
case <- time.Tick(time.Second):
fmt.Println("time out")
return -1,false
}
}
中断退出
之前创建的生成器,生成的是不断发送数据的channel,在main函数中不断接收数据。这种情况下,没有接收完数据,程序一般不会退出。
但是当我们创建一个生成器,生成不断接收数据的channel,在main函数中不断发送数据,channel接收到数据后输出。这样就会有一个问题,有可能main函数发送完数据,没等到从channel中接收到数据并输出,就退出了程序,这样生成器中正在执行的内容就会突然中断。
为了避免这个问题,可以向生成器中传入一个channel,当main函数中发送完数据后,向这个channel中发送个数据,生成器从这个channel中接收到数据就不再继续之前内容,开始执行退出过程
//使用chan struct{}是因为如果不传数据,不占空间
func createChan(done chan struct{})chan int{
out := make(chan int)
go func(d chan struct{}){
for{
select {
//每隔1秒从out取一次数据,在这段间隔内就会一直判断done是否有数据
case <- time.Tick(time.Second):
fmt.Println(<-out)
case <- done:
fmt.Println("good bye")
return
}
}
}(done)
return out
}
优雅退出
main函数中向生成器生成的channel中发送完数据后,有可能直接退出,也有可能继续执行其他的功能模块,并没有判断生成器中的goroutine是否执行完退出,所以要实现优雅的退出,可以在生成器的goroutine中执行完数据退出的过程中向done再发送个数据,然后在main函数中接收数据。
这样如果goroutine没执行完,没有向done中发送数据,main函数中就会一直等待着,到接收到为止
//使用chan struct{}是因为如果不传数据,不占空间
func createChan(done chan struct{})chan int{
out := make(chan int)
go func(d chan struct{}){
for{
select {
//每隔1秒从out取一次数据,在这段间隔内就会一直判断done是否有数据
case <- time.Tick(time.Second):
fmt.Println(<-out)
case <- done:
fmt.Println("cleaning up")
fmt.Println("cleaned yes")
fmt.Println("good bye")
done <- 1
return
}
}
}(done)
return out
}