在 go 中并发只需要用 goruntime 的轻量级线程就行,但是 go 首先是不喜欢直接操作共享内存,而是推荐你通过信道的方式对不同进程进行交互。
同时主进程的结束会导致子进程的异常,比如信道接受方消失,或者输出未被执行
信道
信道只能接受声明时声明的数据类型
信道分为带缓冲和不带缓冲两种,两种方式只在声明时有区别,例如
ch1 := make(chan string) //无缓冲信道
ch2 := make(chan string, 1) //有缓冲信道
读取信道时可以通过第二个变量来判断是否信道被关闭
val, ok := <- ch1
同时信道可以是只读、只写的
//定义只读的channel
read_only := make (<-chan int)
//定义只写的channel
write_only := make (chan<- int)
不带缓冲的信道
不带缓冲的信道只有输出和输入同时准备好才能进行使用,也就是不能在一个程序中同时输出和输入
ch1 := make(chan string)
ch1 <- "1"
fmt.Println(<-ch1)
以上这段代码会导致死锁。
只有当我们开启一个新进程时,在两端同时准备好才能进行使用
ch1 := make(chan string)
go func () {
ch1 <- "1"
}()
fmt.Println(<-ch1)
可以同时可以由多个进程进行存入,由多个进行取出
ch1 := make(chan string)
for i := 0; i < 10; i++ {
go func() {
ch1 <- "1"
}()
go func() {
ch1 <- "2"
}()
}
for i := 0; i < 10; i++ {
fmt.Println(<-ch1)
fmt.Println(<-ch1)
}
带缓冲的信道
带缓冲的信道的缓冲就想PV原语中mutex,通过信道操作符进行类似PV的操作
这是带缓冲的信道
func main() {
ch1 := make(chan string, 2)
go func() {
ch1 <- "1"
}()
go func() {
ch1 <- "2"
}()
fmt.Println(<-ch1)
fmt.Println(<-ch1)
}
缓冲区空时接受方阻塞,缓冲区满时输出方阻塞
举个例子,有两台打印机,打印机可以装纸也可以打印,打印机能容纳2张纸,打印消耗打印机里的纸,装纸就增加打印机里的纸,没纸打印不出来会导致阻塞,没地方放纸会导致阻塞。
这个情景下,打印机是信道,容量是缓存大小,打印是输出,装纸是输入,导致阻塞会错误
range 和 close
go 可以通过 range 不断取出信道的中的内容,直到信道被关闭
for i := range ch1 {
fmt.Println("i:", i)
}
虽然直接使用 range 取出十分方便,但是由于不关闭信道会导致 range 出现错误,并且由于有些情况下比较难以进行关闭,比如递归,所以使用 range 直接读取可能会比较麻烦
close(ch1)
通过这样关闭信道,主要是告诉接收者没有需要发送的值,例如终止一个 range 循环。但是 close 以后不能再写入,重复 close 会出现 panic ,只读的信道不能 close,以及 close 以后还可以读取数据
selcet 和默认选择
select 使一个进程可以等待多个通信操作。
而当 select 中的其它分支都没有准备好时,default 分支就会执行。
select {
case i := <-ch1:
//对 i 进行操作
case j := <-ch2:
//对 j 进行操作
default:
// 从 ch1, ch2 中接收都阻塞时执行
}
总结
本文基于 gotour 的并发一部分并按照我自己的实际感觉进行修改和调整,个人感觉 go 的并发使用简单,但是由于有些原生操作不支持并发,导致实际使用时需要借用一些锁