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

=====


后厂村村长
7 声望2 粉丝

Hello, Debug World