Channel底层是一个先进先出的环形列队,固定大小的环形数组实现。

  • full或empty就会阻塞
  • send发送
  • recv接收并移除
  • sendx表示最后一次插入元素的index
  • recvx表示最后一次接收元素的index
  • 发送、接收的操作符是<-

Go通道Channel_死锁

构造通道

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
  • 合理使用协程,要清楚协程什么时候能结束。