为什么我的goroutine在完成任务后会等待彼此,而不是立即结束?

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

Why do my goroutines wait for each other instead of finishing when done?

问题

我对Go语言还不太熟悉,我的代码中有一部分我不太理解。
我写了一个简单的冒泡排序算法(我知道它不是很高效)。
现在我想要启动3个Go协程。每个协程应该独立地对它的数组进行排序。当排序完成后,函数应该打印一个“done”消息。

这是我的代码:

  1. package main
  2. import (
  3. "fmt"
  4. "time" //用于时间函数,例如Now()
  5. "math/rand" //用于伪随机数
  6. )
  7. /* 简单的冒泡排序算法 */
  8. func bubblesort(str string, a []int) []int {
  9. for n:=len(a); n>1; n-- {
  10. for i:=0; i<n-1; i++ {
  11. if a[i] > a[i+1] {
  12. a[i], a[i+1] = a[i+1], a[i] //交换
  13. }
  14. }
  15. }
  16. fmt.Println(str+" done") //完成消息
  17. return a
  18. }
  19. /* 用伪随机数填充切片 */
  20. func random_fill(a []int) []int {
  21. for i:=0; i<len(a); i++ {
  22. a[i] = rand.Int()
  23. }
  24. return a
  25. }
  26. func main() {
  27. rand.Seed( time.Now().UTC().UnixNano()) //设置随机数种子
  28. a1 := make([]int, 34589) //创建切片
  29. a2 := make([]int, 42) //创建切片
  30. a3 := make([]int, 9999) //创建切片
  31. a1 = random_fill(a1) //填充切片
  32. a2 = random_fill(a2) //填充切片
  33. a3 = random_fill(a3) //填充切片
  34. fmt.Println("Slices filled ...")
  35. go bubblesort("Thread 1", a1) //启动第一个协程
  36. go bubblesort("Thread 2", a2) //启动第二个协程
  37. go bubblesort("Thread 3", a3) //启动第三个协程
  38. fmt.Println("Main working ...")
  39. time.Sleep(1*60*1e9) //等待1分钟以获取“done”消息
  40. }

这是我得到的结果:

  1. Slices filled ...
  2. Main working ...
  3. Thread 1 done
  4. Thread 2 done
  5. Thread 3 done

Thread 2应该先完成,因为它的切片最小,为什么没有呢?
似乎所有的协程都在等待其他协程完成,因为“done”消息同时出现,无论切片的大小如何。

我的问题出在哪里呢?=)

提前感谢。

*编辑:
当在冒泡排序函数的for循环中加入“time.Sleep(1)”时,似乎可以工作。但是我想用这段代码在不同的机器上计时(我知道,我必须改变随机数),所以sleep会使结果失真。

英文:

I'm pretty new to Go and there is one thing in my code which I don't understand.
I wrote a simple bubblesort algorithm (I know it's not really efficient ;)).
Now I want to start 3 GoRoutines. Each thread should sort his array independent from the other ones. When finished, the func. should print a "done"-Message.

Here is my Code:

  1. package main
  2. import (
  3. &quot;fmt&quot;
  4. &quot;time&quot; //for time functions e.g. Now()
  5. &quot;math/rand&quot; //for pseudo random numbers
  6. )
  7. /* Simple bubblesort algorithm*/
  8. func bubblesort(str string, a []int) []int {
  9. for n:=len(a); n&gt;1; n-- {
  10. for i:=0; i&lt;n-1; i++ {
  11. if a[i] &gt; a[i+1] {
  12. a[i], a[i+1] = a[i+1], a[i] //swap
  13. }
  14. }
  15. }
  16. fmt.Println(str+&quot; done&quot;) //done message
  17. return a
  18. }
  19. /*fill slice with pseudo numbers*/
  20. func random_fill(a []int) []int {
  21. for i:=0; i&lt;len(a); i++ {
  22. a[i] = rand.Int()
  23. }
  24. return a
  25. }
  26. func main() {
  27. rand.Seed( time.Now().UTC().UnixNano()) //set seed for rand.
  28. a1 := make([]int, 34589) //create slice
  29. a2 := make([]int, 42) //create slice
  30. a3 := make([]int, 9999) //create slice
  31. a1 = random_fill(a1) //fill slice
  32. a2 = random_fill(a2) //fill slice
  33. a3 = random_fill(a3) //fill slice
  34. fmt.Println(&quot;Slices filled ...&quot;)
  35. go bubblesort(&quot;Thread 1&quot;, a1) //1. Routine Start
  36. go bubblesort(&quot;Thread 2&quot;, a2) //2. Routine Start
  37. go bubblesort(&quot;Thread 3&quot;, a3) //3. Routine Start
  38. fmt.Println(&quot;Main working ...&quot;)
  39. time.Sleep(1*60*1e9) //Wait 1 minute for the &quot;done&quot; messages
  40. }

This is what I get:

  1. Slices filled ...
  2. Main working ...
  3. Thread 1 done
  4. Thread 2 done
  5. Thread 3 done

Should'nt Thread 2 finish first, since his slice is the smallest?
It seems that all the threads are waiting for the others to finish, because the "done"-messages appear at the same time, no matter how big the slices are..

Where is my brainbug? =)

