[Golang] Channel
文章目录
- [Golang] Channel
- 什么是Channel
- channel的初始化
- channel的操作
- 双向channel和单向channel
- 为什么有channel
- 有缓冲channel和无缓冲channle
- channel做一把锁
从之前我们知道go关键字可以开启一个Goroutine,但是Goroutine之间的通信还需要另一个条件:channel
什么是Channel
官方定义:Channel are typed conduit through which you can send and receive values with the channel operator.
channel是一个可以收发数据的管道。
channel的初始化
var channel_name chan channel_type
var channel_name [size]chan channel_type//声明一个容量为size的channel
例如:
var ch1 chan int
var ch2 [1]chan int
但是声明后的channel,我们没有进行初始化为其分配空间,其值为nil
,我们还需要使用make
函数来对其初始化,之后才可以在程序中使用该管道。
channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)
例如:
ch1 := make(chan int)
ch2 := make(chan int, 1)//一个容量为size的channel
channel的操作
发送数据:
ch := make(chan int) // 创建管道
ch <- 1 // 向管道发送数据
v := <-ch // 从管道读取数据,并存储的变量v
close(ch) // 关闭管道
注意:用完管道后,我们需要关闭管道close(ch)
,避免程序一直等待以及资源的浪费。但是关闭的管道仍然能读取数据,如果管道中还有数据,那就可以读到实际的值;如果管道中没有数据,此时读取的值就是该类型的零值,不会阻塞等待数据。
比如,例:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
ch <- 1
close(ch)
go func() {
for i := 0; i < 5; i++ {
v := <-ch
fmt.Println(v)
}
}()
time.Sleep(2 * time.Second)
}
执行结果:
创建一个缓存为5的int类型管道,向管道里写入一个1之后关闭管道。开启一个Goroutine从管道中读取数据,读5次,我们可以看到从第二次开始,读到的数据一直是0。
但是如果我们想要向管道中写入0呢?
一般采用:
1.判断读取:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
ch <- 1
close(ch)
go func() {
for i := 0; i < 5; i++ {
v, ok := <-ch // 判断式读取
if ok {
fmt.Println("数据读完了", v)
} else {
fmt.Println("数据没读完", v)
}
}
}()
time.Sleep(2 * time.Second)
}
执行结果:
我们在读取数据时,加上了一个ok进行判断。ok为true时,读取的是正常值;ok为false,读取的是零值。
2.for range 读取
有时,我们的读取是不知道次数的,只是在channel中进行读取,有数据我们就读,直到管道关闭。
此时可以使用for range 读取管道中的数据
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
close(ch)
go func() {
for i := 0; i < 5; i++ {
for v := range ch {
fmt.Println(v)
}
}
}()
time.Sleep(2 * time.Second)
}
执行结果:
我们向管道中只写入了1,2,3,三个数据,之后就关闭了管道。Goroutine中也只能读到三个数据,然后管道被关闭了,Goroutine的for range循环也就退出了。
双向channel和单向channel
channel根据其功能又能分为双向channel和单向channel,双向channel既可以发送数据又可以接收数据,单向channel要么是发送数据,要么是接收数据。
单向读channel
var ch = make(chan int)
type ReadChannel = <-chan int // 给 <-chan int取个别名
var rec ReadChannel = ch
单向写channel
var ch = make(chan int)
type SendChannel = chan<- int
var sec SendChannel = ch
读channel与写channel在定义时只是<-的位置不同,读在chan关键字前,写在chan关键字后。
使用示例:
package main
import (
"fmt"
"time"
)
type ReadChannel = <-chan int
type SendChannel = chan<- int
func main() {
var ch = make(chan int)
defer close(ch)
go func() {
var rec ReadChannel = ch
v := <-rec
fmt.Println(v)
}()
go func() {
var sec SendChannel = ch
sec <- 100
}()
time.Sleep(2 * time.Second)
}
执行结果:
创建一个读channel,一个写channel,向写channel中写入100,从读channel中读取数据。
为什么有channel
Golang中有个重要的思想:不以共享内存来通信,以通信来共享内存,channel就是其特点。
也就是说,协程之间可以利用channel来传递数据,以下例子可以看出父子协程是如何通过channel通信的:
package main
import (
"fmt"
"time"
)
func sum(nums []int, ch chan int) {
cnt := 0
for _, v := range nums {
cnt += v
}
ch <- cnt
}
func main() {
var ch = make(chan int)
defer close(ch)
nums := []int{-5, 4, -3, 2, -1} // 和为-3
go func() {
sum(nums[:len(nums)/2], ch)
}()
go sum(nums[len(nums)/2:], ch)
m, n := <-ch, <-ch
fmt.Println(m, n, m+n)
time.Sleep(2 * time.Second)
}
执行结果:
有缓冲channel和无缓冲channle
之前初始化时,我们已经说明了channel分为有缓冲和无缓冲两种。
为了协程安全,有缓冲channel和无缓冲channle的内部都有一把锁来控制并发访问。
同时,channel底层一定有一个队列来存储数据。
无缓冲channel可以理解为同步模式,即写入一个,如果消费者不消费,写入就会阻塞
有缓冲channel可以理解为异步模式,即写入消息后,即使没有被消费,只要队列没有满,就可以继续写入。
此时如果,channel满了,异步就会退化为同步,发送还是会阻塞。如果一个channel长期处于满队列状态,就没必要使用有缓冲channel了,直接有无缓冲即可。
所以大部分情况,有缓冲的channel一般用来做异步操作。
- 无缓冲channel:适合用于严格同步的场景,比如两个Goroutine之间进行同步,要严格确保操作顺序。
例如,两个协程循环打印A、B
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
var Ach = make(chan int)
var Bch = make(chan int)
defer close(Ach)
defer close(Bch)
go PrintA(&wg, Ach, Bch)
go PrintB(&wg, Ach, Bch)
Ach <- 1 // 从A启动
wg.Wait()
}
func PrintA(wg *sync.WaitGroup, Ach chan int, Bch chan int) {
defer wg.Done()
for {
<-Ach
fmt.Println("A")
time.Sleep(time.Second)
Bch <- 1 // 通知打印B
}
}
func PrintB(wg *sync.WaitGroup, Ach chan int, Bch chan int) {
defer wg.Done()
for {
<-Bch
fmt.Println("B")
time.Sleep(time.Second)
Ach <- 1 // 通知打印A
}
}
- 有缓冲channel:适合一定程度的异步处理的场景,比如提高吞吐量,减少Goroutine之间的阻塞。
channel做一把锁
因为缓冲队列满了之后,再往channel中写数据就会被阻塞,所以我们可以把channel当一把锁来使用
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan bool, 1)
var sum int
for i := 0; i < 1000; i++ {
go add(ch, &sum)
}
time.Sleep(2 * time.Second)
fmt.Println("sum = ", sum)
}
func add(ch chan bool, sum *int) {
ch <- true
*sum = *sum + 1
<-ch
}
执行结果:
如果不用锁,循环次数一多就会出现并发问题。