Go的通道-基本用法
创始人
2024-03-25 08:16:27
0

Go的通道-基本用法

文章目录

  • Go的通道-基本用法
    • 一、Go语言独有的并发编程模式和编程哲学
    • 二、通道的基础知识
      • 2.1 通道类型的值本身是并发安全的
      • 2.2 声明并初始化一个通道
      • 2.3 一个通道相当于一个先进先出(FIFO)的队列
    • 三、对于发送和接收操作都有哪些基本的特性
      • 3.1 对于同一个通道,发送操作之间是互斥的,接收操作之间是互斥的
      • 3.2 发送操作和接收操作中对元素值的处理都是不可分割的
      • 3.3 发送操作在完全完成之前会被阻塞。接收操作也是如此
    • 四、发送操作和接收操作在什么时候可能被长时间的阻塞?
      • 4.1 情况一:针对缓冲通道
      • 4.2 情况二:针对非缓冲通道
      • 4.3 情况三:错误的使用通道,导致的阻塞
    • 五、发送操作和接收操作何时会引起恐慌
    • 六、思考
      • 6.1 通道的长度代表着什么?它在什么时候会与通道的容量相同?
      • 6.2 元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?

一、Go语言独有的并发编程模式和编程哲学

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)

二、通道的基础知识

2.1 通道类型的值本身是并发安全的

通道类型的值本身就是并发安全的,这也是Go语言自带的、唯一一个可以满足并发安全性的类型。

2.2 声明并初始化一个通道

像使用make初始化切片一样初始化一个通道。第一个参数代表通道的具体类型的类型字面量。

// 比如
make(chan int) // chan是通道类型的关键字,int说明该通道类型的元素类型

还可以接受第二个int类型的参数,第二个参数是可选的。指通道的容量,表示该通道最多可以缓存多少个元素值。这个值不能小于0。

当容量为0时,这个通道称为非缓冲通道,也就是不带缓冲的通道。

2.3 一个通道相当于一个先进先出(FIFO)的队列

使用接送操作符(<-)发送和接收元素值。

package mainimport ("fmt"
)/*
输出:
1
*/
func main() {ch1 := make(chan int, 3)ch1 <- 1ch1 <- 2ch1 <- 3fmt.Printf("管道ch1 接收的第一个元素:%d\n", <-ch1)
}

三、对于发送和接收操作都有哪些基本的特性

它们的基本特性如下:

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间是互斥的;
  2. 发送操作和接收操作中对元素值的处理都是不可分割的;
  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此;

3.1 对于同一个通道,发送操作之间是互斥的,接收操作之间是互斥的

在同一时刻,只会执行同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。

类似的,在同一时刻,运行时系统只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的,也是如此。

这里要注意一个细节,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。

另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含两步,第一步时生成正在通道中的这个元素值的副本,并准备给接收方,第二步是删除在通道中的这个元素值。

3.2 发送操作和接收操作中对元素值的处理都是不可分割的

这里“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。

3.3 发送操作在完全完成之前会被阻塞。接收操作也是如此

发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。也就是说,在它之后的代码不会有执行的机会,直到这句代码的阻塞解除。

更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。

接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止

四、发送操作和接收操作在什么时候可能被长时间的阻塞?

4.1 情况一:针对缓冲通道

(1)发送操作的阻塞

针对缓冲通道。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。

这时,通道会优先通知最早因此而等待的、那个发送操作所在的goroutine,后者会再次执行发送操作。

由于发送操作在这种情况下被阻塞后,它们所在的goroutine会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。

(2)接收操作的阻塞

相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。

这时,通道会优先通知最早等待的那个接收操作所在的goroutine,并使它再次执行接收操作。

因此而等待的所有接收操作的goroutine,都会按照先后顺序被放入通道内部的接收等待队列。

4.2 情况二:针对非缓冲通道

无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。因此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有后发双发对接上了,数据才会被传递。

并且,数据是直接从发送方复制到接收方的。中间不会有非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据。

在大多数情况下,缓冲通道会作为收发双方的中间件。正如前文所述,元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。

