【Go语言5-1】如何浅显理解Go语言并发编程中的协程goroutine运行机制

本文深入探讨Go语言的并发编程特性,重点讲解协程goroutine的基本使用、执行过程以及并发控制。通过示例展示了如何启动和管理多个协程,利用sync.WaitGroup确保协程的有序结束。同时,提到了defer关键字在协程中的作用和触发机制。
摘要由CSDN通过智能技术生成

在上一篇幅中介绍了基础内容:

1、Go语言中关于函数定义、闭包函数、作用域、defer关键字详解
2、关于Go语言中的方法详解以及不提供继承机制的解决方案
3、关于Go语言中的接口与空接口解读
4、关于Go语言中的反射功能解读以及反射包reflect的相关API调用

前一篇中我们知道Go语言没有继承,通过struct和方法的组合来实现。
此外, Go语言作为一个静态语言,接口的出现让接口值呈现动态,可以在程序执行时候再确定动态类型和动态值。

并发编程

在CPU的早期,主要目标是提升处理器的处理频率。在提升频率遇到瓶颈后,CPU发展进入了多核时代。
编程语言也开始向并行化的模式发展,Go语言的并发是基于并行的。
并行是指同时运行多个线程,而并发是把一个任务拆分为多个小块去执行(本质上是时间轮片,轮番调度同一个处理器),不过在当前系统线程阻塞的情况下才会分配给其他核的线程。

主流的并行编程模式有多种,最为熟知的是多线程。多线程的并发可以自然地对应到多核处理器。
Go语言的多线程是基于消息传递的。Go语言将基于CSP(Communicating Sequential Process)模型的并发编程内置到语言中,其特点就是goroutine(协程)之间是共享内存的。

我们需要记住,Go语言最引人注目的是其高性能的并发编程,它是通过goroutine(协程是可独立执行的单元)和channel(通道是协程之间传递消息)实现的。

一、协程 goroutine

协程(goroutine)是Go语言特有的一种轻量级线程,使用Go语言关键字启
动。goroutine和系统线程是不一样的,不可等同来看。
实际上,所有的Go语言都是通过goroutine运行的,我们所熟知的main函数
也是启用一个goroutine来调用的。

1.1 协程概念

这里不在对进程、线程、协程进行概念的叙述,goroutine比线程更为轻量,而线程比进程更为轻量。
一个进程可以有多个线程,每个线程可以有多个goroutine;反过来goroutine需要一个有进程的环境才可以运行。goroutine运行的时候,需要有一个进程,并且进程至少有一个线程,进程和线程都由系统负责调度和管理,而Go语言工程师只需要负责goroutine即可。

Go语言程序是通过调度程序组件使用m:n的调度技术来运行goroutine的。m:n是指多路复用n个操作系统线程执行m个goroutine

1.2 协程基本使用

启动一个协程,我们是使用go关键字去启动goroutine

func HelloWorld()  {
	fmt.Println("Hello,World!")
}

func main() {
	go HelloWorld()
	time.Sleep(1*time.Second)
	fmt.Println("The End!")
}

运行的结果:

Hello,World!
The End!

这样看,大家可能觉得没有什么太大的问题,是顺序执行的。当我们把休眠这行代码注释掉,运行结果会发现:

The End!

这是因为,运行程序的时候,我们知道main函数其实也是一个goroutine运行,当我们通过关键字go启动新的协程需要一定时间,此时main函数会执行后面的代码,直接结束掉main函数所在的协程。
只要main函数结束,就会强制终止main函数程序中所有的goroutine程序。

1.3 协程的执行过程

func main() {
	go func() {
		for i:=10;i<20;i++{
			fmt.Print(" ",i)
		}
	}()
	fmt.Println("====================================")
	for i:=0;i<10;i++{
		fmt.Print(" ",i)
	}
	time.Sleep(2*time.Second)
}

运行结果:

====================================================
 10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9

main函数运行启动的goroutine叫作主goroutine,和关键字go自启动goroutine之间是并发执行的,执行过程中彼此没有关系。

1.4 启动多个协程

func main() {
	for i:=0;i<20;i++{
		go func(i int) {
			fmt.Print(" ",i)
		}(i)
	}
	time.Sleep(2*time.Second)
	fmt.Println("\nThe End!")
}

运行结果:

 1 18 2 4 3 11 5 6 13 7 8 14 9 10 15 17 16 19 0 12
The End!

循环语句中,通过匿名函数启动了20个goroutine,这20个协程之间是并发执行的,不额外控制的话,顺序就是随机的。
存在休眠的目的是为了让协程的程序能执行完成,才会执行后面的语句。

1.5 sync包指定协程结束时间

var wg sync.WaitGroup

在这里插入图片描述
sync.WaitGroupstate1字段是一个计数器, 其用法也很好理解。每当有一个goroutine运行的时候就调用Add方法给计数器加1,待一个goroutine运行完后,通过Done方法为计数器减1,然后使用Wait方法等待计数器的数变为o。

Add()、Wait()方法是在主goroutine中运行的,而Done()方法是在自启动的goroutine中运行的。

func main() {
	var wg sync.WaitGroup
	for i:=0;i<20;i++{
		wg.Add(1) //每当有一个goroutine运行时候调用Add加1
		go func(x int) {
			defer wg.Done() //当有一个goroutine运行结束调用Done进行减1
			fmt.Print(" ",x)
		}(i)
	}
	fmt.Printf("\n%#v\n",wg)
	wg.Wait() //使用Wait方法等待计数器的数变为0
	fmt.Println("\nThe End!")
}

这里的关键字defer存在,defer关键字后面跟函数调用语句,defer的触发机制包含下面三种情况:
这里我们需要注意调用时机(偏向于延迟调用):

  • defer所在的函数返回时,触发defer后面的函数调用语句。
  • defer所在的函数执行到末尾时,触发defer后面的函数调用语句。
  • defer所在的函数报panic时, 触发defer后面的函数调用语句。

注意:当我们将 wg 传入到方法内的时候,我们需要传递的是地址而不能是变量,因为值传递过程中,地址作为参数传入,会与方法作用域外一致。而传入的是变量,比如 ( wg sync.WaitGroup )则会产生新的变量与作用域外不一致,导致死锁问题出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值