Go 的并发机制(3)

Go 的并发机制

4.2 goroutine

go 关键字时用户程序启用 goroutine 的唯一途径。

4.2.1 go 语句和 goroutine

go 语句是由 go 关键字和表达式组成的,其中调用表达式使用的比较多。调用表达式所表达的就是针对函数或方法的调用,其中的函数可以是命名的,也可以是匿名的。

go func() {
	println("goroutine")
}()

无论是否需要传递参数值给匿名函数,都需要在最后加上圆括号,它们代表了对函数的调用行为,也是调用表达式的必要组成部份。

Go 运行时系统对 go 语句中的函数的执行时并发的,更确切地说,当 go 语句执行的时候,其中的 go 函数会被单独放入一个 goroutine 中。这之后,该 go 函数的执行会独立于当前 goroutine 运行。

至于执行时机,一句话:go 函数并发执行,但是谁先谁后并不确定。还有就是,go 函数是可以有结果声明的,但是其返回的结果值会在执行完成时被丢弃。(可以使用 channel 传递给其他 goroutine)

package main
func main() {
	go println("hello")
}

上述程序执行,大概率不会打印任何东西。一旦 main 函数执行结束,就意味着 Go 程序运行的结束,但这个时候运行 go 函数的 G 可能还没来得及执行。

可以使用 time.Sleep 或者 runtime.Gosched 暂停 main 函数所在的 goroutine,这样子就会执行打印的 goroutine。但是,复杂情况下这种方式也并不保险。

再介绍一个有趣的例子:

package main
import (
	"fmt"
	"time"
)
func main() {
	names := []string{"rob", "marry", "bob", "alice"}
	for _, name := range names {
		go func() {
			fmt.Printf("name: %v\n", name)
		}()
	}
	time.Sleep(time.Millisecond)
}

上述程序运行结果会是什么呢?不妨先猜测一下,下面揭晓答案:

name: alice
name: alice
name: alice
name: alice

当然,如果读者拷贝上述代码在自己本地运行,可能并不是这个执行结果。那你的 go 版本一定是 1.22 版本及以上的,因为这些版本已经对此进行了优化调整。(之前是for 循环的变量只创建一次,在每次循环的时候都会更新,而闭包在访问 name 的时候实际都访问的是同一个内存地址,所以最终打印都是同一个值。)我们这里考虑1.21 版本之前的,因为实际运用中还有很多环境可能并没有升级到这个版本。

那么我们需要怎么调整来避免这个问题呢?只需要给闭包函数添加一个参数声明(who)即可。因为传入时,name的值会被复制并且在 go 函数中由 who 指代。后续 name 值改变,不会影响 go 函数里面的值。

package main
import (
	"fmt"
	"time"
)
func main() {
	names := []string{"rob", "marry", "bob", "alice"}
	for _, name := range names {
		go func(who string) {
			fmt.Printf("name: %v\n", who)
		}(name)
	}
	time.Sleep(time.Millisecond)
}

4.2.2 主 goroutine 的运作

封装 main 函数的 goroutine 称为主 goroutine,由 runtime.m0 负责运行。主 goroutine 的工作内容如下:

  • 设定每一个 goroutine 所能申请的栈空间的最大尺寸(32位250MB,64位1GB),如果后续有某个 goroutine 超过了,就会出现一个栈溢出的运行时恐慌,随机终止 Go 程序的运行。
  • 随后,主 goroutine 会在当前 M 地 g0 上执行系统监测任务(为调度器查漏补缺)
  • 接着,进行一系列的初始化工作
    • 检查当前M 是否是 runtime.m0
    • 创建一个特殊的 defer 语句,用于主 goroutine 退出时,做必要的善后处理
    • 启用专用于后台清扫内存垃圾的 goroutine,并设置 GC 可用的标识
    • 执行 main 包中的 init 函数
  • 执行 main 函数,执行之后,检查主 goroutine 是否引发了运行时恐慌,是的话进行必要处理并结束自己以及当前进程的运行

在main 函数执行期间,运行时系统会根据 Go 程序中的 go 语句,复用或新建 goroutine 来封装 go 函数。这些 goroutine 都会放入相应 P 的可运行 G 队列中,然后等待调度器的调度。这里的等待时机虽然很短,但是可能会永远失去运行时机。

  • 20
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jiangw557

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值