Thanks in advance.

*Edit:
When putting "time.Sleep(1)" in the for-loop in the bubblesort func. it seems to work.. but I want to clock the duration on different machines with this code (I know, i have to change the random thing), so sleep would falsify the results.

答案1

得分: 9

确实,关于goroutine执行顺序没有任何保证。

但是,如果你通过显式地让两个处理器核心运行来强制进行真正的并行处理:

  1. import (
  2. "fmt"
  3. "time" //用于时间函数,例如Now()
  4. "math/rand" //用于伪随机数
  5. "runtime"
  6. )
  7. ...
  8. func main() {
  9. runtime.GOMAXPROCS(2)
  10. rand.Seed( time.Now().UTC().UnixNano()) //为rand设置种子
  11. ...

那么你将会得到预期的结果:

  1. Slices filled ...
  2. Main working ...
  3. Thread 2 done
  4. Thread 3 done
  5. Thread 1 done

最好的问候

英文:

Indeed, there is no garantee regarding the order in which your goroutines will be executed.

However if you force the true parallel processing by explicitly letting 2 processor cores run :

  1. import (
  2. &quot;fmt&quot;
  3. &quot;time&quot; //for time functions e.g. Now()
  4. &quot;math/rand&quot; //for pseudo random numbers
  5. &quot;runtime&quot;
  6. )
  7. ...
  8. func main() {
  9. runtime.GOMAXPROCS(2)
  10. rand.Seed( time.Now().UTC().UnixNano()) //set seed for rand.
  11. ...

Then you will get the expected result :

  1. Slices filled ...
  2. Main working ...
  3. Thread 2 done
  4. Thread 3 done
  5. Thread 1 done

Best regards

答案2

得分: 3

重要的是能够在整个潜在长时间运行的工作负载完成之前将处理器“让出”给其他进程。这在单核上下文或多核上下文中同样适用(因为并发不等于并行)。

这正是runtime.Gosched()函数所做的:

Gosched让出处理器,允许其他goroutine运行。它不会挂起当前的goroutine,因此执行会自动恢复。

请注意,“上下文切换”不是免费的:每次都会花费一点时间。

  • 在我的机器上,如果不让出处理器,您的程序运行时间为5.1秒。
  • 如果在外部循环(for n:=len(a); n>1; n--)中让出处理器,运行时间为5.2秒:开销很小。
  • 如果在内部循环(for i:=0; i<n-1; i++)中让出处理器,运行时间为61.7秒:开销巨大!!

这是修改后的程序,正确地让出处理器,开销很小:

  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "runtime"
  6. "time"
  7. )
  8. /* 简单的冒泡排序算法 */
  9. func bubblesort(str string, a []int, ch chan []int) {
  10. for n := len(a); n > 1; n-- {
  11. for i := 0; i < n-1; i++ {
  12. if a[i] > a[i+1] {
  13. a[i], a[i+1] = a[i+1], a[i] //交换
  14. }
  15. }
  16. runtime.Gosched() //在部分工作负载后让出处理器
  17. }
  18. fmt.Println(str + " done") //完成消息
  19. ch <- a
  20. }
  21. /* 使用伪随机数填充切片 */
  22. func random_fill(a []int) []int {
  23. for i := 0; i < len(a); i++ {
  24. a[i] = rand.Int()
  25. }
  26. return a
  27. }
  28. func main() {
  29. rand.Seed(time.Now().UTC().UnixNano()) //设置随机数种子
  30. a1 := make([]int, 34589) //创建切片
  31. a2 := make([]int, 42) //创建切片
  32. a3 := make([]int, 9999) //创建切片
  33. a1 = random_fill(a1) //填充切片
  34. a2 = random_fill(a2) //填充切片
  35. a3 = random_fill(a3) //填充切片
  36. fmt.Println("Slices filled ...")
  37. ch1 := make(chan []int) //创建结果通道
  38. ch2 := make(chan []int) //创建结果通道
  39. ch3 := make(chan []int) //创建结果通道
  40. go bubblesort("Thread 1", a1, ch1) //1. 启动协程
  41. go bubblesort("Thread 2", a2, ch2) //2. 启动协程
  42. go bubblesort("Thread 3", a3, ch3) //3. 启动协程
  43. fmt.Println("Main working ...")
  44. <-ch1 //等待结果1
  45. <-ch2 //等待结果2
  46. <-ch3 //等待结果3
  47. }
  48. 输出
  49. Slices filled ...
  50. Main working ...
  51. Thread 2 done
  52. Thread 3 done
  53. Thread 1 done
  54. 我还使用通道实现了我之前评论中建议的会合
  55. 最好的问候 :)
  56. <details>
  57. <summary>英文:</summary>
  58. The important thing is the ability to &quot;yield&quot; the processor to other processes, before the whole potentialy long-running workload is finished. This holds true as well in single-core context or multi-core context (because [Concurrency is not the same as Parallelism][1]).
  59. This is exactly what the [runtime.Gosched()][2] function does :
  60. &gt; Gosched yields the processor, allowing other goroutines to run. It
  61. &gt; does not suspend the current goroutine, so execution resumes
  62. &gt; automatically.
  63. Be aware that a &quot;context switch&quot; is not free : it costs a little time each time.
  64. - On my machine without yielding, your program runs in 5.1s.
  65. - If you yield in the outer loop (`for n:=len(a); n&gt;1; n--`), it runs in 5.2s : small overhead.
  66. - If you yield in the inner loop (`for i:=0; i&lt;n-1; i++`), it runs in 61.7s : huge overhead !!
  67. Here is the modified program correctly yielding, with the small overhead :
  68. package main
  69. import (
  70. &quot;fmt&quot;
  71. &quot;math/rand&quot;
  72. &quot;runtime&quot;
  73. &quot;time&quot;
  74. )
  75. /* Simple bubblesort algorithm*/
  76. func bubblesort(str string, a []int, ch chan []int) {
  77. for n := len(a); n &gt; 1; n-- {
  78. for i := 0; i &lt; n-1; i++ {
  79. if a[i] &gt; a[i+1] {
  80. a[i], a[i+1] = a[i+1], a[i] //swap
  81. }
  82. }
  83. runtime.Gosched() // yield after part of the workload
  84. }
  85. fmt.Println(str + &quot; done&quot;) //done message
  86. ch &lt;- a
  87. }
  88. /*fill slice with pseudo numbers*/
  89. func random_fill(a []int) []int {
  90. for i := 0; i &lt; len(a); i++ {
  91. a[i] = rand.Int()
  92. }
  93. return a
  94. }
  95. func main() {
  96. rand.Seed(time.Now().UTC().UnixNano()) //set seed for rand.
  97. a1 := make([]int, 34589) //create slice
  98. a2 := make([]int, 42) //create slice
  99. a3 := make([]int, 9999) //create slice
  100. a1 = random_fill(a1) //fill slice
  101. a2 = random_fill(a2) //fill slice
  102. a3 = random_fill(a3) //fill slice
  103. fmt.Println(&quot;Slices filled ...&quot;)
  104. ch1 := make(chan []int) //create channel of result
  105. ch2 := make(chan []int) //create channel of result
  106. ch3 := make(chan []int) //create channel of result
  107. go bubblesort(&quot;Thread 1&quot;, a1, ch1) //1. Routine Start
  108. go bubblesort(&quot;Thread 2&quot;, a2, ch2) //2. Routine Start
  109. go bubblesort(&quot;Thread 3&quot;, a3, ch3) //3. Routine Start
  110. fmt.Println(&quot;Main working ...&quot;)
  111. &lt;-ch1 // Wait for result 1
  112. &lt;-ch2 // Wait for result 2
  113. &lt;-ch3 // Wait for result 3
  114. }
  115. Output :
  116. Slices filled ...
  117. Main working ...
  118. Thread 2 done
  119. Thread 3 done
  120. Thread 1 done
  121. I also used channels to implement the rendez-vous, as suggested in my previous comment.
  122. Best regards :)
  123. [1]: http://blog.golang.org/2013/01/concurrency-is-not-parallelism.html
  124. [2]: http://golang.org/pkg/runtime/#Gosched
  125. </details>
  126. # 答案3
  127. **得分**: 3
  128. 自从Go 1.2发布以来原始程序现在可能可以正常工作而无需修改您可以在[Playground][1]中尝试它
  129. 这在[Go 1.2发布说明][2]中有解释
  130. &gt; 在之前的版本中一个无限循环的goroutine可能会使同一线程上的其他goroutine饿死GOMAXPROCS只提供一个用户线程时这是一个严重的问题Go 1.2这部分得到了解决调度器在进入函数时会偶尔被调用
  131. [1]: http://play.golang.org/p/n3Mh86QfiC
  132. [2]: https://golang.org/doc/go1.2#preemption
  133. <details>
  134. <summary>英文:</summary>
  135. Since the release of Go 1.2, the original program now &lt;s&gt;works&lt;/s&gt; _may work_ fine without modification. You may try it in [Playground][1].
  136. This is explained in the [Go 1.2 release notes][2] :
  137. &gt; In prior releases, a goroutine that was looping forever could starve
  138. &gt; out other goroutines on the same thread, a serious problem when
  139. &gt; GOMAXPROCS provided only one user thread. In Go 1.2, this is partially
  140. &gt; addressed: The scheduler is invoked occasionally upon entry to a
  141. &gt; function.
  142. [1]: http://play.golang.org/p/n3Mh86QfiC
  143. [2]: https://golang.org/doc/go1.2#preemption
  144. </details>

huangapple
  • 本文由 发表于 2013年1月18日 20:50:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/14399378.html
匿名

发表评论

匿名网友

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

确定