Go 并发

482 阅读4分钟

1 协程

线程是操作系统的内核对象,有 cpu时间片 的概念,线程之间进行抢占式调度。在 java 中,尽管 jvm 是系统的一个进程,但 cpu 的最小调度单位是线程而非进程,所以 java 中多线程仍然可能会被 OS 调度到多个 cpu中执行。

协程也被称作“用户态线程”,操作系统不会感知到协程的存在,由 go 进程对协程进行分配,所以协程切换不会存在线程上下文切换(用户态和内核态)的损耗。

协程之间是协作式调度,将 goroutine 合理地分配到每个 CPU 中,最大限度地使用多核CPU性能。一个协程在哪个线程上执行是不确定的,由调度器决定。

特点:

1. 轻量级,所有的消耗几乎只有栈空间的分配,并且开始时栈非常小,仅在需要时才会分配堆空间。 
2. 同一个线程内部最多有一个协程正在运行。当协程睡眠或结束后,线程的运行权会转让给其他协程。
3. 子协程的异常退出会传播到主协程,直接导致主协程也跟着挂掉。

2 通道

共享内存通道是程序中最常用的协程通信方式。

锁属于共享内存中的一种方式,至于共享内存是如何进行通信的,详见博客:juejin.cn/post/698101…

事实上 go 主要使用通道作为通信模型。

创建通道
ch := make(chan int, len)  // len 表示缓冲区大小,FIFO
ch <- 1   // 写入
<- ch     // 读取

注意区分无缓冲区通道和缓冲区为 1 的通道。缓冲通道的发送和接收不会互相依赖,用异步的方式传递数据。

无缓冲区通道会造成协程阻塞,阻塞 channel 的发送或接收可能会引起 goroutines 的内存泄漏:即使被阻塞的 channel 无法访问,垃圾收集器也不会终止 goroutine。

关闭通道

通过 close() 方法能够关闭通道,读取一个已经关闭的通道会立即返回通道类型的零值(如果缓冲区中还有数据,会读出来),而写入一个已经关闭的通道会造成 panic。

通过 for-range 读通道,能够避免读取已关闭通道的零值,当通道为空时,循环会阻塞;当通道关闭,循环会停止。

var c = make(chan int, 3)

//子协程写
go func() {
	c <- 1
	defer close(c)
}()

//直接读取通道,存在不知道子协程是否已关闭的情况
//fmt.Println(<-c)

//主协程读取:使用for...range安全的读取
for value := range c {
	fmt.Println(value)
}

如何安全的写通道,确保不会写入已经关闭的通道而导致异常呢?

确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。

但这种方式只能解决单个写通道的情形,我们可以使用内置的 sync.WaitGroup,它使用计数来等待指定事件完成,需要额外一个专门的通道做这件事。

func main() {

	var ch = make(chan int, 8)

	var wg = new(sync.WaitGroup)

        //写协程
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go func(num int, ch chan int, wg *sync.WaitGroup) {
			defer wg.Done()
			ch <- num
			ch <- num * 10
		}(i, ch, wg)
	}

	//读
	go func(ch chan int) {
		for num := range ch {
			fmt.Println(num)
		}
	}(ch)

	//Wait阻塞等待所有的写通道协程结束,待计数值变成零,Wait才会返回
	wg.Wait()

	//安全的关闭通道
	close(ch)

	//防止读取通道的协程还没有完毕
	time.Sleep(time.Second)

	fmt.Println("finish")
}

3 协程和通道的常见场景

消息队列
type message struct {
	num int
}

func main() {

	ch := make(chan message, 10)

	go connsumer(ch)

	go produce1(ch)
	go produce2(ch)

	time.Sleep(time.Second)
}

func producer1(c chan<- message)  {
	for i := 0; i < 10; i++ {
		c <- message{i}
	}
}

func producer2(c chan<- message)  {
	for i := 10; i < 20; i++ {
		c <- message{i}
	}
}

func connsumer(c <-chan message)  {
	for m := range c{
		fmt.Println(m)
	}
}
替代锁的功能

首先需要声明的是,chan 的设计目的不是为了替代(取代)锁的功能,具体情况到底是用 chan 还是直接用锁,需要具体情况具体分析,大的决策点是,哪种简单方便更易理解更具有可读性就用哪种。(显然,使用chan实现共享变量在协程之间的安全性,不如直接使用锁可读性高)

func main() {
	x := 0
	ch := make(chan int, 1)

	for i := 0; i < 1000; i++{
		go func() {
			ch <- i
			x ++
			<- ch
		}()

		go func() {
			ch <- i
			x --
			<- ch
		}()
	}

	time.Sleep(5 * time.Second)

	fmt.Println(x)
}

4. channel 底层原理

channel 底层数据结构

   type hchan struct {
       qcount   uint           
       dataqsiz uint           
       buf      unsafe.Pointer  // 缓冲数据队列 
       elemsize uint16 
       closed   uint32 
       elemtype *_type 
       sendx    uint   // 后续写数据的位置
       recvx    uint   // 后续读数据的位置
       recvq    waitq  // 等待读消息的goroutine队列
       sendq    waitq  // 等待写消息的goroutine队列
       lock mutex      // 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作
  }

发送数据:

image.png

读取数据:

image.png