Go语言 channel进阶

Posted by kzdgt on Wednesday, March 29, 2023

原文:Go语言 channel进阶 - 掘金 (juejin.cn)

Go channel有什么特点?

channel有2种类型:无缓冲、有缓冲

channel有3种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)

写操作模式 读操作模式 读写操作模式
创建 make(chan<- int) make(<-chan int) make(chan int)

channel有3种状态:未初始化、正常、关闭

未初始化 关闭 正常
关闭 panic panic 正常关闭
发送 永远阻塞导致死锁 panic 阻塞或者成功发送
接收 永远阻塞导致死锁 缓冲区为空则为零值, 否则可以继续读 阻塞或者成功接收

注意点

  1. 一个 channel不能多次关闭,会导致painc
  2. 如果多个 goroutine 都监听同一个 channel,那么 channel 上的数据都可能随机被某一个 goroutine 取走进行消费
  3. 如果多个 goroutine 监听同一个 channel,如果这个 channel 被关闭,则所有 goroutine 都能收到退出信号

Go channel有无缓冲的区别?

无缓冲:一个送信人去你家送信,你不在家他不走,你一定要接下信,他才会走。

有缓冲:一个送信人去你家送信,扔到你家的信箱转身就走,除非你的信箱满了,他必须等信箱有多余空间才会走。

无缓冲 有缓冲
创建方式 make(chan TYPE) make(chan TYPE, SIZE)
发送阻塞 数据接收前发送阻塞 缓冲满时发送阻塞
接收阻塞 数据发送前接收阻塞 缓冲空时接收阻塞

非缓冲 channel

package main

import (
    "fmt"
    "time"
)

func loop(ch chan int) {
    for {
        select {
        case i := <-ch:
            fmt.Println("this  value of unbuffer channel", i)
        }
    }
}

func main() {
    ch := make(chan int)
    ch <- 1
    go loop(ch)
    time.Sleep(1 * time.Millisecond)
}
复制代码

这里会报错 fatal error: all goroutines are asleep - deadlock! 就是因为 ch<-1 发送了,但是同时没有接收者,所以就发生了阻塞

但如果我们把 ch <- 1 放到 go loop(ch) 下面,程序就会正常运行

缓冲 channel

package main

import (
    "fmt"
    "time"
)

func loop(ch chan int) {
    for {
        select {
        case i := <-ch:
            fmt.Println("this  value of unbuffer channel", i)
        }
    }
}

func main() {
    ch := make(chan int,3)
    ch <- 1
    ch <- 2
    ch <- 3
    ch <- 4
    go loop(ch)
    time.Sleep(1 * time.Millisecond)
}
复制代码

这里也会报 fatal error: all goroutines are asleep - deadlock! ,这是因为 channel 的大小为 3 ,而我们要往里面塞 4 个数据,所以就会阻塞住,解决的办法有两个:

  1. 把 channel 长度调大一点
  2. 把 channel 的信息发送者 ch <- 1 这些代码移动到 go loop(ch) 下面 ,让 channel 实时消费就不会导致阻塞了

Go channel为什么是线程安全的?

为什么设计成线程安全?

不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全

如何实现线程安全的?

channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据

Go channel如何控制goroutine并发执行顺序?

多个goroutine并发执行时,每一个goroutine抢到处理器的时间点不一致,gorouine的执行本身不能保证顺序。即代码中先写的gorouine并不能保证先执行

思路:使用channel进行通信通知,用channel去传递信息,从而控制并发执行顺序

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {
    ch1 := make(chan struct{}, 1)
    ch2 := make(chan struct{}, 1)
    ch3 := make(chan struct{}, 1)
    ch1 <- struct{}{}
    wg.Add(3)
    start := time.Now().Unix()
    go print("gorouine1", ch1, ch2)
    go print("gorouine2", ch2, ch3)
    go print("gorouine3", ch3, ch1)
    wg.Wait()
    end := time.Now().Unix()
    fmt.Printf("duration:%d\n", end-start)
}

func print(gorouine string, inputchan chan struct{}, outchan chan struct{}) {
    // 模拟内部操作耗时
    time.Sleep(1 * time.Second)
    select {
    case <-inputchan:
        fmt.Printf("%s\n", gorouine)
        outchan <- struct{}{}
    }
    wg.Done()
}
复制代码
  • 输出:
gorouine1
gorouine2
gorouine3
duration:1
复制代码

Go channel共享内存有什么优劣势?

“不要通过共享内存来通信,我们应该使用通信来共享内存” 这句话想必大家已经非常熟悉了,在官方的博客,初学时的教程,甚至是在 Go 的源码中都能看到

无论是通过共享内存来通信还是通过通信来共享内存,最终我们应用程序都是读取的内存当中的数据,只是前者是直接读取内存的数据,而后者是通过发送消息的方式来进行同步。而通过发送消息来同步的这种方式常见的就是 Go 采用的 CSP(Communication Sequential Process) 模型以及 Erlang 采用的 Actor 模型,这两种方式都是通过通信来共享内存。

image.png 大部分的语言采用的都是第一种方式直接去操作内存,然后通过互斥锁,CAS 等操作来保证并发安全。Go 引入了 Channel 和 Goroutine 实现 CSP 模型将生产者和消费者进行了解耦,Channel 其实和消息队列很相似。而 Actor 模型和 CSP 模型都是通过发送消息来共享内存,但是它们之间最大的区别就是 Actor 模型当中并没有一个独立的 Channel 组件,而是 Actor 与 Actor 之间直接进行消息的发送与接收,每个 Actor 都有一个本地的“信箱”消息都会先发送到这个“信箱当中”。

优点

  • 使用 channel 可以帮助我们解耦生产者和消费者,可以降低并发当中的耦合

缺点

  • 容易出现死锁的情况

Go channel发送和接收什么情况下会死锁?

死锁:

  • 单个协程永久阻塞
  • 两个或两个以上的协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象。

channel死锁场景:

  • 非缓存channel只写不读
  • 非缓存channel读在写后面
  • 缓存channel写入超过缓冲区数量
  • 空读
  • 多个协程互相等待
  1. 非缓存channel只写不读
func deadlock1() {
    ch := make(chan int) 
    ch <- 3 //  这里会发生一直阻塞的情况,执行不到下面一句
}
复制代码
  1. 非缓存channel读在写后面
func deadlock2() {
    ch := make(chan int)
    ch <- 3  //  这里会发生一直阻塞的情况,执行不到下面一句
    num := <-ch
    fmt.Println("num=", num)
}

func deadlock2() {
    ch := make(chan int)
    ch <- 100 //  这里会发生一直阻塞的情况,执行不到下面一句
    go func() {
        num := <-ch
        fmt.Println("num=", num)
    }()
    time.Sleep(time.Second)
}
复制代码
  1. 缓存channel写入超过缓冲区数量
func deadlock3() {
    ch := make(chan int, 3)
    ch <- 3
    ch <- 4
    ch <- 5
    ch <- 6  //  这里会发生一直阻塞的情况
}
复制代码
  1. 空读
func deadlock4() {
    ch := make(chan int)
    // ch := make(chan int, 1)
    fmt.Println(<-ch)  //  这里会发生一直阻塞的情况
}
复制代码
  1. 多个协程互相等待
func deadlock5() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 互相等对方造成死锁
    go func() {
        for {
            select {
            case num := <-ch1:
                fmt.Println("num=", num)
                ch2 <- 100
            }
        }
    }()

    for {
        select {
        case num := <-ch2:
            fmt.Println("num=", num)
            ch1 <- 300
        }
    }
}

「真诚赞赏,手留余香」

kzdgt Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付