侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计收到 0 条评论

深入理解Golang中的chan特性之上篇

2023-1-20 / 0 评论 / 192 阅读
温馨提示:
本文最后更新于 2023-1-20,已超过半年没有更新,若内容或图片失效,请留言反馈。

Golang 的一大特色就是其简单高效的 天然并发机制

channelGolang 语言中的一个 核心数据类型, 可以把他看成通道(管道), 所以他非常重要。主要用来解决go程的同步问题以及协程之间数据共享(数据传递)的问题并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。

Golang 中使用 goroutinechannel 实现了 CSP (Communicating Sequential Processes) 模型, channelgoroutine 的通信和同步中承担着重要的角色。

goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

引用类型 channel 可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

Channel 的定义与使用

map 类似,channel 也是一个对应 make 创建的底层数据结构的引用。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel 引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel零值也是nil
定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建。

我们看一段代码:

package main

import (
    "fmt"
)

func main()  {
    ch := make(chan int)    //  这里就是创建一个无缓冲的 channel

    go func() {
        for i := 0; i <= 6; i++ {
            ch <- i // 循环写入管道数据
            fmt.Println("写入管道数据", i)
        }
    }()

    for j := 0; j <= 6; j++ {
        value := <- ch  //  循环读出管道数据
        fmt.Println("读出管道数据", value)
    }
}

我们在代码中 先创建了一个匿名函数的子go程,和main的主go程一起争夺 CPU,但是我们在里面创建了一个管道,无缓冲管道有一个规则那就是必须读写同时操作才会有效果,如果只进行读或者只进行写那么会被阻塞,被暂时停顿等待另外一方的操作,在这里我们定义了一个容量为0的通道,这就是无缓冲通道

我们再看下面的代码加深 无缓冲通道 的印象

var c = make(chan int)
go func() {
    // 等待 3秒后循环写入值到通道
    time.Sleep(3 * time.Second)
    for i := 0; i < 5; i++ {
        c <- i
    }
}()

select {
case value := <-c:
    fmt.Println(value)
}

fmt.Println("到此为止")

// 输出结果: select 阻塞了 channel 通道, 将会先等待 3秒后打印 0, 然后打印 '到此为止'
var c = make(chan int)
go func() {
    // 等待 3秒后循环写入值到通道
    time.Sleep(3 * time.Second)
    for i := 0; i < 5; i++ {
        c <- i
    }
}()

select {
case value := <-c:
    fmt.Println(value)
default:
    fmt.Println("我不等了")
}

fmt.Println("到此为止")

// 输出结果: select 阻塞了 channel 通道, 但是有带 default, 将会直接打印 '我不等了', 然后打印 '到此为止'
var c = make(chan int)
go func() {
    // 等待 3秒后循环写入值到通道
    time.Sleep(3 * time.Second)
    for i := 0; i < 5; i++ {
        c <- i
    }
}()

fmt.Println("等待中")

<-c

fmt.Println("到此为止")

// 输出结果: 先打印 '等待中', 等待 3秒后打印 '到此为止'
var c = make(chan int)
go func() {
    // 等待 3秒后循环写入值到通道
    time.Sleep(3 * time.Second)
    for i := 0; i < 5; i++ {
        c <- i
    }
}()

fmt.Println("等待中")

value := <-c
fmt.Println(value)

fmt.Println("到此为止")

// 输出结果: 先打印 '等待中', 等待 3秒后打印 0, 然后打印 '到此为止'
使用无缓冲的通道在 Goroutine 之间同步数据 (一次只能传输一个数据)

总结一下就是无缓冲特性:

  • 同一时刻,同时有 读、写两端把持 channel
  • 如果只有读端,没有写端,那么 “读端”阻塞
  • 如果只有写端,没有读端,那么 “写端”阻塞
  • 读channel: <- channel
  • 写channel: channel <- 数据
使用有缓冲的通道在 Goroutine 之间同步数据 (读写数据可以同时进行)

我来描述下上面的步骤:

第一步 | 右侧的 goroutine 正在从通道接收一个值。
第二步 | 右侧的这个 goroutine 独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。
第三步 | 左侧的 goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
第四步 | 所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。

有缓冲通道就是图中所示,一方可以写入很多数据,不用等对方的操作,而另外一方也可以直接拿出数据,不需要等对方写,但是注意一点(如果写入的一方把channel写满了,那么如果要继续写就要等对方取数据后才能继续写入,这也是一种阻塞,读出数据也是一样,如果里面没有数据则不能取,就要等对方写入)

有缓冲channel的定义很简单:

ch := make(chan int, 10) // chan int 只能写入读入int类型的数据, 10代表容量。

总结一下就是有缓冲特性:

  • 写数据时, 直到缓冲区被填满后,“写端”才会阻塞
  • 读数据时, 缓冲区被读空,“读端”才会阻塞
  • len: 代表缓冲区中,剩余元素个数
  • cap: 代表缓冲区的容量

最后, 举个简单的例子再次帮助理解 channel:

同步通信 (无缓冲channel): 数据发送端,和数据接收端,必须同时在线。
比如打电话, 只有等对方接收才会通,要不然只能阻塞。

异步通信 (有缓冲channel): 数据发送端 发送完数据后立即返回。数据接收端 有可能立即读取,也可能延迟处理。
不用等对方接受, 只需发送过去就行, 比如发短信, 发邮件...

评论一下?

OωO
取消