Channel
底层是一个先进先出的环形列队,固定大小的环形数组实现。
- full或empty就会阻塞
- send发送
- recv接收并移除
- sendx表示最后一次插入元素的index
- recvx表示最后一次接收元素的index
- 发送、接收的操作符是
<-
构造通道
nil通道
package main
import "fmt"
func main() {
var c1 chan int
fmt.Printf("c1: %d,%d,%v\n", len(c1), cap(c1), c1) //c1: 0,0,<nil>
c1 <- 111 //阻塞,deadlock,没有初始化的chan,写不进去
<-c1 //阻塞,deadlock,没有元素可以拿出
}
nil通道:chan的零值是nil,可以理解为未被初始化的通道容器。nil通道可以认为是一个只要操作就阻塞当前协程的容器。这种通道不要使用,阻塞无法解除发生死锁。
非缓冲通道
非缓冲通道:容量为0的通道,也叫同步通道。这种通道发送第一个元素是,如果没有接收操作就立即阻塞,直到被接收。同样接收时,如果没有数据被发送就立即阻塞,直到有数据发送。
缓冲通道
缓冲通道:容量不为0的通道。通道已满,再往通道发送数据的操作就被阻塞;通道为空,再从该通道取数据就会被阻塞。
package main
import "fmt"
func main() {
c4 := make(chan int, 8) //缓冲通道,容量为8,长度为0
fmt.Printf("c4: %d,%d,%v\n", len(c4), cap(c4), c4)
c4 <- 111
c4 <- 222
fmt.Printf("c4: %d,%d,%v\n", len(c4), cap(c4), c4)
<-c4
t1 := <-c4
t2 := <-c4
fmt.Printf("%T %[1]v\n", t1)
fmt.Printf("%T %[1]v\n", t2)
}
单向通道
<- chan type
这种定义表示只从一个通道里读,只读通道。chan <- type
这种定义表示只往一个通道里写,只写通道。
package main
import (
"fmt"
"math/rand"
"sync"
)
func produce(ch chan<- int) { //生产,只写,只要该通道具有写能力就行
for {
ch <- rand.Intn(10)
//time.Sleep(1 * time.Second)
}
}
func consume(ch <-chan int) { //消费,只读
for {
t := <-ch
fmt.Println("消费,从只读通道接收", t)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := make(chan int, 8) //创建可读可写的非缓冲通道
go produce(c)
go consume(c)
wg.Wait()
}
通道关闭
- 使用
close(ch)
关闭通道 - 只有发送方才能关闭通道,一旦通道关闭,发送者就无法发送数据了,否则panic
- 通道关闭的作用:告诉接收者再无薪数据到达了
- 通道关闭:
t,ok := <-ch 或 t:= <-ch
从通道读取数据- 正在阻塞中的等待通道中数据的接收者,由于通道被关闭,接收者将不再阻塞,获取数据失败,ok为,false,返回零值
- 接收者依然可以访问关闭的通道而不阻塞
- 如果通道内还有剩余数据,ok为true,接收数据
- 如果通道内剩余的数据都被读完了,继续接收不阻塞,ok为false,返回零值。
- 已经关闭的通道,若再次关闭则抛出panic。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func produce(ch chan<- int) { //生产,只写,只要该通道具有写能力就行
for i := 0; i < 10; i++ {
ch <- rand.Intn(10)
time.Sleep(time.Second)
if i == 9 {
close(ch)
return
}
}
}
func consume(ch <-chan int) { //消费,只读
for {
if t, ok := <-ch; !ok {
fmt.Println("通道已关闭", t, ok, len(ch), cap(ch))
} else {
fmt.Println("消费,从只读通道接收", t, ok, len(ch), cap(ch))
}
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := make(chan int) //创建可读可写的非缓冲通道
go produce(c)
go consume(c)
wg.Wait()
}
通道遍历
1、nil通道
- 发送、接收、遍历都阻塞
2、缓冲的、未关闭的通道
- 相当于无限元素的通道,遍历不完,阻塞在等下一个元素的到达。
package main
import "fmt"
func main() {
c1 := make(chan int, 5)
fmt.Printf("c1: %d %d %v\n", len(c1), cap(c1), c1)
c1 <- 111
c1 <- 222
c1 <- 333
fmt.Println(<-c1, "===========") //读取一个
for v := range c1 {
fmt.Println(v, "===========")
}
fmt.Println("遍历结束")
}
3、缓冲的、关闭的通道
- 关闭后,通道不能进入新的元素,相当于遍历有限个元素的容器,遍历完就结束
package main
import "fmt"
func main() {
c1 := make(chan int, 5)
fmt.Printf("c1: %d %d %v\n", len(c1), cap(c1), c1)
c1 <- 111
c1 <- 222
c1 <- 333
fmt.Println(<-c1, "===========") //读取一个
close(c1)
for v := range c1 {
fmt.Println(v, "===========")
}
fmt.Println("遍历结束")
}
/*
输出:
c1: 0 5 0xc000104000
111 ===========
222 ===========
333 ===========
遍历结束
*/
4、非缓冲的、未关闭的通道
- 相当于一个无限元素的通道,阻塞等待下一个元素到达
5、非缓冲的、关闭的通道
- 关闭后,通道不能进入新的元素,那么相当于遍历有限个元素容器。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan int)
fmt.Printf("c1: %d %d %b\n", len(c1), cap(c1), c1)
go func() {
defer close(c1)
count := 1
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
c1 <- count
count++
}
}()
for v := range c1 {
fmt.Println(v, "======")
}
fmt.Println("遍历结束")
}
总之:
- 未关闭的通道,如同一个无限的容器,将一直迭代通道内的元素,没有元素就阻塞,最后死锁。
- 已关闭的通道,将不能加入新的元素,迭代完当前通道内的元素,哪怕是0个元素,然后结束迭代。
定时器
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second) //通道阻塞2秒后只能接收一次
for {
fmt.Println(<-timer.C)
}
}
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTicker(2 * time.Second)
for {
fmt.Println(<-t.C) // 通道每阻塞2秒就接收一次
}
}
通道死锁
Channel满了,就阻塞写;Channel空了,就阻塞读。容量为0的通道可以理解为0个元素就满了。阻塞了当前协程之后会交出CPU,去执行其他协程,希望其他协程可以帮助自己解除阻塞。main函数结束了,整个进程就结束了。如果在main协程中,执行语句阻塞时,已经没有其他子协程可以执行,只剩下主协程自己,解锁无望,就把自己kill掉,抛出deadlock.
2023-08-31 14:28:45.997312 +0800 CST m=+2.001392524
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
如果通道阻塞不再main协程中发生,而是发生在子协程中,子协程会继续阻塞着,也可能发生死锁。但是由于至少main协程还在,编译器无法识别出死锁,如果真的无任何协程帮助该协程解除阻塞状态,那么事实上该子协程已经死锁了。
死锁的危害可能会导致进程还活着,但实际上某个协程未能真正工作而阻塞,增加cpu消耗。
结构体型通道
struct{}
是一个结构体类型,这个结构体实例没有成员,也就是实例内存占用为0,这种类型数据构成的通道,非常节约内存,仅仅是为了传递一个信号标识。
package main
import (
"fmt"
"time"
)
func main() {
flag := make(chan struct{})
go func() {
time.Sleep(2 * time.Second)
flag <- struct{}{} //无数据成员的结构体实例
}()
fmt.Printf("信号: %T %[1]v\n", <-flag)
}
通道的多路复用
Go语言中提供了select
来监听多个channel,主要有一路好了就可以执行。select{}可以永远阻塞。
package main
import (
"fmt"
"time"
)
func main() {
count := make(chan int, 4)
fin := make(chan bool)
go func() {
defer func() { fin <- true }()
for i := 0; i < 10; i++ {
count <- i
//time.Sleep(time.Second)
}
}()
for {
select { //监听多路通道
case n := <-count:
fmt.Println("count = ", n)
case <-count:
fmt.Println("结束")
goto END
default:
fmt.Println("nothing")
time.Sleep(time.Second)
}
}
END:
fmt.Println("++++++++++++++++++++")
}
package main
import (
"fmt"
"time"
)
func main() {
count := make(chan int, 4)
fin := make(chan bool)
newBase := 1000
t1 := time.NewTicker(time.Second)
t2 := time.NewTicker(3 * time.Second)
go func() {
defer func() { fin <- true }()
for i := 0; i < 4; i++ {
count <- i
}
}()
time.Sleep(time.Second)
fmt.Println("len_count=====", len(count))
for {
select {
case <-t1.C:
fmt.Println("每1秒", len(count), <-count)
case <-t2.C:
fmt.Println("每3秒", len(count), <-count)
case count <- newBase: //发送数据成功进入通道执行case
newBase++
fmt.Println("over-------")
}
}
}
通道并发
Go语言采用并发同步模型叫做Communication Sequential Process
通讯顺序进程,这是一种消息传递模型,在goroutine
之间传递消息,而不是对数据进行加锁来实现同步访问。在goroutine
之间使用channel来同步和传递数据。
- 多个协程之间通讯的管道
- 一端推入数据,一端取走数据
- 同一时间,只有一个协程可以访问通道的数据
- 协调协程的执行顺序
如果多个线程都使用同一个数据,就会出现竞争问题。因为线程的切换不会听从程序员的意志,时间片用完就切换了。解决办法往往是加锁,让其他线程不能对共享数据进行修改,从而保证逻辑正确,但锁的引入带来了并行性能问题。
假设有个需求:一个全局的数count初始为0,编写一个函数inc,能够对count增加100000次,执行5次inc函数,请问最终count是多少?很显然最终count为50w才对,但是并发情况下可能会有问题:
1、串行:
package main
import (
"fmt"
"runtime"
"time"
)
var count int64 = 0
func inc() {
for i := 0; i < 100000; i++ {
count++
}
}
func main() {
start := time.Now()
for i := 0; i < 5; i++ {
inc()
}
fmt.Printf("协程数: %d\n", runtime.NumGoroutine())
fmt.Printf("执行时长:%d 微妙\n", time.Since(start).Microseconds())
fmt.Printf("count和:%d\n", count)
}
//输出:
协程数: 1
执行时长:1905 微妙
count和:500000
2、并发:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
var count int64 = 0
func inc() {
defer wg.Done()
for i := 0; i < 100000; i++ {
count++
}
}
func main() {
start := time.Now()
wg.Add(5)
for i := 0; i < 5; i++ {
go inc()
}
fmt.Printf("协程数: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("执行时长:%d 微妙\n", time.Since(start).Microseconds())
fmt.Printf("协程数: %d\n", runtime.NumGoroutine())
fmt.Printf("count和:%d\n", count)
}
//输出:
协程数: 6
执行时长:806 微妙
协程数: 1
count和:278070
当设置 runtime.GOMAXPROCS(1) 可以输出正确结果:
协程数: 6
执行时长:1126 微妙
协程数: 1
count和:500000
以上原因在于count++不是原子操作,会被打断,所以即使使用goroutine也会有竞争,存在并发安全问题。
改进:
func inc() {
defer wg.Done()
for i := 0; i < 100000; i++ {
//count++
atomic.AddInt64(&count, 1)
}
}
协程数: 6
执行时长:14515 微妙
协程数: 1
count和:500000
结果正确了,但是执行时间长了
1、使用互斥锁来保证原子性
var wg sync.WaitGroup
var mx sync.Mutex //互斥锁
var count int64 = 0
func inc() {
defer wg.Done()
for i := 0; i < 100000; i++ {
mx.Lock()
count++
mx.Unlock()
//atomic.AddInt64(&count, 1)
}
}
协程数: 6
执行时长:28998 微妙
协程数: 1
count和:500000
2、使用通道,来同步多个协程
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
var ch = make(chan int64, 1)
func inc() {
defer wg.Done()
for i := 0; i < 100000; i++ {
t := <-ch
t++
ch <- t
}
}
func main() {
start := time.Now()
ch <- 0
wg.Add(5)
for i := 0; i < 5; i++ {
go inc()
}
fmt.Printf("协程数: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("执行时长:%d 微妙\n", time.Since(start).Microseconds())
fmt.Printf("协程数: %d\n", runtime.NumGoroutine())
fmt.Printf("count和:%d\n", <-ch)
}
协程数: 6
执行时长:111325 微妙
协程数: 1
count和:500000
上述例子是计算密集型,对同一个数据进行争抢,不能发挥并行计算的优势。也不适合用通道,用锁实现反而效率更高。
通道适合数据流动的场景
- 如同管道一样,一级一级处理,一个协程处理完后,发送给其他协程
- 生产者、消费者模型,M:N
协程泄露
- 协程阻塞,未能如期结束,之后就会大量堆积
- 协程阻塞最常见的原因都和通道有关
- 由于每个协程都要占用内存,所以协程泄露也会导致OOM
- 合理使用协程,要清楚协程什么时候能结束。