golang内幕之for-go-statement

func ForGoStatement_1() {
	go func() {
		fmt.Println("go-func-1")
	}()
}

func main() {
	ForGoStatement_1()
}

参考如下文章,会更清楚golang的协程:

【golang内幕之程序启动流程】【https://blog.csdn.net/QQ1130141391/article/details/96197570

【golang内幕之协程状态切换】【https://blog.csdn.net/QQ1130141391/article/details/96350019

golang的协程写法很简单:

go func()

func可以是匿名函数,也可以是命名函数。

golang编译器在编译时会将go func()转换程runtime中的newproc(func),这样就新创建了一个golang协程,此时新建的协程处于runnable状态并放置在p的队列中,并没有马上执行,而是等待协程调度器调度执行。

而main.main函数则是在主协程中执行,即main routine。当main.main函数返回时,main routine做一些简单的资源回收后,会调用exit(0)退出整个进程。

所以上面代码会不会输出go-func-1呢?

我只能说,从调度角度说,有可能,但微乎其微。

所以,现在运行很多次,基本都没任何输出,直接退出了进程。

Process finished with exit code 0

原因,新建的routine都还没机会调度,main routine就调用了exit(0)退出了进程。

那我们让main routine睡眠一下,让新建的routine有足够时间给调度:

func ForGoStatement_1() {
	go func() {
		fmt.Println("go-func-1")
	}()
}

func main() {
	ForGoStatement_1()
	time.Sleep(1)
}
go-func-1

Process finished with exit code 0
func ForGoStatement_2() {
	//loop 1
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println("i=", i)
		}()
	}

	//loop 2
	for j := 0; j < 10; j++ {
		go func(v int) {
			fmt.Println("j", v)
		}(j)
	}
}

