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 () PCDATA $2, $-2
0x00de 00222 () PCDATA $0, $-2
0x00de 00222 () MOVQ 48(SP), BP
0x00e3 00227 () ADDQ $56, SP
0x00e7 00231 () RET
0x00e8 00232 () 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可以简单理解为,值传递,使用时使用传进去的值。