多个goroutine间通信的通道channel
(⼀)、通道的概述
- 使⽤通道的意义
- 单纯地将函数并发执⾏没有意义。函数与函数间需要交换数据才能体现出并发执⾏的意义。
- 虽然可以使⽤共享内存进⾏数据交换,但是共享内存在不同goroutine中容易发⽣竞态问题,必须使⽤互斥对内存进⾏加锁,所以造成性能问题。
- Go语⾔中提倡使⽤通道channel的⽅式代替共享内存。也就是说,Go语⾔中主张,应该通过数据传递来实现共享内存,⽽不是通过共享内存来实现消息传递。
- 排队的⽬的是避免拥堵、插队造成的资源使⽤和交换过程低效问题。多个goroutine为了争抢数据,势必造成低效,使⽤队列的⽅式是最⾼效的,channel就是⼀种队列结构。
- 什么是通道?
- Go语⾔中的通道channel是⼀种特殊的类型。通道像⼀个传送带或者队列,总是遵循先⼊先出first in first out的规则,保证收发数据的顺序。
- 通道可以被认为是Goroutine通信的管道。类似于管道中的⽔从⼀端到另⼀端的流动,数据可以从⼀端发送到另⼀端,通过通道接收。
- 每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何⽤处,因此通道必须使⽤类似于map和slice的⽅法来定义。)
(⼆)、声明通道类型
- 语法
var 通道变量 chan 通道类型 - chan类型的空值是nil,声明后需要配合make才能使⽤。
(三)、创建通道
- 通道是引⽤类型,需要使⽤make进⾏创建
语法:通道示例 := make(chan 数据类型) - 例如:
ch1 := make(chan int) //创建⼀个整数类型通道
ch2 := make(chan interface{}) //创建⼀个空接⼝类型的通道,可以存放任意数据
type Equip struct {}
ch3 := make(chan *Equip) //创建⼀个Equip指针类型的通道,可以存放Equip指针
(四)、通道发送数据
- 使⽤通道发送数据的格式
通道发送使⽤特殊的操作符"<-",将数据通过通道发送的语法为:
通道变量 <- 值
- 通道发送的值可以是变量、常量、表达式或函数返回值等。值的类型必须与ch通道的元素类型⼀致。
- 把数据往通道中发送,如果接收⽅⼀直没有接收,那么发送操作将持续阻塞。此时所有的goroutine,包括main的goroutine都处于等待状态。
- 运⾏会提示报错:fatal error: all goroutines are asleep - deadlock!
- 死锁deadlock
- 使⽤通道时要考虑的⼀个重要因素是死锁。
- 如果Goroutine在⼀个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发⽣,那么程序将在运⾏时出现死锁。
- 类似地,如果Goroutine正在等待从通道接收数据,那么另⼀些Goroutine将会在该通道上写⼊数据,否则程序将会死锁。
- 示例代码:
func main() {
//创建⼀个空指针型通道
ch := make(chan interface{})
//将0通过通道发送
ch <- 0
//发送字符串
ch <- "StevenWang"
}
(五)、阻塞
- ⼀个通道发送和接收数据,默认是阻塞的。
- 当⼀个数据被发送到通道时,在发送语句中被阻塞,直到另⼀个Goroutine从该通道读取数据。类似地,当从通道读取数据时,读取被阻塞,直到⼀个Goroutine将数据写⼊该通道。
- 这些通道的特性是帮助Goroutines有效地进⾏通信,⽽⽆需像使⽤其他编程语⾔中⾮常常⻅的显式锁或条件变量。
- 示例代码:
func main() {
var ch1 chan int
fmt.Println(ch1) //,
fmt.Printf("%Tn", ch1) //chan int
ch1 = make(chan int)
fmt.Println(ch1) //0xc4200200c0
ch2 := make(chan bool)
go func() {
fmt.Println("⼦goroutine。。。")
data, ok := <-ch1 //阻塞式,从通道中读取数据
time.Sleep(1 * time.Second)
fmt.Println("⼦goroutine从通道中读取到main传来的数据是:", ok, data)
ch2 <- true //向通道中写⼊数据,表示结束
}()
ch1 <- 100 //阻塞式,main goroutine向通道中写⼊数据
<-ch2 //⽬的是防⽌main goroutine先执⾏完毕后退出。因为如果main的goroutine终⽌
了,程序将被终⽌,⽽其他Goroutine将不再运⾏。
fmt.Println("main..over") }
运⾏结果:
chan int
0xc420076060
⼦goroutine。。。
⼦goroutine从通道中读取到main传来的数据是: true 100
main..over
(六)、通道接收数据
- 使⽤通道接收数据的格式
通道接收同样使⽤特殊的操作符"<-"。
- 通道变量 <- 值
- 通道收发操作在不同的两个goroutine间进⾏。
- 接收操作将持续阻塞,直到发送⽅发送数据。
- 每次接收⼀个元素。
(七)、通道接收数据的四种写法
- 阻塞接收数据
- data := <-ch
- 执⾏该语句时将会阻塞,直到接收到数据并赋值给data变量。
- 阻塞接收数据的完整写法
- data , ok := <-ch
- data :表示接收到的数据。未接收到数据时,data为通道类型的零值。
- ok:表示是否接收到数据。
- 通过ok值可以判断当前通道是否被关闭。
- 接收任意数据,忽略接收的数据
- <-ch
- 执⾏该语句时将会阻塞。
- 其⽬的不在于接收通道中数据,⽽是为了阻塞goroutine。
- 循环接收数据
1)、循环接收数据
- 循环接收数据,需要配合使⽤关闭通道
- 借助普通for循环和for … range语句循环接收多个元素
- 遍历通道,遍历的结果就是接收到的数据,数据类型就是通道的数据类型。
- 普通for循环接收通道数据,需要有break循环的条件;for range会⾃动判断出通道已关闭,⽽⽆需通过判断来终⽌循环。
2)、循环接收数据的三种⽅式
func main() {
ch1 := make(chan string)
go sendData(ch1)
//1、循环接收数据⽅式1
for {
data := <-ch1
//如果通道关闭,通道中传输的数据则为各数据类型的默认值。chan int 默认值为0,
chan string默认值为"" 等。
if data == "" {
break
}
fmt.Println("从通道中读取数据⽅式1:", data)
}
//2、循环接收数据⽅式2
for {
data, ok := <-ch1
//通过多个返回值的形式来判断通道是否关闭,如果通道关闭,则ok值为false。
if !ok {
break
}
fmt.Println("从通道中读取数据⽅式2:", data)
}
//3、循环接收数据⽅式3
//for range循环会⾃动判断通道是否关闭,⾃动break循环。
for value := range ch1 {
fmt.Println("从通道中读取数据⽅式3:", value)
}
}
func sendData(ch1 chan string) {
for i := 1; i <= 10; i++ {
ch1 <- fmt.Sprintf("发送数据%dn", i)
}
fmt.Println("发送数据完毕。。")
//显式调⽤close()实现关闭通道
close(ch1) }
(⼋)、关闭通道
- 发送⽅如果数据写⼊完毕,需要关闭通道,⽤于通知接受⽅数据传递完毕。⼀般都是发送⽅关闭通道。
- 如何判断⼀个channel是否已经关闭?可以在读取的时候使⽤多重返回值的⽅式。如果返回值是false,则表示通道已经被关闭。
- 如果往关闭的通道中写⼊数据,会报错:panic: send on closed channel。但是可以从关闭后的通道中取数据,不过返回数据默认值和false。
- 示例代码
func main() {
//通道关闭后是否可以写⼊和读取呢?
ch1 := make(chan int)
go func() {
ch1 <- 100
ch1 <- 200
close(ch1)
//ch1 <- 10 //关闭的通道,⽆法写⼊数据
}()
data, ok := <-ch1
fmt.Println("main读取数据:", data, ok)
data, ok = <-ch1
fmt.Println("main读取数据:", data, ok)
data, ok = <-ch1
fmt.Println("main读取数据:", data, ok)
data, ok = <-ch1
fmt.Println("main读取数据:", data, ok)
data, ok = <-ch1
fmt.Println("main读取数据:", data, ok) }
返回结果:
main读取数据: 100 true
panic: send on closed channel
main读取数据: 200 true
main读取数据: 0 false
main读取数据: 0 false
main读取数据: 0 false
goroutine 5 [running]:
main.main.func1(0xc42006e060)
/Users/steven/Documents/go_project/src/ch10_2/demo01_test1.go:12
+0x79
created by main.main
/Users/steven/Documents/go_project/src/ch10_2/demo01_test1.go:8
+0x70
缓冲通道和定向通道
(⼀)、缓冲通道
- ⾮缓冲通道:默认创建的通道都是⾮缓冲通道,读写都是即时阻塞;
- 缓冲通道:⾃带⼀块缓冲区,可以暂时存储数据,如果缓冲区满了, 那么才
会阻塞; - 示例代码
func main() {
//1.⾮缓冲通道
ch1 := make(chan int)
fmt.Println("⾮缓冲通道:", len(ch1), cap(ch1)) //0, 0
go func() {
data := <-ch1 //阻塞
fmt.Println("获取数据:", data)
}()
ch1 <- 100 //阻塞
//time.Sleep(1)
fmt.Println("写⼊数据okn-----------------------")
//2.缓冲通道,缓冲区满了才会阻塞
//ch2 := make(chan int, 5)
//fmt.Println("缓冲通道:", len(ch2), cap(ch2)) //0,5
//go func() {
// for data := range ch2 {
// //time.Sleep(1)
// fmt.Println("获取数据:", data)
// }
//}()
//
//ch2 <- 1
//fmt.Println(len(ch2), cap(ch2))
//ch2 <- 2
//fmt.Println(len(ch2), cap(ch2))
//ch2 <- 3
//fmt.Println(len(ch2), cap(ch2)) //3,5
//ch2 <- 4
//fmt.Println(len(ch2), cap(ch2))
//ch2 <- 5
//fmt.Println(len(ch2), cap(ch2)) //5, 5
//ch2 <- 6 //阻塞
//fmt.Println(len(ch2), cap(ch2))
//close(ch2)
//fmt.Println("main。。。over。。")
//3.缓冲通道
ch3 := make(chan string, 5)
fmt.Printf("%Tn", ch3)
go sendData(ch3)
for data := range ch3 {
//time.Sleep(1 * time.Second)
fmt.Println("t读取数据:", data)
}
fmt.Println("读取完毕。。") }
func sendData(ch3 chan string) {
for i := 1; i <= 10; i++ {
ch3 <- fmt.Sprintf("data%d", i) //1,2,3,4,5,6,7
fmt.Println("写⼊数据:", i) //1,2,3,4,5
fmt.Println(len(ch3), cap(ch3))
}
close(ch3) }
- 缓冲通道模拟⽣产者和消费者
func main() {
ch1 := make(chan int , 5)
ch2 := make(chan bool)//判断结束
rand.Seed(time.Now().UnixNano())
//写⼊数据:⽣产者
go func() {
for i := 1; i <= 20; i++ {
ch1 <- i
fmt.Println("写⼊数据:", i)
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
}
close(ch1)
}()
//读取数据:消费者
go func() {
for data := range ch1 {
fmt.Println("t1号消费者:", data)
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
}
ch2 <- true
}()
go func() {
for data := range ch1 {//1
fmt.Println("t2号消费者:", data)
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
}
ch2 <- true
}()
<- ch2
fmt.Println("main...over...") }
(⼆)、定向通道
- 通道默认都是双向通道。即可写⼊数据,⼜可读取数据。
- 定向通道:也叫单向通道,只读,或只写。
- 只读:make(<- chan Type),只能读取数据,不能写⼊数据
<- chan - 只写:make(chan <- Type),只能写⼊数据,不能读取数据
chan <- data
- 创建通道时,采⽤单向通道,没有意义的。都是创建双向通道。
- 将通道作为参数传递的时候使⽤单向通道。
定义函数,只有写⼊数据功能,或定义函数,只有读取数据功能。
- 定向通道的意义:在语法级别,保证通道的操作安全
- 示例代码
func main() {
//1.双向通道
ch1 := make(chan string)
go fun1(ch1)
data := <-ch1
fmt.Println("main,接受到数据:", data)
ch1 <- "Go语⾔好学么?"
ch1 <- "区块链好学么?"
go fun2(ch1)
go fun3(ch1)
time.Sleep(1 * time.Second)
fmt.Println("main over!") }
func fun1(ch1 chan string) {
ch1 <- "我是Steven⽼师"
data := <-ch1
data2 := <-ch1
fmt.Println("回应:", data, data2) }
//功能:只有写⼊数据
func fun2(ch1 chan<- string) {
//只能写⼊
ch1 <- "How are you?"
//<- ch1 //invalid operation: <-ch1 (receive from send-only type chan<- string) }
//功能:只有读取数据
func fun3(ch1 <-chan string) {
data := <-ch1
fmt.Println("只读:", data)
//ch1 <- "hello" //invalid operation: ch1 <- "hello" (send to receive-only type <-chan
string) }