尝试理解 goroutines(Go 语言中的协程)

huangapple go评论79阅读模式
英文:

Trying to understand goroutines

问题

我一直在尝试修改A Tour of Go中的以下代码,但是我不明白在应用一些小的改动时发生了什么。原始代码如下:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

它产生的输出是:

world
hello
hello
world
world
hello
hello
world
world
hello

这是正常的:五次hello,五次world。当我调用以下代码时情况开始变得奇怪:

say("world")
go say("hello")

现在的输出只有:

world
world
world
world
world

没有任何hello。如果我使用两个goroutine:

go say("world")
go say("hello")

现在根本没有输出。当我将i < 5改为i < 2并调用:

go say("world")
say("hello")

我得到的输出是:

world
hello
hello

我在这里漏掉了什么?

英文:

I've been playing around with the following code from A Tour of Go, but I don't understand what is going on when I apply some minor changes. The original code is this

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func say(s string) {
    for i := 0; i &lt; 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say(&quot;world&quot;)
    say(&quot;hello&quot;)
}

and it produces this

world
hello
hello
world
world
hello
hello
world
world
hello

which is OK: five times hello, five times world. I starts to get strange when I call

say(&quot;world&quot;)
go say(&quot;hello&quot;)

Now the output is just

world
world
world
world
world

No hello whatsoever. It gets even weirder with two goroutines

go say(&quot;world&quot;)
go say(&quot;hello&quot;)

Now there is no output at all. When I change i &lt; 5 to i &lt; 2 and call

go say(&quot;world&quot;)
say(&quot;hello&quot;)

I get

world
hello
hello

What am I missing here?

答案1

得分: 5

在以下情况下:

 say("world")
 go say("hello")

在启动"go say("hello")"的goroutine之前,"world"的调用必须完成。"hello"的goroutine不会运行或完成因为main函数返回

对于:

go say("world")
go say("hello")

这些goroutine不会运行或完成,因为main函数返回。

使用sync.WaitGroup来防止main函数在goroutine完成之前退出:

func say(wg *sync.WaitGroup, s string) {
  defer wg.Done()
  for i := 0; i < 5; i++ {
	time.Sleep(100 * time.Millisecond)
	fmt.Println(s)
  }
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go say(&wg, "world")
  go say(&wg, "hello")
  wg.Wait()
}

playground示例

英文:

In the case of

 say(&quot;world&quot;)
 go say(&quot;hello&quot;)

The "world" call must complete before the "hello" goroutine is started. The "hello" goroutine does not run or complete because main returns.

For

go say(&quot;world&quot;)
go say(&quot;hello&quot;)

the goroutines do not run or complete because main returns.

Use sync.WaitGroup to prevent main from exiting before the goroutines complete:

func say(wg *sync.WaitGroup, s string) {
  defer wg.Done()
  for i := 0; i &lt; 5; i++ {
	time.Sleep(100 * time.Millisecond)
	fmt.Println(s)
  }
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go say(&amp;wg, &quot;world&quot;)
  go say(&amp;wg, &quot;hello&quot;)
  wg.Wait()
}

playground example

答案2

得分: 3

恭喜你学会了Go语言。作为一个新手,了解并发性以及它与并行性的区别是很好的。

并发性
并发性就像一个杂耍者用一只手同时抛起多个球。无论他同时抛起多少个球,每次只有一个球触碰到他的手。

并行性
当杂耍者开始用另一只手同时抛起更多的球时,我们就有了两个并发的进程同时运行。

Goroutines非常棒,因为它们既是并发的,又是自动并行的,这取决于可用的计算核心和设置的GOMAXPROCS变量。

单手杂耍者
回到单手、单核心、并发的杂耍者。想象一下他用手作为main例程,分别抛起三个球,分别命名为"hello"、"world"和"mars"。

var balls = []string{"hello", "world", "mars"}

func main() {
        go say(balls[0])
        go say(balls[1])
        go say(balls[2])
}

或者更合适的是,

func main() {
        for _, ball := range balls {
                go say(ball)
        }
}

一旦这三个球按顺序被抛起,杂耍者立即收回手。也就是说,在第一个球着陆之前,main例程就退出了。可惜,球们只能掉到地上。表演不好。

为了让球重新回到他的手中,杂耍者必须确保他等待它们。这意味着他的手需要能够跟踪和计数他抛出的球,并学会每个球何时着陆。

最直接的方法是使用sync.WaitGroup

import (
	"fmt"
	"time"
	"sync"
)

var balls = []string{"hello", "world", "mars"}
var wg sync.WaitGroup

func main() {
        for _, ball := range balls {
                // 抛出一个球
                wg.Add(1)
                go func(b string) {
                        // 信号组已完成该例程
                        defer wg.Done()
                        // 每个球悬停1秒钟
                        time.Sleep(time.Duration(1) * time.Second)
                        fmt.Println(b)
                        // 在goroutine退出之前调用wg.Done()
                }(ball)
        }

        // 球在空中时,杂耍者可以做任何他想做的事情。

        // 这只手将在1秒后回来接住球。
        wg.Wait()
}

