Go循环内多协程引用循环变量问题

在golang的循环里面,多协程并发引用循环变量!

package main 
import (
 "fmt"
 "time"
)
 
func Process1(tasks []string) {
  for _, task := range tasks {
  // 启动协程并发处理任务
  go func() {
 	fmt.Printf("Worker start process task: %s\n", task)
  }()
 }
}
 
func Process2(tasks []string) {
  for _, task := range tasks {
  // 启动协程并发处理任务
  go func(t string) {
 	fmt.Printf("Worker start process task: %s\n", t)
  }(task)
 }
}

func main() {
 tasks := []string{"1", "2", "3", "4", "5"}
 Process2(tasks)
 time.Sleep(2 * time.Second)
}

结果:
第一次运行:
Worker start process task: 5
Worker start process task: 4
Worker start process task: 2
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
Worker start process task: 1
Worker start process task: 3

如果把main函数调用的precess2改成precess1:结果:
第一次运行
Worker start process task: 3
Worker start process task: 4
Worker start process task: 4
Worker start process task: 5
Worker start process task: 5
第二次运行
Worker start process task: 2
Worker start process task: 5
Worker start process task: 5
Worker start process task: 5
Worker start process task: 5

上述问题,有个共同点就是都引用了循环变量。即在for index, value := range xxx语句中,

index和value便是循环变量。不同点是循环变量的使用方式,有的是直接在协程中引用precess1,有的作为参数传递precess2。

显然precess1是不安全的。

for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数接收到的参数始终是同一个变量,在 goroutine 开始执行时都会得到同一个迭代值

循环变量是易变的

首先,循环变量实际上只是一个普通的变量。

语句for index, value := range xxx中,每次循环index和value都会被重新赋值(并非生成新的变量)。

如果循环体中会启动协程(并且协程会使用循环变量),就需要格外注意了,因为很可能循环结束后协程才开始执行,

此时,所有协程使用的循环变量有可能已被改写。(是否会改写取决于引用循环变量的方式)

循环变量需要绑定

在precess1中,协程函数体中引用了循环变量task,协程从被创建到被调度执行期间循环变量极有可能被改写,所以会出现两次结果相差较大,比如第一个协程启动for range变量正好循环到3,for属于主协程的一部分。go func是子协程,主子分开看。这种情况下,其实for range里面的循环变量没有跟子协程绑定,称之为变量没有绑定。所以,打印结果是混乱的。很有可能(随机)所有协程执行的task都是列表中的最后一个task,也可能不是。

在precess2中,协程函数体中并没有直接引用循环变量task,而是使用的参数与协程进行了绑定。而在创建协程时,循环变量task

作为函数参数传递给了协程。参数传递的过程实际上也生成了新的变量,也即间接完成了绑定。

所以,precess2实际上是没有问题的。就是实际参数顺序是按照for range产生的变量顺序绑定给子协程的。

还有另一种代替precess2的方式就是给临时变量赋值循环变量类似于这样:

for i := 0; i < 10; i++ {
    i0 := i
    go func() {
        fmt.Println(i0)
    } ()
}

这个写法其实与前面原理是一样的,即变量i已经有了一个副本,协程中针对副本处理

具体参考:https://studygolang.com/articles/21721

例子2;

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

// 错误示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 three three three 
}


// 正确示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        v := v
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

// 正确示例
func main() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {    // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

ps:
简单点来说

如果循环体没有并发出现,则引用循环变量一般不会出现问题;

如果循环体有并发,则根据引用循环变量的位置不同而有所区别

通过参数完成绑定,则一般没有问题;

函数体中引用,则需要显式地绑定

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值