湖南盈达电力建设有限公网站,长垣住房和城乡建设局 网站,推广项目,网站设计连接数据库怎么做这一章是接上一章内容继续#xff0c;上一章说到协程也就是goroutine#xff0c;如何使用它#xff0c;这一张是讲一种数据结构。当然这个章节的数据结构非常重要。可以说这个数据结构就是为了方便协程#xff0c;才制作出来的。
单纯地将函数并发执行是没有意义的。函数与…这一章是接上一章内容继续上一章说到协程也就是goroutine如何使用它这一张是讲一种数据结构。当然这个章节的数据结构非常重要。可以说这个数据结构就是为了方便协程才制作出来的。
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换但是共享内存在不同的 goroutine 中容易发生竞态问题。
为了保证数据交换的正确性很多并发模型中必须使用互斥量对内存进行加锁这种做法势必造成性能问题。 文章目录 channel序章channel类型channel的初始化操作channel无缓冲的通道有缓冲的通道多返回值模式for range接收值单向通道 channel序章
Go语言采用的并发模型是CSPCommunicating Sequential Processes提倡 通过通信共享内存而不是通过共享内存而实现通信
如果说 goroutine 是Go程序并发的执行体channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。
Go 语言中的通道channel是一种特殊的类型。通道像一个传送带或者队列总是遵循先入先出First In First Out的规则保证收发数据的顺序。每一个通道都是一个具体类型的导管也就是声明channel的时候需要为其指定元素类型。
channel类型
channel是 Go 语言中一种特有的类型。
var 变量名称 chan 元素类型chan是关键字元素类型是指通道中传递元素的类型
比如
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道channel零值 未初始化的通道类型变量其默认零值是nil
var ch chan int
fmt.Println(ch) // nilchannel的初始化
数据结构的初始化其实都差不多都用make 声明的通道类型变量需要使用内置的make函数初始化之后才能使用。
make(chan 元素类型, [缓冲大小])channel的缓冲大小是可选的
ch4 : make(chan int)
ch5 : make(chan bool, 1) // 声明一个缓冲区大小为1的通道操作channel
通道共有发送send、接收(receive和关闭close三种操作。而发送和接收操作都使用-符号。
定义通道
ch : make(chan int)发送 将一个值发送到通道中。
ch - 10 // 把10发送到ch中接收 从一个通道中接收值。
x : - ch // 从ch中接收值并赋值给变量x
-ch // 从ch中接收值忽略结果关闭 我们通过调用内置的close函数来关闭通道。
close(ch)注意 一个通道值是可以被垃圾回收掉的。
通道通常由发送方执行关闭操作并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样通常在结束操作之后关闭文件是必须要做的但关闭通道不是必须的。
关闭后的通道有以下特点
对一个关闭的通道再发送值就会导致 panic。对一个关闭的通道进行接收会一直获取值直到通道为空。对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。关闭一个已经关闭的通道会导致 panic。
无缓冲的通道
无缓冲的通道又称为阻塞的通道
func main() {ch : make(chan int)ch - 10fmt.Println(发送成功)
}G 大写的报错。。。。 deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了。
因为我们使用ch : make(chan int)创建的是无缓冲的通道无缓冲的通道只有在有接收方能够接收值的时候才能发送成功否则会一直处于等待发送的阶段。
同理如果对一个无缓冲通道执行接收操作时没有任何向通道中发送值的操作那么也会导致接收操作阻塞
简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功
如何解决上述问题 方案1创建一个 goroutine 去接收值
var wait sync.WaitGroupfunc recv(c chan int) {ret : -cfmt.Println(接收成功, ret)}func main() {ch : make(chan int)go recv(ch) // 创建一个 goroutine 从通道接收值ch - 10fmt.Println(发送成功)
}首先无缓冲通道ch上的发送操作会阻塞直到另一个 goroutine 在该通道上执行接收操作这时数字10才能发送成功两个 goroutine 将继续执行。
相反如果接收操作先执行接收方所在的goroutine将阻塞直到 main goroutine 中向该通道发送数字10。
使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此无缓冲通道也被称为同步通道。
有缓冲的通道
另外一种解决上面死锁问题的方法那就是使用有缓冲区的通道。
我们可以在使用 make 函数初始化通道时可以为其指定通道的容量
func main() {ch : make(chan int, 1) // 创建一个容量为1的有缓冲区通道ch - 10fmt.Println(发送成功)
}只要通道的容量大于零那么该通道就属于有缓冲的通道通道的容量表示通道中最大能存放的元素数量。
当通道内已有元素数达到最大容量后再向通道执行发送操作就会阻塞除非有从通道执行接收操作。
我们可以使用内置的len函数获取通道内元素的数量使用cap函数获取通道的容量虽然我们很少会这么做。
多返回值模式
当向通道中发送完数据时我们可以通过close函数来关闭通道。
当一个通道被关闭后再往该通道发送值会引发panic从该通道取值的操作会先取完通道中的值。
通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢
value, ok : - chvalue从通道中取出的值如果通道被关闭则返回对应类型的零值。ok通道ch关闭时返回 false否则返回 true。
下面代码片段中的f2函数会循环从通道ch中接收所有值直到通道被关闭后退出
func f2(ch chan int) {for {v, ok : -chif !ok {fmt.Println(通道已关闭)break}fmt.Printf(v:%#v ok:%#v\n, v, ok)}
}func main() {ch : make(chan int, 2)ch - 1ch - 2close(ch)f2(ch)
}for range接收值
通常我们会选择使用for range循环从通道中接收值当通道被关闭后会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用for range改写后会很简洁。
func f3(ch chan int) {for v : range ch {fmt.Println(v)}
}注意 目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。
单向通道
在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递通常我们会选择在不同的任务函数中对通道的使用进行限制比如限制通道在某个函数中只能执行发送或只能执行接收操作。
想象一下我们现在有Producer和Consumer两个函数其中Producer函数会返回一个通道并且会持续将符合条件的数据发送至该通道并在发送完成后将该通道关闭。
而Consumer函数的任务是从通道中接收值进行计算这两个函数之间通过Processer函数返回的通道进行通信。
package mainimport (fmt
)// Producer 返回一个通道
// 并持续将符合条件的数据发送至返回的通道中
// 数据发送完成后会将返回的通道关闭
func Producer() chan int {ch : make(chan int, 2)// 创建一个新的goroutine执行发送数据的任务go func() {for i : 0; i 10; i {if i%2 1 {ch - i}}close(ch) // 任务完成后关闭通道}()return ch
}// Consumer 从通道中接收数据进行计算
func Consumer(ch chan int) int {sum : 0for v : range ch {sum v}return sum
}func main() {ch : Producer()res : Consumer(ch)fmt.Println(res) // 25}大家可以调试一下可有意思了比线程有意思多了。
可以看出正常情况下Consumer函数中只会对通道进行接收操作但是这不代表不可以在Consumer函数中对通道进行发送操作。
作为Producer函数的提供者我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。
然而这种方式我们没有办法阻止在Consumer函数中对通道进行发送操作
Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况
- chan int // 只接收通道只能接收不能发送
chan - int // 只发送通道只能发送不能接收箭头-和关键字chan的相对位置表明了当前通道允许的操作这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的因为默认通道的关闭操作应该由发送方来完成。
修改上述代码
// Producer2 返回一个接收通道
func Producer2() -chan int {ch : make(chan int, 2)// 创建一个新的goroutine执行发送数据的任务go func() {for i : 0; i 10; i {if i%2 1 {ch - i}}close(ch) // 任务完成后关闭通道}()return ch
}// Consumer2 参数为接收通道
func Consumer2(ch -chan int) int {sum : 0for v : range ch {sum v}return sum
}func main() {ch2 : Producer2()res2 : Consumer2(ch2)fmt.Println(res2) // 25
}Producer函数返回的是一个只接收通道这就从代码层面限制了该函数返回的通道只能进行接收操作保证了数据安全。
看到这个示例可能会觉着这样的限制是多余的但是试想一下如果Producer函数可以在其他地方被其他人调用你该如何限制他人不对该通道执行发送操作呢?
在函数传参及任何赋值操作中全向通道正常通道可以转换为单向通道但是无法反向转换。
var ch3 make(chan int, 1)
ch3 - 10
close(ch3)
Consumer2(ch3) // 函数传参时将ch3转为单向通道var ch4 make(chan int, 1)
ch4 - 10
var ch5 -chan int // 声明一个只接收通道ch5
ch5 ch4 // 变量赋值时将ch4转为单向通道
-ch5对已经关闭的通道再执行 close 也会引发 panic