go 通道-channel、协程-routine、sync
golang 里不需要学习如何创建维护进程池/线程池,也不需要分析什么情况使用多线程,什么情况使用多进程,因为你没得选。
当然,也不需要选。
go原生的 goroutine(协程)已足够优秀,能自动帮你处理好所有事情,而你要做的只是执行它,so easy...
goroutine 也是go天生支持高并发的底气。
goroutine 奉行通过通信来共享内存,而不是共享内存来通信
。
参考:https://www.topgoer.com/并发编程/并发介绍.html
goroutine 可以简单类比为一个函数:
直接调用时,它就是一个普通函数;
如果在调用前加一个关键字go
,那你就开启了一个 goroutine,开启了一扇新世界的大门。
下面看一个示例:
routineTest := func(rName string) {
for i := 0; i < 5; i++ {
fmt.Println("Goroutine: ", rName, "; idx: ", i)
time.Sleep(20 * time.Millisecond)
}
}
go routineTest("协程1号")
go routineTest("协程2号")
fmt.Println("hello,world... 编写顺序在 Go-Routine 之后")
如果直接执行你会发现,只输出了 hello,world 这一行,并没有按预期的输出 协程1号和2号;
这是因为协程的创建需要时间
,hello,world 打印后,协程还没来得及执行,就结束了。
为了便于观测,我们追加一行 sleep,以阻塞 main 的执行,保证协程的输出;
go routineTest("协程1号")
go routineTest("协程2号")
fmt.Println("hello,world... 编写顺序在 Go-Routine 之后")
time.Sleep(time.Second) // 追加一行 sleep 以阻塞 main 的执行
这时成功输出了 协程1号和2号 的信息,但你会发现它的输出是无序的(无序才是符合预期的,如同两个线程一样,并发执行);
当然了,真正的并发程序不能依赖sleep,需要结合 channel(信道)
来实现。
通道-channel
channel是一种类型,一种引用类型。声明通道类型的格式:var 变量 chan 元素类型
;
通道有 发送(send)、接收(receive)和关闭(close)
三种操作;
发送和接收都使用 <-
符号,箭头指向谁,就代表由谁来接收。
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 6; i++ {
// 向channel发送消息
ch1 <- i
}
// 如果你的管道不往里存值或者取值的时候一定记得关闭管道
close(ch1)
}()
go func() {
for {
d, ok := <-ch1 // 判断通道是否被关闭:通道关闭后再取值ok=false
if !ok {
break
}
fmt.Println("从ch1接收[", d, "]赋值到ch2")
ch2 <- d
}
close(ch2)
}()
for v := range ch2 {
// 判断通道是否被关闭:通道关闭后会退出 for range 循环
fmt.Println("ch2接收到:", v)
}
fmt.Println("main-完成")
Tips:
通道关闭需要注意的是:只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
`通道是可以被垃圾回收机制回收的`,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
另外需要注意的是,上面的通道创建方式,make(chan int)
创建的是 无缓冲的通道
,必须显式定义接收方goroutine之后才能发送值;
就好比你的小区没有快递柜,那么快递员只能和你电话确认有人在家接收的时候,才能给你派送。所以如下的方式执行会报deadlock错误:
ch := make(chan int)
fmt.Println("无缓冲channel-死锁测试")
ch <- 1
<-ch
报错内容:fatal error: all goroutines are asleep - deadlock!
这里发送和接收,无论谁先执行,都会阻塞以等待另一个 goroutine 来执行;
无缓冲 channel,发送者会阻塞,直到接收者接收了发送的值; 所以这里串行执行,会导致死锁;
因为,无缓冲 channel,要求在消息发送时需要接收者已就绪,很显然单协程无法满足,因为会导致发送和接收串行。
如果想让如上实例运行成功,需要显示指定通道的size-容量: make(chan int, 3)
,这里的3就是指容量3;
只要给定的通道容量大于0,就代表定义的是 有缓冲的通道
,这时上面的实例就可以成功执行了。
为啥可以呢?
这就好比,快递公司在你的小区建立了一个快递柜,柜子的容量是3,只要有空余的柜子,快递员就可以把快递扔到柜子里,他就可以收工了。
但是,有缓冲的chan也不是万能的,如果chan的缓冲区满了,这时再写入依然会阻塞
,比如下面的情况,也会dead-lock:
ch := make(chan int, 1) // 设置缓冲区大小为 1
fmt.Println("有缓冲channel-死锁测试")
ch <- 1
// 第二次写入时缓冲区已满,阻塞,然后死锁;
ch <- 1
这就相当于小区的快递柜只能容纳一件快递,如果快递员带着两件过来,就只能死等了。
引申:单向通道
说到快递柜,这里就有一个问题,快递柜只允许快递员投放快递,而不允许用户自己投;
也就是说,它的使用场景是单向的,即 单向通道:
单向发送:`chan<- int` 代表一个只能向chan发送消息的通道,不能接收来自chan的消息;
单向接收:`<-chan int` 代表一个只能接收来自chan的消息的通道,但不能向chan发消息。
其实很好理解:
箭头指向 chan,代表仅能将消息发给 chan,相当于 chan 只写;
箭头从chan向外指,代表仅能单向接收来自 chan 的消息,相当于 chan 只读;
参考示例:
// 单向发送的channel:仅能向chan发送消息,相当于 chan 只写
sendOnlyChan := func(in chan<- int) {
for i := 1; i < 5; i++ {
in <- i
}
close(in)
}
// 单向接收的channel:仅能接收来自chan的消息,相当于 chan 只读
recvOnlyChan := func(out <-chan int) {
for v := range out {
fmt.Println("当前接收到来自chan的消息:", v)
}
}
ch := make(chan int)
go sendOnlyChan(ch)
recvOnlyChan(ch)
参考文档:https://www.topgoer.com/并发编程/channel.html
=====
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。