golang:协程goroutine、管道通信channel

Golang 专栏收录该内容
16 篇文章 0 订阅

协程

原先使用的进程线程,在进行切换的时候都要切换到内核态:在进程控制块PCB中保存中断现场,然后再将CPU恢复到下一个进程的现场。

使用协程,可以将中断现场信息保存在用户空间,对内核来说是透明的。在切换协程的时候无需切换到内核态,大大地减少了开销。

如下图,协程和线程是M:N的关系,效率高与否具体取决于协程调度器的调度。
图源自刘丹冰视频

GMP

G:表示goroutine协程。
M:表示thread线程。
P:表示processor处理器。

当创建新的协程的时候,优先将其加入某一个processor处理器的本地队列,若当前processor处理器的本地队列都满了,则进入全局队列。processor处理器负责调度某一个协程执行,每一个processor能够让一个协程执行,实现多个协程并行的效果。其中,协程并行的数量取决于处理器的数量,可以通过GOMAXPROCS限定并行个数。
图源自刘丹冰视频

调度器设计策略

1、复用线程策略

work stealing 机制

当出现如下图的情况:processor处理器允许执行协程,但本地队列为空,且全局队列也为空的时候,这时候另一个处理器中的本地队列不为空,就会从另一个处理器中的本地队列中“偷”一个协程到当前的处理器中执行。避免某一处理器长时间处于空闲,浪费资源。
在这里插入图片描述

hand off 机制

当处理器Processor当前正在执行的协程受到某些因素进入阻塞状态的时候,这时候会先创建/唤醒新的线程,然后让原先的协程独占线程M1,处理器移动到新的线程M3处继续后续的操作。避免处理器因某一协程长时间处于阻塞状态而影响执行效率。
在这里插入图片描述

2、并行利用策略

我们可以通过GOMAXPROCS限定P的个数。
比如有4个CPU,但是我们只需要用2个,就可以限定P的个数为2,剩余的2个留给其他进程使用。

3、抢占策略

以前的co-routine,当协程A和CPU处于绑定关系的时候,如果此时有协程B在等待使用CPU资源,那么只能等协程A主动释放CPU,才会轮到协程B。

现在goroutine,会通过一定的策略,如果有新的协程申请资源的话,原来的协程最多使用10ms,如果未主动释放,新的协程会抢占CPU资源。

4、全局G队列

全局队列中加入和取出协程都是需要加锁解锁才能进行操作的。某些情况,如处理器P想执行协程,但当前本地队列为空,那么会先考虑从其他处理器的本地队列中获取,如果其他处理器的本地队列也为空,那么处理器就会从全局G队列中获取一个协程来执行。

创建goroutine

使用语法go 方法名,即表示开启一个协程

import (
	"fmt"
)

func Goroutine1() {
	go method10("first")
	go method10("two")
	go method10("three")
	i:=0
	// for{}代表死循环
	for {
		fmt.Println("main",i)
		i++
		// 为了观察让程序让出cpu,休眠一会儿再继续向下
		//time.Sleep(1*time.Second)
	}
}

func method10(name string) {
	i:=0
	// for{}代表死循环
	for {
		fmt.Println(name,":",i)
		i++
		// 为了观察让程序让出cpu,休眠一会儿再继续向下
		//time.Sleep(1*time.Second)
	}
}

执行效果如下:
在这里插入图片描述
之所以要在主程序中也添加死循环,是因为一旦主程序停止执行,其他协程都会停止

func method10(name string) {
	i:=0
	// for{}代表死循环
	for {
		fmt.Println(name,":",i)
		i++
		// 为了观察让程序让出cpu,休眠一会儿再继续向下
		//time.Sleep(1*time.Second)
	}
}

func Goroutine2() {
	go method10("first")
	go method10("two")
	go method10("three")
	fmt.Println("main over")
}

执行输出结果:main over

匿名函数