func main() {
	ForGoStatement_2()
}
"".ForGoStatement_2 STEXT size=242 args=0x0 locals=0x38
	0x0000 00000 (for-go-statement.go:7)	TEXT	"".ForGoStatement_2(SB), ABIInternal, $56-0
    ...
	0x0028 00040 (for-go-statement.go:9)	LEAQ	type.int(SB), AX
	0x002f 00047 (for-go-statement.go:9)	PCDATA	$2, $0
	0x002f 00047 (for-go-statement.go:9)	MOVQ	AX, (SP)
	0x0033 00051 (for-go-statement.go:9)	CALL	runtime.newobject(SB)
	0x0038 00056 (for-go-statement.go:9)	PCDATA	$2, $1
	0x0038 00056 (for-go-statement.go:9)	MOVQ	8(SP), AX
	0x003d 00061 (for-go-statement.go:9)	PCDATA	$0, $1
	0x003d 00061 (for-go-statement.go:9)	MOVQ	AX, "".&i+40(SP)
	0x0042 00066 (for-go-statement.go:9)	PCDATA	$2, $0
	0x0042 00066 (for-go-statement.go:9)	MOVQ	$0, (AX)
	0x0049 00073 (for-go-statement.go:9)	JMP	75
	0x004b 00075 (for-go-statement.go:9)	PCDATA	$2, $1
	0x004b 00075 (for-go-statement.go:9)	MOVQ	"".&i+40(SP), AX
	0x0050 00080 (for-go-statement.go:9)	PCDATA	$2, $0
	0x0050 00080 (for-go-statement.go:9)	CMPQ	(AX), $10
	0x0054 00084 (for-go-statement.go:9)	JLT	88
	0x0056 00086 (for-go-statement.go:9)	JMP	150
	0x0058 00088 (for-go-statement.go:10)	PCDATA	$2, $1
	0x0058 00088 (for-go-statement.go:10)	MOVQ	"".&i+40(SP), AX
	0x005d 00093 (for-go-statement.go:12)	MOVQ	AX, ""..autotmp_5+32(SP)
	0x0062 00098 (for-go-statement.go:10)	MOVL	$8, (SP)
	0x0069 00105 (for-go-statement.go:10)	PCDATA	$2, $2
	0x0069 00105 (for-go-statement.go:10)	LEAQ	"".ForGoStatement_2.func1·f(SB), CX
	0x0070 00112 (for-go-statement.go:10)	PCDATA	$2, $1
	0x0070 00112 (for-go-statement.go:10)	MOVQ	CX, 8(SP)
	0x0075 00117 (for-go-statement.go:10)	PCDATA	$2, $0
	0x0075 00117 (for-go-statement.go:10)	MOVQ	AX, 16(SP)
	0x007a 00122 (for-go-statement.go:10)	CALL	runtime.newproc(SB)
	0x007f 00127 (for-go-statement.go:10)	JMP	129
	0x0081 00129 (for-go-statement.go:9)	PCDATA	$2, $1
	0x0081 00129 (for-go-statement.go:9)	MOVQ	"".&i+40(SP), AX
	0x0086 00134 (for-go-statement.go:9)	PCDATA	$2, $0
	0x0086 00134 (for-go-statement.go:9)	MOVQ	(AX), AX
	0x0089 00137 (for-go-statement.go:9)	PCDATA	$2, $3
	0x0089 00137 (for-go-statement.go:9)	MOVQ	"".&i+40(SP), CX
	0x008e 00142 (for-go-statement.go:9)	INCQ	AX
	0x0091 00145 (for-go-statement.go:9)	PCDATA	$2, $0
	0x0091 00145 (for-go-statement.go:9)	MOVQ	AX, (CX)
	0x0094 00148 (for-go-statement.go:9)	JMP	75
	0x0096 00150 (for-go-statement.go:16)	PCDATA	$0, $0
	0x0096 00150 (for-go-statement.go:16)	MOVQ	$0, "".j+24(SP)
	0x009f 00159 (for-go-statement.go:16)	JMP	161
	0x00a1 00161 (for-go-statement.go:16)	CMPQ	"".j+24(SP), $10
	0x00a7 00167 (for-go-statement.go:16)	JLT	171
	0x00a9 00169 (for-go-statement.go:16)	JMP	222
	0x00ab 00171 (for-go-statement.go:17)	MOVL	$8, (SP)
	0x00b2 00178 (for-go-statement.go:17)	PCDATA	$2, $1
	0x00b2 00178 (for-go-statement.go:17)	LEAQ	"".ForGoStatement_2.func2·f(SB), AX
	0x00b9 00185 (for-go-statement.go:17)	PCDATA	$2, $0
	0x00b9 00185 (for-go-statement.go:17)	MOVQ	AX, 8(SP)
	0x00be 00190 (for-go-statement.go:17)	MOVQ	"".j+24(SP), CX
	0x00c3 00195 (for-go-statement.go:17)	MOVQ	CX, 16(SP)
	0x00c8 00200 (for-go-statement.go:17)	CALL	runtime.newproc(SB)
	0x00cd 00205 (for-go-statement.go:17)	JMP	207
	0x00cf 00207 (for-go-statement.go:16)	MOVQ	"".j+24(SP), AX
	0x00d4 00212 (for-go-statement.go:16)	INCQ	AX
	0x00d7 00215 (for-go-statement.go:16)	MOVQ	AX, "".j+24(SP)
	0x00dc 00220 (for-go-statement.go:16)	JMP	161
	0x00de 00222 (<unknown line number>)	PCDATA	$2, $-2
	0x00de 00222 (<unknown line number>)	PCDATA	$0, $-2
	0x00de 00222 (<unknown line number>)	MOVQ	48(SP), BP
	0x00e3 00227 (<unknown line number>)	ADDQ	$56, SP
	0x00e7 00231 (<unknown line number>)	RET
	0x00e8 00232 (<unknown line number>)	NOP
	0x00e8 00232 (for-go-statement.go:7)	PCDATA	$0, $-1
	0x00e8 00232 (for-go-statement.go:7)	PCDATA	$2, $-1
	0x00e8 00232 (for-go-statement.go:7)	CALL	runtime.morestack_noctxt(SB)
	0x00ed 00237 (for-go-statement.go:7)	JMP	0
	...

通过汇编,可以看出,loop 1中先创建了一个int类型的变量,然后作为newproc的参数传入,跟defer有点类似,但defer不会new一个object出来,而是直接取函数内变量地址值,而go func不同,有可能调用go func的调用函数已经结束了,但go func还会执行,因此需要开辟一个堆空间用于存放变量值。

而loop 2是直接使用j的值,没有取变量地址获取创建新的堆变量。

上面代码,会有输出吗?

