首先上代码
题目一:
package main import ( "fmt" "time" ) func Process1(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func() { fmt.Printf("Worker start process task: %s ", task) }() } } func main() { tasks := []string{"1", "2", "3", "4", "5"} Process1(tasks) time.Sleep(2 * time.Second) } #连续运行两次,输出结构不一致: kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task: 5 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5 kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task: 4 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5
再运行下面代码
题目二:
package main import ( "fmt" "time" ) func Process1(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func() { fmt.Printf("Worker start process task: %s ", task) }() } } func Process2(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func(t string) { fmt.Printf("Worker start process task: %s ", t) }(task) } } func main() { tasks := []string{"1", "2", "3", "4", "5"} Process2(tasks) time.Sleep(2 * time.Second) } #除顺序不一致,运行结果可以达到预期: kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task: 2 Worker start process task: 1 Worker start process task: 5 Worker start process task: 4 Worker start process task: 3 kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task: 1 Worker start process task: 3 Worker start process task: 4 Worker start process task: 5 Worker start process task: 2
题目三:
package main import ( "fmt" "time" ) func Process1(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func() { fmt.Printf("Worker start process task: %s ", task) }() } } func Process2(tasks []string) { for _, task := range tasks { // 启动协程并发处理任务 go func(t string) { fmt.Printf("Worker start process task: %s ", t) }(task) } } func Process3(tasks []string){ for _,task := range tasks { tasknew := task go func() { fmt.Printf("Worker start process task:%s ", tasknew) }() } } func main() { tasks := []string{"1", "2", "3", "4", "5"} Process3(tasks) time.Sleep(2 * time.Second) } #运行输出如下,结果也能达到预期: kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task:5 Worker start process task:2 Worker start process task:4 Worker start process task:1 Worker start process task:3 kun@kundeMacBook-Pro study % go run goroutine/goroutine4.go Worker start process task:3 Worker start process task:1 Worker start process task:2 Worker start process task:5 Worker start process task:4
以下分析摘自网上资料:
原理剖析
上述问题,有个共同点就是都引用了循环变量。即在for index, value := range xxx
语句中,index
和value
便是循环变量。不同点是循环变量的使用方式,有的是直接在协程中引用(题目一),有的作为参数传递(题目二)。
回答以上问题,记住以下两点即可。
循环变量是易变的
首先,循环变量实际上只是一个普通的变量。
语句for index, value := range xxx
中,每次循环index和value都会被重新赋值(并非生成新的变量)。
如果循环体中会启动协程(并且协程会使用循环变量),就需要格外注意了,因为很可能循环结束后协程才开始执行,
此时,所有协程使用的循环变量有可能已被改写。(是否会改写取决于引用循环变量的方式)
循环变量需要绑定
在题目一中,协程函数体中引用了循环变量task
,协程从被创建到被调度执行期间循环变量极有可能被改写,
这种情况下,我们称之为变量没有绑定。
所以,题目一打印结果是混乱的。很有可能(随机)所有协程执行的task
都是列表中的最后一个task。
在题目二中,协程函数体中并没有直接引用循环变量task
,而是使用的参数。而在创建协程时,循环变量task
作为函数参数传递给了协程。参数传递的过程实际上也生成了新的变量,也即间接完成了绑定。
所以,题目二实际上是没有问题的。
在题目三中,通过 tasknew := task
显式地绑定,每次循环会生成一个新的变量
总结
简单点来说
- 如果循环体没有并发出现,则引用循环变量一般不会出现问题;
- 如果循环体有并发,则根据引用循环变量的位置不同而有所区别
- 通过参数完成绑定,则一般没有问题;
- 函数体中引用,则需要显式地绑定