直接使用在传入方法的地方定义方法。注意,如果想要方法执行,必须如func(参数) { } (参数) 在方法体结束后面添加添加()表明自调用,如果方法的定义有参数要传入参数。

func Goroutine3() {
	go func() {
		// 死循环
		for{
			fmt.Println("goroutine")
			func() {
				fmt.Println("sun")
			}()
			time.Sleep(1*time.Second)
		}
	// 在方法体后面加()表示自调用,如果方法有参数,还要输入参数
	}()
	for {
		time.Sleep(1*time.Second)
		fmt.Println("main")
	}
}

注意:使用匿名函数不应该带返回值,因为协程是与主函数并行的,无法通过协程中方法体的return 值来获得其返回值。

退出当前协程:runtime.Goexit()

当使用return的时候,只是表明退出当前的方法,要谨慎使用。如果想要停止协程执行,要使用runtime.Goexit()来退出协程:

func Goroutine4() {
	go func() {
		// 死循环
		for{
			fmt.Println("start")
			// ①如果在此处使用retun,则会退出当前方法,即协程停止,只会输出一次start
			// return
			func() {
				// ②如果在此处使用return,只会退出当前方法,start和over继续输出
				// return
				fmt.Println("sun")
				// ③真正结束当前协程的执行,只会输出一次start和sun
				runtime.Goexit()
			}()
			time.Sleep(1*time.Second)
			fmt.Println("over")
		}
		// 在方法体后面加()表示自调用,如果方法有参数,还要输入参数
	}()
	for {
		time.Sleep(1*time.Second)
		fmt.Println("main")
	}
}

注意:只会结束当前执行的程序(协程),其他不会被影响,因此如上面的例子,后面还会一直输出main。

channel

前面提到,协程之间、或与主程序是并行的,无法通过return获取返回值,因此就出现了channel来实现它们的通信。

创建:make(chan Type),也可以在创建时先指定容量make(chan Type,capacity),若不指定默认是0,即无缓冲.
存入数据:channel名 <- value
接收数据并丢弃:<- channel名
使用变量接收数据变量 := <- cahnnel名,后续可以通过变量获取值
接收数据并检查通道是否关闭或为空:变量名,布尔值 := <- cahnnel名

对于只是定义了channel的,并没有使用make创建的,是nil型,无法进行收发数据。

func Channel1() {
	// 创建channel方式一
	c1:=make(chan int)
	// 创建channel方式二:创建时指定容量,可以缓冲2个元素
	c2:=make(chan string,2)
	go func() {
		// 存入数据到channel中
		c1 <- 1001
		c2 <- "hello"
		c2 <- "world"
		c2 <- "lena"
	}()
	// 接收数据,同时flag用来判断通道是否关闭或是否为空
	b,flag:= <- c1
	fmt.Println(flag,b)	// true 1001
	// 接收数据
	x:= <- c2
	// // 接收数据并丢弃:因为没有定义变量接收
	<- c2
	y:= <- c2
	fmt.Println(x,y)	// hello lena
}

如何保证每次读取的时候,channel中都已经有值了呢?即如何实现同步?
这里采用的是阻塞方式,当要读取的程序进行读取的时候,发现channel还没有数据,会阻塞,直到有程序存入数据。同理,当程序要存入数据的时候,如果发现读取的程序还未执行到读取数据的步骤,那么存入数据的程序就会阻塞,直到读取数据的程序准备好。类似于java中的socket通信。即永远能保证读取在存入之后,因此异步也能够保证先后顺序。

那么在创建管道的时候,是否有指定容量,有无缓冲有什么区别?

无缓冲channel

无缓冲即表示通道内不允许暂存数据。当某一个goroutine准备好了传送数据的时候,如果另一goroutine还未准备好接收数据,那么发送方会阻塞等待接收,因为通道不允许缓冲数据,只能在发送和接收都准备好的时候能传送数据。
图源自刘丹冰
代码检测:当无缓冲的时候,结果的确是发送方会阻塞到接收方接收数据,接收方会阻塞直到发送方要发送数据。

