本篇文章主要介绍Go语言 无缓冲管道和有缓冲管道概念,特点及其使用示例。
目录
无缓冲通道
sync.RWMutex{}
当涉及到多go程时,c语言使用互斥量,上锁来保持资源同步,免资源竞争问题
go语言也支持这种方式,但是go语言更好的解决方案是使用管道、通道
使用管道不需要我们去进行加解锁
A 往管道里面写数据 B从管道里面读数据,go自动帮我们做好了数据同步。
示例如下:
Go
package main
import (
"fmt"
"time"
)
func main() {
// 创建管道:创建一个装数字的管道 ==》 channel
// strChan := make(chan string) // 装字符串的管道
// 装数字的管道,使用管道的时候一定要make,同map一样,否则nil
// 此时是无缓冲的管道
numChan := make(chan int)
// 创建两个go程,父写数据,子读数据
go func() {
for i := 0; i < 50; i++ {
data := <-numChan
fmt.Println("data:", data)
}
}()
for i := 0; i < 50; i++ {
// 向管道中写入数据
numChan <- i
fmt.Println("===> 主go程,写入数据:", i)
}
time.Sleep(5 * time.Second)
}
运行结果:
bash
data: 0
===> 主go程,写入数据: 0
===> 主go程,写入数据: 1
data: 1
data: 2
===> 主go程,写入数据: 2
===> 主go程,写入数据: 3
data: 3
data: 4
===> 主go程,写入数据: 4
===> 主go程,写入数据: 5
data: 5
data: 6
===> 主go程,写入数据: 6
===> 主go程,写入数据: 7
写入和读取无规律,可能读在写的前面打印出来。
有缓冲的 管道
语法
numsChan := make(chan int,10)
特点
1.当缓冲写满的时候,写阻塞,当被读取后,再恢复写入
2.当缓冲区读取完毕,读阻塞
3.如果管道没有使用make分配空间,那么管道默认是ni1的,读取、写入都会阻塞
4.对一个管道,读与写次数必须对等
代码示例
通过创建一个数字管道,主go程和一个子go程写入数据,另一个子go程读取数据。
需要注意写入数据的总条数与读取数据的总条数相等。
示例如下:
Go
package main
import (
"fmt"
"time"
)
func main() {
// 有缓冲的管道
numChan := make(chan int, 10)
go func() {
for i := 0; i < 50; i++ {
// 从管道中读取数据
data := <- numChan
fmt.Println("子go程1 读取数据 ==》 data:", data)
}
}()
go func() {
for i := 0; i < 20; i++ {
// 从管道中读取数据
numChan <- i
fmt.Println("子go程2 写入数据:", i)
}
}()
for i := 20; i < 50; i++ {
// 向管道中写入数据
numChan <- i
fmt.Println("===> 主go程,写入数据:", i)
}
time.Sleep(5 * time.Second)
}
运行结果:
bash
===> 主go程,写入数据: 20
===> 主go程,写入数据: 21
===> 主go程,写入数据: 22
===> 主go程,写入数据: 23
===> 主go程,写入数据: 24
===> 主go程,写入数据: 25
子go程2 写入数据: 0
子go程2 写入数据: 1
子go程2 写入数据: 2
子go程2 写入数据: 3
===> 主go程,写入数据: 26
子go程1 读取数据 ==》 data: 20
子go程1 读取数据 ==》 data: 0
子go程1 读取数据 ==》 data: 21
子go程1 读取数据 ==》 data: 22
子go程1 读取数据 ==》 data: 23
子go程1 读取数据 ==》 data: 24
子go程1 读取数据 ==》 data: 25
子go程1 读取数据 ==》 data: 26
子go程1 读取数据 ==》 data: 1
子go程1 读取数据 ==》 data: 2
未分配空间示例
如果管道不分配空间直接使用,会怎么样呢?
示例如下:
Go
package main
import (
"fmt"
"time"
)
func main() {
var names chan string // 默认是nil的
go func() {
fmt.Println("names:", names)
}()
names <- "hello world"
time.Sleep(1 * time.Second)
}
运行结果:
bash
$ go run 不分配空间.go
names: <nil>
fatal error: all goroutines are asleep - deadlock!
解决方法进行分配空间
示例如下:
Go
package main
import (
"fmt"
"time"
)
func main() {
//var names chan string // 默认是nil的
names := make(chan string, 10)
go func() {
fmt.Println("names:", names)
}()
names <- "hello world" // 由于names是nil的,写操作会阻塞在这里
time.Sleep(1 * time.Second)
}
读取次数不一致示例
读,当主程序被管道阻塞时,那么程序将锁死崩溃
要求我们一定要读写次数保持一致
示例如下:
Go
package main
import (
"fmt"
"time"
)
func main() {
numsChan1 := make(chan int, 20)
//写
go func() {
for i := 0; i < 15; i++ {
// 向管道中写入数据
numsChan1 <- i
fmt.Println("===> 子go程,写入数据:", i)
}
}()
// 读
for i := 0; i < 20; i++ {
// 从管道中读取数据
data := <- numsChan1
fmt.Println("主go程 读取数据 ==》 data:", data)
}
time.Sleep(5 * time.Second)
}
当管道的读写次数不一致的时候
如果阻塞在主go程,那么程序会崩溃
如果阻塞在子go程,那么会出现内存泄漏
For-range遍历
避免出现读写不一致,直接使用for-range写法。
示例如下:
Go
numsChan2 := make(chan int, 20)
//写
go func() {
for i := 0; i < 15; i++ {
// 向管道中写入数据
numsChan2 <- i
fmt.Println("===> 子go程,写入数据:", i)
}
fmt.Println("数据全部写入完毕,准备关闭管道")
close(numsChan2)
}()
// 读
// 遍历管道时,只返回一个值
// for range并不知道管道是否已经写完了,所以会一直在这里等待
// 在写入端,将管道关闭,for range遍历关闭管道时,会退出
for v := range numsChan2{
fmt.Println("读取数据:", v)
}
time.Sleep(5 * time.Second)
总结
1.当管道写满了,写阻塞;
2.当缓冲区读完了,读阻塞;
3.如果管道没有使用make分配空间,管道默认是nil;
4.从nil管道读取数据,写入数据,都会阻塞(注意,不会崩溃);
5.从一个已经colse的管道读取数据时,会返回零值(不会崩溃);
6.像一个已经colse的管道写数据时,会崩溃;
7.关闭一个已经colse的管道,程序会崩溃;
8.关闭管道的动作,一定要在写端执行,不应该放到读端,否则写的继续写会崩溃;
9.读和写的次数一定要对等,否则:
在多个go程中:资源泄露;
在主go程中,程序崩溃(deadlock)。