浅谈golang for 循环中使用协程的问题

两个例子

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 main() {
 tasks := []string{"1", "2", "3", "4", "5"}
 Process1(tasks)
 time.Sleep(2 * time.Second)
}

结果:

第一次运行

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
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

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

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

循环变量是易变的

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

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

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

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

循环变量需要绑定

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

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

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

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

ps:

简单点来说

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

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

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

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

补充:Go语言的协程中,写死循环的注意点:

现象:

在写Go的多协程程序时,出现过几次无法理解的情况。

有一次,我想写一个能跑满cpu的程序,最容易想到的就是,开几个Go的协程,每个协程里写死循环。没想到,运行的时候发现,协程就只开出了一个。

另一次,我写了个程序,也是开了多个协程。因为如果不阻塞住主函数,主函数一结束,程序就会结束。所以我就在主函数结束前加了个死循环。然后就发现整个协程都被卡住了。

分析:

其实,这个东西是协程的特点。以前没用过协程,加上Go又说可以当线程用。所以想当然的写了死循环。

准确的说,是在Go语言里,写了死循环,并且死循环内并没有什么系统调用,只有简单的计算这类的。你就会发现,Go的协程调度就废掉了。

协程并非像线程那样,是由CPU中断来触发切换的。它不是应用程序能控制的(操作系统内核的某些关键操作会被保护,不被中断)。即使你在线程里写了死循环,只要周期一到,CPU产生终端,死循环会被打断,重新调度。但是,协程就不是这样了,协程的调度其实是在协程调用了某个系统调用时,自动跳到另一个协程执行。也就是这个“中断”是程序主动产生的,而不是被”中断”。

所以,协程中,如果你写了死循环,那你的死循环就会一直跑着,而不会让别的协程运行。主函数中也是一样,而且主函数中执行这个会让整个协程卡住,因为调度的代码没法被执行。

在Go语言中,如果你想写死循环,循环里面没有系统调用,又想让Go的协程能起作用,只需要在死循环里面加一条语句即可。估计系统调用时也是这个语句起的作用。

runtime.Gosched() //主动让出时间片

还可以使用

select{}