func Channel2() {
	// 创建无缓冲channel
	c:=make(chan int)
	fmt.Println("容量",cap(c))
	// 存入数据
	go func() {
		defer fmt.Println("go over")
		for i:=0;i<3;i++ {
			c <- i
			fmt.Println("save",i,"len",len(c))
		}
	}()
	// 等待:看数据存入多少
	time.Sleep(1*time.Second)
	// 取数据
	for i:=0;i<3;i++ {
		res := <- c
		fmt.Println("get",res)
	}
	// 等待:确保协程执行完毕
	time.Sleep(2*time.Second)
}

有缓冲channel

在创建channel的时候,可以指定缓冲容量。当一个goroutine要存入数据的时候,如果当前通道内的数据量<通道的容量,那么就可以直接把数据放进通道内存放着,继续下一步操作,无需goroutine取数据。但如果要存放数据的时候容量满了,则会阻塞。
在这里插入图片描述
通过代码检测是否如上述所说:

func Channel3() {
	// 创建容量为3的channel
	c := make(chan int,3)
	// 存入数据
	go func() {
		defer fmt.Println("go over")
		for i:=0;i<4;i++ {
			c <- i
			fmt.Println("save",i,"len",len(c))
		}
	}()
	// 等待:确保数据存入
	time.Sleep(1*time.Second)
	// 取数据
	for i:=0;i<4;i++ {
		res := <- c
		fmt.Println("get",res)
	}
	// 等待:确保协程执行完毕
	time.Sleep(1*time.Second)
}

控制台输出:

save 0 len 1
save 1 len 2
save 2 len 3		// 容量满了,阻塞
get 0		// 获取一个数据
save 3 len 3		// 因为容量设置是3,会阻塞到容量小于3的时候才能存入
go over	
get 1
get 2
get 3

关闭channel:close(channel)

管道确定不再使用,或者想要结束某一个尝试读取数据的循环,可以进行关闭。

// 关闭管道
func Channel4() {
	c:=make(chan int)
	go func() {
		// 存入数据
		for i := 0; i < 4; i++ {
			c <- i
		}
		// 关闭channel
		close(c)
	}()
	// 死循环:取出管道内数据
	for {
		// 只要channel还是打开状态,就会尝试获取数据
		if data,ok := <-c;ok {
			fmt.Println(data)
		} else {
			fmt.Println("nodata")
			break
		}
	}
	fmt.Println("over")
}

关闭channel后,不能再向该channel发送数据(会引发panic错误后导致接收立即返回零值)。
关闭channel后,可以继续从channel接收数据,例如使用有缓冲channel,关闭时channel中有数据未取,关闭后也可以继续取数据。

select

单流程下,一个go只能监控一个channel的状态,select可以完成同时监控多个channel的状态

// 当参数类型一致的时候,可以不用都写
func fibonacii(data,quit chan int) {
	x,y:=1,1
	// 死循环,直到quit有信号
	for{
		// 同时监控data和quit的状态
		select {
		// 如果能向data内写入x,则会进入case内部
		case data <- x:
			x,y=x+y,x
		// 如果能从quit读到数据,则会进入case
		case <- quit:
			fmt.Println("quit")	// 退出
			return 	// 退出当前循环
		}
	}
}

// select
func Channel5() {
	data:=make(chan int)
	quit:=make(chan int)	// 如果要退出,向管道存放数据
	// 取数据
	go func() {
		for i:=0;i<10;i++ {
			// 从data中取数据 若无 则阻塞直到有数据
			fmt.Println(<-data)
		}
		// 相当于一个退出标志位:当退出取数据循环,表明已经不需要再存数据了
		quit <- 0
	}()
	// 存数据
	fibonacii(data,quit)
}

学习自刘丹冰Aceld视频:https://www.bilibili.com/video/BV1gf4y1r79E

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值