有可能输出,也有可能不会输出。因为有可能main routine先退出了,新创建的routine没机会执行,但因为在main routine调用了多次的for循环,每次循环都会创建一个新routine,这本身就会消耗点时间,所以在main routine之前,已经创建好的routine还是有比较大的机会得到调度的。

上面代码,会输出全部吗?

有可能,但也有可能在main routine退出了进程,导致其他routine没机会执行,所以导致不会全部输出。

i= 3
i= 3
i= 10
i= 10
i= 10

Process finished with exit code 0

Process finished with exit code 0
i= 3
i= 3
i= 10
i= 10
i= 10
j 2
i= 10
i= 10
j 0

Process finished with exit code 0

以上是执行多次的结果,可以说输出是随机的,会不会输出,随机;会不会全部输出,随机(几率比较小);输出内容的顺序,随机。

如果我们想让全部输出后,才退出进程呢?

可以使用time.Sleep让main routine休眠,只要休眠时间足够长,是会输出全部内容的。

上面已经使用过这个套路了,换个新套路:

func ForGoStatement_3() {
	var wg sync.WaitGroup
	wg.Add(10 + 10)

	//loop 1
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println("i=", i)
			wg.Done()
		}()
	}

	//loop 2
	for j := 0; j < 10; j++ {
		go func(v int) {
			fmt.Println("j", v)
			wg.Done()
		}(j)
	}

	wg.Wait()
}

func main() {
	ForGoStatement_3()
}

上面使用了WaitGroup同步原语,上面写法:

总共有10+10=20次机会,在20机会消耗完之前,WaitGroup会通过Wait等待,而通过Done就会消耗一次机会。

所以在20个新创建的routine都执行完且调用Done前,main routine都因为Wait阻塞着。

每个新创建的routine执行时都会输出一段内容,然后执行done,然后routine就执行完毕,即进入_Gdead状态,不会再执行,等待回收分配的资源了。

运行结果:

i= 3
i= 3
i= 10
i= 7
j 1
j 4
i= 8
j 7
j 3
j 9
i= 8
i= 10
j 0
j 6
j 5
i= 8
j 2
i= 6
j 8
i= 7

Process finished with exit code 0

多次运行,都会输出20行内容然后才退出进程,WaitGroup达到了效果。

至于每次输出的20行内容还是随机的,因为20个新创建的routine都在随机执行。

问题:为什么新建的routine为什么会新执行?而不是新创建新执行?

因为我的机器CPU是多核的,在golang协程调度里面,多个核,意味这个创建多个m(系统线程)和多个p(golang对调度资源的一种抽象,每一个p维护一个存放待运行的routine的队列,新创建routine时,golang协程调度器会选择一个空闲的p,)并将新创建的routine放在p的本地队列中。

这意味着,我们新创建了20个routine,这20个routine可能分布在不同的p的队列中,也就有可能在不同的m(线程)中执行。

那如果只有一个m和一个p的情况,会怎么样呢?

func ForGoStatement_3() {
	runtime.GOMAXPROCS(1)
	
	var wg sync.WaitGroup
	wg.Add(10 + 10)

	//loop 1
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println("i=", i)
			wg.Done()
		}()
	}

	//loop 2
	for j := 0; j < 10; j++ {
		go func(v int) {
			fmt.Println("j", v)
			wg.Done()
		}(j)
	}

	wg.Wait()
}

func main() {
	ForGoStatement_3()
}

通过runtime.GOMAXPROCS(1)设置了只有一个p,即只有一个routine队列,其实还有一个全局队列。

j 9
i= 10
i= 10
i= 10
i= 10
i= 10
i= 10
i= 10
i= 10
i= 10
i= 10
j 0
j 1
j 2
j 3
j 4
j 5
j 6
j 7
j 8

Process finished with exit code 0

此时,多次运行,结果不是随机的。

事实上,我们不应依赖p数量和routine执行,我们开发过程中,应该认为每个routine的执行顺序的是随机的,不可预知的。

如果我们希望按照某种顺序执行,则应该使用chan或者锁来解决并发问题。

总结:p数量为1和>1的两种情况,在不考虑顺序问题(不应该也很难控制routine并发顺序),我们发现loop 1输出的值可以认为是随机的,而loop 2是按照我们传入的参数值。

loop 1可以简单理解为,引用传递,使用时使用引用指向的值。

loop 2可以简单理解为,值传递,使用时使用传进去的值。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值