来实现无限阻塞,而不是使用for{}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • Golang实现for循环运行超时后自动退出的方法

    前言 for循环是用来遍历数组或数字的.用for循环遍历字符串时,也有 byte 和 rune 两种方式.第一种为byte,第二种rune.下面话不多说了,来一起看看详细的介绍吧. Golang实现for循环 package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) } 跟C语言中一样,可以让前置.后置语句为空. package ma

  • golang中for循环遍历channel时需要注意的问题详解

    前言 for循环是Go语言唯一的循环结构,最近在做一个基于RabbitMQ的应用,由于官方的qos没有golang的版本,所以出了一点问题. 问题代码如下: _, ch, err := component.NewRabbitMQ() if err != nil { panic(err) } if err := ch.Qos(10, 0, true); err != nil { panic(err) } msgs, err := ch.Consume("push", "&quo

  • Golang 使用gorm添加数据库排他锁,for update

    适用于先读后更新的数据竞争场景,且应该将加锁操作放到事务中,防止锁被自动释放,原因参考mysql doc func UpdateUser(db *gorm.DB, id int64) error { tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() if err := tx.Error; err != nil { return err } user := User{} // 锁住指定

  • Golang常见错误之值拷贝和for循环中的单一变量详解

    前言 golang(中文名:go语言)是谷歌2009发布的第二款开源编程语言.Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全.支持并行进程..如果你想知道得更多,请移步至官网golang官网 在 Go 中函数的调用是值拷贝 copy value,而且在 for 循环中 v 的变量始终是一个变量.如果 v 是 pointer,print 这个 method 接收的是指针的拷贝,for 循环体中每次迭代 v 的 pointer va

  • golang 跳出for循环操作

    执行以下代码,发现无法跳出for循环: func SelectTest() { i := 0 for { select { case <-time.After(time.Second * time.Duration(2)): i++ if i == 5 { fmt.Println("跳出for循环") } } fmt.Println("for循环内 i=", i) } fmt.Println("for循环外") } 解决办法有两个: 1.使

  • 浅谈golang for 循环中使用协程的问题

    两个例子 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 main() { tasks := []string{&quo

  • 浅谈JS for循环中使用break和continue的区别

    1.For循环 格式: for( 初始语句 ; 执行条件 ; 增量 ){ 循环体 } 执行顺序:1.初始语句 2.执行条件是否符合 3.循环体 4.增加增量 初始化语句只在循环开始前执行一次,每次执行循环体时要先判断是否符合条件,如果循环条件为true,则执行循环体,再执行迭代语句. 所以对于for循环,循环条件总比循环体多执行一次. 注意:for循环的循环体和迭代语句不在一起(while和do-while是在一起的)所以如果使用continue来结束本次循环,迭代语句还有继续运行,而while

  • 浅谈Golang是如何读取文件内容的(7种)

    本文旨在快速介绍Go标准库中读取文件的许多选项. 在Go中(就此而言,大多数底层语言和某些动态语言(如Node))返回字节流. 不将所有内容自动转换为字符串的好处是,其中之一是避免昂贵的字符串分配,这会增加GC压力. 为了使本文更加简单,我将使用string(arrayOfBytes)将bytes数组转换为字符串. 但是,在发布生产代码时,不应将其作为一般建议. 1.读取整个文件到内存中 首先,标准库提供了多种功能和实用程序来读取文件数据.我们将从os软件包中提供的基本情况开始.这意味着两个先决

  • 浅谈golang 的高效编码细节

    目录 struct 和 map 用谁呢? 字符串如何拼接是好? 用 + 的方式 使用 fmt.Sprintf() 的方式 使用 strings.Join 的方式 使用 buffer 的方式 xdm,我们都知道 golang 是天生的高并发,高效的编译型语言 可我们也都可知道,工具再好,用法不对,全都白费,我们来举 2 个常用路径来感受一下 struct 和 map 用谁呢? 计算量很小的时候,可能看不出使用 临时 struct 和 map 的耗时差距,但是数量起来了,差距就明显了,且会随着数量越

  • 浅谈Golang内存逃逸

    目录 1.什么是内存逃逸 2.什么是逃逸分析 3.小结 4.逃逸分析案例 1.函数返回局部指针变量 2.interface类型逃逸 1.interface产生逃逸 2.指向栈对象的指针不能在堆中 3.闭包产生逃逸 4. 变量大小不确定及栈空间不足引发逃逸 5.总结 1.什么是内存逃逸 在一段程序中,每一个函数都会有自己的内存区域分配自己的局部变量,返回值,这些内存会由编译器在栈中进行分配,每一个函数会分配一个栈帧,在函数运行结束后销毁,但是有些变量我们想在函数运行结束后仍然使用,就需要把这个变量

  • 浅谈Golang数据竞态

    目录 一个数据竞态的case 检查数据竞态 解决方案 1.WaitGroup等待 2.Channel阻塞等待 3.Channel通道 4.互斥锁 典型数据竞态 1.循环计数上的竞态 2.意外共享变量 3.无保护的全局变量 4.原始无保护变量 5.未同步的发送和关闭操作 本文以一个简单事例的多种解决方案作为引子,用结构体Demo来总结各种并发读写的情况 一个数据竞态的case package main import ( "fmt" "testing" "ti

  • 浅谈python for循环的巧妙运用(迭代、列表生成式)

    介绍 我们可以通过for循环来迭代list.tuple.dict.set.字符串,dict比较特殊dict的存储不是连续的,所以迭代(遍历)出来的值的顺序也会发生变化. 迭代(遍历) #!/usr/bin/env python3 #-*- coding:utf-8 -*- vlist=['a','b','c'] vtuple=('a','b','c') vdict={'a': 1, 'b': 2, 'c': 3} vset={'a','b','c'} vstr='abc' for x in vl

  • 浅谈django开发者模式中的autoreload是如何实现的

    在开发django应用的过程中,使用开发者模式启动服务是特别方便的一件事,只需要 python manage.py runserver 就可以运行服务,并且提供了非常人性化的autoreload机制,不需要手动重启程序就可以修改代码并看到反馈.刚接触的时候觉得这个功能比较人性化,也没觉得是什么特别高大上的技术.后来有空就想着如果是我来实现这个autoreload会怎么做,想了很久没想明白,总有些地方理不清楚,看来第一反应真是眼高手低了.于是就专门花了一些时间研究了django是怎样实现autor

  • 浅谈头文件algorithm中的常用函数

    一.非修改性序列操作(12个) 循环         对序列中的每个元素执行某操作         for_each() 查找         在序列中找出某个值的第一次出现的位置         find() 在序列中找出符合某谓词的第一个元素     find_if() 在序列中找出一子序列的最后一次出现的位置         find_end() 在序列中找出第一次出现指定值集中之值的位置     find_first_of() 在序列中找出相邻的一对值         adjacent_

  • 浅谈js for循环输出i为同一值的问题

    1.最近开发中遇到一个问题,为什么每次输出都是5,而不是点击每个p,就alert出对应的1,2,3,4,5. 代码如下: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>闭包演示</title> </head> <body> <p>1<

随机推荐