WaitGroup很简单。当一个goroutine被创建时,使用WaitGroup.Add(1)增加一个"待处理计数器",并调用WaitGroup.Done()来减少计数器。一旦计数器变为0,就意味着所有的goroutine都已完成,WaitGroup应该停止等待(并接住球!)。

虽然使用通道进行同步是可以的,但鼓励在适当的时候使用可用的并发工具,特别是当使用通道使代码变得更复杂和难以理解时。

英文:

Congratulations for learning Go. As someone new, it is nice to understand concurrency and how it is different from parallelism.

Concurrency
Concurrency is like a juggler juggling several balls in the air with one hand. No matter how many balls he is juggling, only one ball touch his hand at any moment.

Parallelism
When the juggler starts juggling more balls with another hand in parallel, we have two concurrent processes running at the same time.

Goroutines are great because they're both concurrent and auto-parallel, depending on the computing cores available and the GOMAXPROCS variable being set.

The One-handed Juggler
Back to the one-handed, single-cored, concurrent juggler. Imagine him juggling three balls named "hello", "world", and "mars" respectively with the hand being the main routine.

var balls = []string{&quot;hello&quot;, &quot;world&quot;, &quot;mars&quot;}

func main() {
        go say(balls[0])
        go say(balls[1])
        go say(balls[2])
}

Or more appropriately,

func main() {
        for _, ball := range balls {
                go say(ball)
        }
}

Once the three balls are thrown up into the air sequentially, the juggler simply retreats his hand right away. That is, the main routine exits before the first ball thrown can even land on his hand. Shame, the balls just drop to the ground. Bad show.

In order to get the balls back in his hand, the juggler has to make sure he waits for them. This means his hand needs to be able to keep track of and count the balls he threw and learn when each is landing.

The most straightforward way is to use sync.WaitGroup:

import (
	&quot;fmt&quot;
	&quot;time&quot;
	&quot;sync&quot;
)

var balls = []string{&quot;hello&quot;, &quot;world&quot;, &quot;mars&quot;}
var wg sync.WaitGroup

func main() {
        for _, ball := range balls {
                // One ball thrown
                wg.Add(1)
                go func(b string) {
                        // Signals the group that this routine is done.
                        defer wg.Done()
                        // each ball hovers for 1 second
                        time.Sleep(time.Duration(1) * time.Second)
                        fmt.Println(b)
                        // wg.Done() is called before goroutine exits
                }(ball)
        }

        // The juggler can do whatever he pleases while the 
        // balls are hanging in the air.

        // This hand will come back to grab the balls after 1s.
        wg.Wait()
}

WaitGroup is simple. When a goroutine is spawned, one adds to a "backlog counter" with WaitGroup.Add(1) and call WaitGroup.Done() to decrease the counter. Once the backlog becomes 0, it means that all goroutines are done and WaitGroup should stop waiting (and grab the balls!).

While using channel(s) for synchronization is fine, it is encouraged to use available concurrent tools as appropriate especially when the use of channels make the code more complex and hard to comprehend.

答案3

得分: 1

这是因为主函数已经退出。

当主函数返回时,所有的goroutine都会被突然终止,然后程序退出。

你可以添加以下语句:

time.Sleep(100 * time.Second)

在主函数返回之前,这样一切都会正常运行。

但在Go语言中,一个好的实践是使用通道(channel),它用于在goroutine之间进行通信。你可以使用通道让主函数等待后台goroutine完成。

英文:

It is because the main function has been exited.

When main function return, all goroutines are abruptly terminated, then program exits.

You add a statment:

time.Sleep(100 * time.Second)

before main function return, and everything goes well.

But a good practice in Go is to use channel, which is used to communicate between goroutines. You can use it to let main function wait for background goroutines to finish.

答案4

得分: 0

通过以下代码:

func main() {
    go say("world")
    say("hello")
}

你创建了两个独立的goroutine,一个是主函数的goroutine,另一个是go say("world")。通常情况下,当函数被执行时,程序会跳转到该函数,执行函数内的所有代码,然后跳转到调用该函数的那一行之后的代码。

但是,使用goroutine时,你不是跳转到函数内部,而是在一个单独的线程中启动goroutine,并继续执行调用之后的代码,而不等待它。

因此,在主goroutine完成之前,goroutine没有足够的时间来完成执行。

英文:

With

func main() {
go say(&quot;world&quot;)
say(&quot;hello&quot;)
}

You are creating two separate goroutines, one is the main functions goroutine and one is the go say("world"). Normally when functions are executed the programs jumps to that function, execute all code inside and then jumps to the line after where the function was called from.

With goroutines you are not jumping inside the function but you are starting the goroutine in a separate thread and continuing to execute the line just after the call without waiting for it.

Therefore the goroutine will not have time to finish before the main goroutine is done.

huangapple
  • 本文由 发表于 2016年4月17日 22:11:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/36677614.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定