GoRoutine协程间通信

原发于taskhub

goroutine是Golang原生支持并发的基础,也是go语言中最基本的执行单元,它具有如下的特性:

  • 独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

在使用goroutine进行并发编程时,往往会遇到协程先后、交替执行的问题,此时可使用go语言中专有的数据结构chan(管道)进行协程间的通信,其中又可分为如下几个具体情形:

1. 无缓冲--单个channel变量

func groutine3(ch1 chan bool, e chan bool) {
	for i := 1; i <= POOL; i++ {
		fmt.Println("A",i)
		ch1 <- true
		if i%2 == 1 {
			fmt.Println("groutine-1:", i)
		}
	}
	e <- true
}

func groutine4(ch1 chan bool) {
	for i := 1; i <= POOL; i++ {
		fmt.Println("B",i)
		<-ch1
		if i%2 == 0 {
			fmt.Println("groutine-2:", i)
		}
	}
}

func main() {
	ch1 := make(chan bool)
	exit := make(chan bool)
	go groutine3(ch1,exit)
	go groutine4(ch1)
	<-exit
	time.Sleep(time.Second * 1)
}
  • 协程3作为生产者,负责给chan写入数据,写入数据后,未立即停止并阻塞协程,而是继续向下执行;等到第二次向chan写入数据时,若发现chan的数据未被读取,则阻塞等待;
  • 协程4作为消费者,需要读取chan内的数据;若在读取时发现chan为空,则阻塞协程并等待,直到chan被写入数据才继续向下执行
  • 无缓存模式下,chan的数据容量默认为1,即只能传入一个数据
  • channel是同步阻塞的
  • 上述半开半闭方式的协程交替执行会出现先后顺序混乱BAABBAAB,仅考虑起始条件而忽略了终止条件
  • 协程4开启->打印B->读取阻塞->协程3开启->打印A->写入chan->写入未阻塞->继续打印A->写入阻塞->协程4开启->打印B->读取chan->打印B->读取阻塞
  • goroutine的执行顺序不是代码行编写的前后顺序,而是按照一定规则

2. 无缓冲--两个channel变量

func groutine1(ch1 chan bool, ch2 chan bool, e chan bool) {
	for i := 1; i <= POOL; i++ {
		ch1 <- true
		fmt.Println("A1",i)
		if i%2 == 1 {
			fmt.Println("groutine-1:", i)
		}
		fmt.Println("A2",i)
		<- ch2
		fmt.Println("A3",i)
	}
	e <- true
}

func groutine2(ch1 chan bool, ch2 chan bool) {
	for i := 1; i <= POOL; i++ {
		<-ch1
		fmt.Println("B1",i)
		if i%2 == 0 {
			fmt.Println("groutine-2:", i)
		}
		fmt.Println("B2",i)
		ch2 <- true
		fmt.Println("B3",i)
	}
}
func main() {
	ch1 := make(chan bool)
	ch2 := make(chan bool)
	exit := make(chan bool)
	go groutine1(ch1,ch2,exit)
	go groutine2(ch1,ch2)
	<-exit
	time.Sleep(time.Second * 1)
}
  • 生产者-消费者模式
  • 循环体首尾阻塞独占模式,两个chan交替释放控制权
  • 协程1向ch1写入数据后,执行后续代码段,待到读取ch2的数据时发生阻塞,协程1阻塞等待
  • 协程2读取ch1数据,执行后续代码段,然后向ch2写入数据,并再进入一次for循环,但此次被阻塞在ch1的读取阶段
  • 协程2阻塞后,轮到协程1执行,协程1在等待ch2被写入数据后,开始执行后续代码
  • 完成了两个协程的交替运行,且实现了并发安全

3. 缓冲模式

缓冲区可以存储10个int类型的整数,在执行生产者线程的时候,线程就不会阻塞,一次性将10个整数存入channel,在读取的时候,也是一次性读取。

package main

// 带缓冲区的channel

import (
	"fmt"
	"time"
)

func produce(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("Send:", i)
	}
}

func consumer(ch <-chan int) {
	for i := 0; i < 10; i++ {
		v := <-ch
		fmt.Println("Receive:", v)
	}
}

func main() {
	ch := make(chan int, 10)
	go produce(ch)
	go consumer(ch)
	time.Sleep(1 * time.Second)
}

4. 总结

  • 在有缓冲模式下,可以在主线程中先写后读,无需新开协程负责读取
  • 在无缓冲模式下,写入数据的同时必须有协程在等待读取管道中的数据,否则将抛出死锁的错误
  • ch := make(chan int,1) ch := make(chan int) 在使用上完全不一样
  • 无缓冲模式是同步阻塞,有缓冲模式是异步执行
原文地址:https://www.cnblogs.com/litchi99/p/13724821.html