英文:
Why do my goroutines wait for each other instead of finishing when done?
问题
我对Go语言还不太熟悉,我的代码中有一部分我不太理解。
我写了一个简单的冒泡排序算法(我知道它不是很高效)。
现在我想要启动3个Go协程。每个协程应该独立地对它的数组进行排序。当排序完成后,函数应该打印一个“done”消息。
这是我的代码:
package main
import (
"fmt"
"time" //用于时间函数,例如Now()
"math/rand" //用于伪随机数
)
/* 简单的冒泡排序算法 */
func bubblesort(str string, a []int) []int {
for n:=len(a); n>1; n-- {
for i:=0; i<n-1; i++ {
if a[i] > a[i+1] {
a[i], a[i+1] = a[i+1], a[i] //交换
}
}
}
fmt.Println(str+" done") //完成消息
return a
}
/* 用伪随机数填充切片 */
func random_fill(a []int) []int {
for i:=0; i<len(a); i++ {
a[i] = rand.Int()
}
return a
}
func main() {
rand.Seed( time.Now().UTC().UnixNano()) //设置随机数种子
a1 := make([]int, 34589) //创建切片
a2 := make([]int, 42) //创建切片
a3 := make([]int, 9999) //创建切片
a1 = random_fill(a1) //填充切片
a2 = random_fill(a2) //填充切片
a3 = random_fill(a3) //填充切片
fmt.Println("Slices filled ...")
go bubblesort("Thread 1", a1) //启动第一个协程
go bubblesort("Thread 2", a2) //启动第二个协程
go bubblesort("Thread 3", a3) //启动第三个协程
fmt.Println("Main working ...")
time.Sleep(1*60*1e9) //等待1分钟以获取“done”消息
}
这是我得到的结果:
Slices filled ...
Main working ...
Thread 1 done
Thread 2 done
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:
package main
import (
"fmt"
"time" //for time functions e.g. Now()
"math/rand" //for pseudo random numbers
)
/* Simple bubblesort algorithm*/
func bubblesort(str string, a []int) []int {
for n:=len(a); n>1; n-- {
for i:=0; i<n-1; i++ {
if a[i] > a[i+1] {
a[i], a[i+1] = a[i+1], a[i] //swap
}
}
}
fmt.Println(str+" done") //done message
return a
}
/*fill slice with pseudo numbers*/
func random_fill(a []int) []int {
for i:=0; i<len(a); i++ {
a[i] = rand.Int()
}
return a
}
func main() {
rand.Seed( time.Now().UTC().UnixNano()) //set seed for rand.
a1 := make([]int, 34589) //create slice
a2 := make([]int, 42) //create slice
a3 := make([]int, 9999) //create slice
a1 = random_fill(a1) //fill slice
a2 = random_fill(a2) //fill slice
a3 = random_fill(a3) //fill slice
fmt.Println("Slices filled ...")
go bubblesort("Thread 1", a1) //1. Routine Start
go bubblesort("Thread 2", a2) //2. Routine Start
go bubblesort("Thread 3", a3) //3. Routine Start
fmt.Println("Main working ...")
time.Sleep(1*60*1e9) //Wait 1 minute for the "done" messages
}
This is what I get:
Slices filled ...
Main working ...
Thread 1 done
Thread 2 done
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执行顺序没有任何保证。
但是,如果你通过显式地让两个处理器核心运行来强制进行真正的并行处理:
import (
"fmt"
"time" //用于时间函数,例如Now()
"math/rand" //用于伪随机数
"runtime"
)
...
func main() {
runtime.GOMAXPROCS(2)
rand.Seed( time.Now().UTC().UnixNano()) //为rand设置种子
...
那么你将会得到预期的结果:
Slices filled ...
Main working ...
Thread 2 done
Thread 3 done
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 :
import (
"fmt"
"time" //for time functions e.g. Now()
"math/rand" //for pseudo random numbers
"runtime"
)
...
func main() {
runtime.GOMAXPROCS(2)
rand.Seed( time.Now().UTC().UnixNano()) //set seed for rand.
...
Then you will get the expected result :
Slices filled ...
Main working ...
Thread 2 done
Thread 3 done
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秒:开销巨大!!
这是修改后的程序,正确地让出处理器,开销很小:
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
/* 简单的冒泡排序算法 */
func bubblesort(str string, a []int, ch chan []int) {
for n := len(a); n > 1; n-- {
for i := 0; i < n-1; i++ {
if a[i] > a[i+1] {
a[i], a[i+1] = a[i+1], a[i] //交换
}
}
runtime.Gosched() //在部分工作负载后让出处理器
}
fmt.Println(str + " done") //完成消息
ch <- a
}
/* 使用伪随机数填充切片 */
func random_fill(a []int) []int {
for i := 0; i < len(a); i++ {
a[i] = rand.Int()
}
return a
}
func main() {
rand.Seed(time.Now().UTC().UnixNano()) //设置随机数种子
a1 := make([]int, 34589) //创建切片
a2 := make([]int, 42) //创建切片
a3 := make([]int, 9999) //创建切片
a1 = random_fill(a1) //填充切片
a2 = random_fill(a2) //填充切片
a3 = random_fill(a3) //填充切片
fmt.Println("Slices filled ...")
ch1 := make(chan []int) //创建结果通道
ch2 := make(chan []int) //创建结果通道
ch3 := make(chan []int) //创建结果通道
go bubblesort("Thread 1", a1, ch1) //1. 启动协程
go bubblesort("Thread 2", a2, ch2) //2. 启动协程
go bubblesort("Thread 3", a3, ch3) //3. 启动协程
fmt.Println("Main working ...")
<-ch1 //等待结果1
<-ch2 //等待结果2
<-ch3 //等待结果3
}
输出:
Slices filled ...
Main working ...
Thread 2 done
Thread 3 done
Thread 1 done
我还使用通道实现了我之前评论中建议的会合。
最好的问候 :)
<details>
<summary>英文:</summary>
The important thing is the ability to "yield" 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]).
This is exactly what the [runtime.Gosched()][2] function does :
> Gosched yields the processor, allowing other goroutines to run. It
> does not suspend the current goroutine, so execution resumes
> automatically.
Be aware that a "context switch" is not free : it costs a little time each time.
- On my machine without yielding, your program runs in 5.1s.
- If you yield in the outer loop (`for n:=len(a); n>1; n--`), it runs in 5.2s : small overhead.
- If you yield in the inner loop (`for i:=0; i<n-1; i++`), it runs in 61.7s : huge overhead !!
Here is the modified program correctly yielding, with the small overhead :
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
/* Simple bubblesort algorithm*/
func bubblesort(str string, a []int, ch chan []int) {
for n := len(a); n > 1; n-- {
for i := 0; i < n-1; i++ {
if a[i] > a[i+1] {
a[i], a[i+1] = a[i+1], a[i] //swap
}
}
runtime.Gosched() // yield after part of the workload
}
fmt.Println(str + " done") //done message
ch <- a
}
/*fill slice with pseudo numbers*/
func random_fill(a []int) []int {
for i := 0; i < len(a); i++ {
a[i] = rand.Int()
}
return a
}
func main() {
rand.Seed(time.Now().UTC().UnixNano()) //set seed for rand.
a1 := make([]int, 34589) //create slice
a2 := make([]int, 42) //create slice
a3 := make([]int, 9999) //create slice
a1 = random_fill(a1) //fill slice
a2 = random_fill(a2) //fill slice
a3 = random_fill(a3) //fill slice
fmt.Println("Slices filled ...")
ch1 := make(chan []int) //create channel of result
ch2 := make(chan []int) //create channel of result
ch3 := make(chan []int) //create channel of result
go bubblesort("Thread 1", a1, ch1) //1. Routine Start
go bubblesort("Thread 2", a2, ch2) //2. Routine Start
go bubblesort("Thread 3", a3, ch3) //3. Routine Start
fmt.Println("Main working ...")
<-ch1 // Wait for result 1
<-ch2 // Wait for result 2
<-ch3 // Wait for result 3
}
Output :
Slices filled ...
Main working ...
Thread 2 done
Thread 3 done
Thread 1 done
I also used channels to implement the rendez-vous, as suggested in my previous comment.
Best regards :)
[1]: http://blog.golang.org/2013/01/concurrency-is-not-parallelism.html
[2]: http://golang.org/pkg/runtime/#Gosched
</details>
# 答案3
**得分**: 3
自从Go 1.2发布以来,原始程序现在可能可以正常工作而无需修改。您可以在[Playground][1]中尝试它。
这在[Go 1.2发布说明][2]中有解释:
> 在之前的版本中,一个无限循环的goroutine可能会使同一线程上的其他goroutine饿死,当GOMAXPROCS只提供一个用户线程时,这是一个严重的问题。在Go 1.2中,这部分得到了解决:调度器在进入函数时会偶尔被调用。
[1]: http://play.golang.org/p/n3Mh86QfiC
[2]: https://golang.org/doc/go1.2#preemption
<details>
<summary>英文:</summary>
Since the release of Go 1.2, the original program now <s>works</s> _may work_ fine without modification. You may try it in [Playground][1].
This is explained in the [Go 1.2 release notes][2] :
> In prior releases, a goroutine that was looping forever could starve
> out other goroutines on the same thread, a serious problem when
> GOMAXPROCS provided only one user thread. In Go 1.2, this is partially
> addressed: The scheduler is invoked occasionally upon entry to a
> function.
[1]: http://play.golang.org/p/n3Mh86QfiC
[2]: https://golang.org/doc/go1.2#preemption
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论