但是,当发送操作在执行的时候,发现空的通道中,正好有等待的接收操作,那么它会直接把元素复制给接收方。

4.3 情况三:错误的使用通道,导致的阻塞

对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永远地处于阻塞状态。它们所属的goroutine中的任何代码,都不会再被执行。

由于通道类型是引用类型,所以它的零值就是nil。换句话说,当我们只声明该类型的变量,但是没有用make函数对它进行初始化时,该变量的值就会时nil。

package mainimport ("fmt"
)func main() {// 示例1ch1 := make(chan int, 1)ch1 <- 1// ch1 <- 2 // 通道已满,这里会造成则色fmt.Println(<-ch1)// 示例2ch2 := make(chan int, 1)// elem, ok := <-ch2 // 通道已空,这里会造成阻塞// fmt.Printf("%d, %t\n", elem, ok)ch2 <- 1// 示例3var ch3 chan int// ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!// fmt.Println(<-ch3) // 通道的值为nil,因此这里会造成永久的阻塞!_ = ch3
}

五、发送操作和接收操作何时会引起恐慌

(1)对于一个已经初始化,但并没有关闭的通道来说,收发操作一定不会引起恐慌。但是通道一旦关闭,还对它进行发送操作,就会引起恐慌。

(2)另外,如果我们试图关闭一个已经关闭了的通道,也会引起恐慌。注意,接收操作是可以感知通道的关闭,并能够安全的退出。

更具体的说,当我们对接收表达式的结果,同时赋给两个变量时,第二个变量就是一个bool类型。它的值如果是false就说明通道已经关闭,并且再没有元素可取。

注意,如果通道关闭时,里面还有元素未取出,那么接收表达式的第一个结果,仍会是通道里的某一个元素值,而第二个结果值一定是True。因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。

由于通道的收发操作的上述特性,所以除非有特殊的保障措施,我们千万不要让接收方关闭通道,而应当让发送方作做件事。

package mainimport ("fmt"
)func main() {ch1 := make(chan int, 1)// 发送方go func() {for i := 0; i < 10; i++ {fmt.Printf("发送方:发送元素:%v\n", i)ch1 <- i}fmt.Println("发送方:关闭通道")close(ch1)}()// 接收方for {elem, ok := <-ch1if !ok {fmt.Println("接收方:元素接收完毕!")break}fmt.Printf("接收方:接收到元素%d\n", elem)}fmt.Println("结束。")
}

六、思考

6.1 通道的长度代表着什么?它在什么时候会与通道的容量相同?

通道的长度代表着整个通道已经存在的元素值len可以查看,类似于切片,而容量则是刚开始设定的值,整个chan可以看成一个已经长度的队列,操作也可以跟队列相类比。

6.2 元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?

go没有深拷贝,只有浅拷贝,通过值拷贝,而没有所谓的引用拷贝,也是因为这样,整个go语言才能使得效率很高,内存占用少。

Go中没有深层复制,都是浅层复制。数组因为是值类型的,所以即使是浅复制也能完全复制过来。

package mainimport ("fmt"
)type Book struct {name  stringprice int
}type Student struct {name stringage  intdesc []stringoneB Book
}func main() {ch01 := make(chan Student, 1)oneB := Book{name: "b01", price: 2}d01 := []string{"a", "b"}s01 := Student{name: "aa", age: 12, oneB: oneB, desc: d01}ch01 <- s01// 修改前的值:{name:aa age:12 desc:[a b] oneB:{name:b01 price:2}}fmt.Printf("修改前的值:%+v\n", s01)s01.name = "bb"s01.oneB.name = "b02"d01[1] = "c"// 修改后的值:{name:bb age:12 desc:[a c] oneB:{name:b02 price:2}}fmt.Printf("修改后的值:%+v\n", s01)// 从通道里出来的值:{name:aa age:12 desc:[a c] oneB:{name:b01 price:2}}fmt.Printf("从通道里出来的值:%+v\n", <-ch01)
}

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...