了解goroutines
我想了解Go中的并发性。 特别是,我写了这个线程不安全的程序:
package main import "fmt" var x = 1 func inc_x() { //test for { x += 1 } } func main() { go inc_x() for { fmt.Println(x) } }
我认识到我应该使用渠道来防止x
竞争条件,但这不是重点。 该程序打印1
,然后似乎永远循环(不打印任何更多)。 我希望它打印出一个无限的数字列表,可能跳过一些,并重复其他人由于竞争条件(或更糟糕的 – 打印数字,而它正在更新inc_x
)。
我的问题是:为什么程序只打印一行?
只是要清楚:我没有使用渠道的目的这个玩具的例子。
关于Go的goroutines,有几件事要记住。
- 它们不是Java或C ++线程意义上的线程。
- 他们更像greenlet。
- go运行时间跨系统线程多路复用goroutine
- 系统线程的数目由一个环境variablesGOMAXPROCS控制,目前我认为默认为1。 这可能会在未来发生变化。
- goroutine返回到当前线程的方式受到几种不同构造的控制。
- select语句可以控制回到线程。
- 在一个频道上发送可以控制回到线程。
- 做IO操作可以让控制回到线程。
- runtime.Gosched()显式地将控制返回给线程。
你看到的行为是因为主函数永远不会退回到线程,而是涉及繁忙的循环,因为只有一个线程主循环没有地方运行。
根据这个和这个 ,一些调用在CPU限制的Goroutine期间不能调用(如果Goroutine从不屈服于调度器)。 这可能会导致其他Goroutines挂起,如果他们需要阻止主线程(这是fmt.Println()
使用的write()
系统调用的情况)
我find的解决scheme涉及调用runtime.Gosched()
在你的CPU绑定线程退让给调度,如下所示:
package main import ( "fmt" "runtime" ) var x = 1 func inc_x() { for { x += 1 runtime.Gosched() } } func main() { go inc_x() for { fmt.Println(x) } }
因为你只在Goroutine中执行一个操作,所以runtime.Gosched()
被经常调用。 在init上调用runtime.GOMAXPROCS(2)
速度要快一个数量级,但是如果你做了比增加数字更复杂的任何事情(例如处理数组,结构,地图等),那么会非常不安全。
在这种情况下,最佳实践可能会使用一个通道来pipe理对资源的共享访问。
更新:从Go 1.2开始, 任何非内联函数调用都可以调用调度器。
这是两件事的交互。 一,默认情况下,Go只使用一个核心,两个Go必须协调调度。 你的函数inc_x不屈服,所以它垄断了正在使用的单核。 解除这些条件之一将导致您期望的输出。
说“核心”是有点光泽。 Go实际上可能在后台使用多个内核,但它使用一个名为GOMAXPROCS的variables来确定要调度正在执行非系统任务的goroutine的线程数。 正如FAQ和Effective Go中所解释的那样,默认值是1,但是可以使用环境variables或运行时函数将其设置得更高。 这可能会提供您期望的输出,但前提是您的处理器具有多个内核。
独立于内核和GOMAXPROCS,您可以在运行时给goroutine调度程序一个机会来完成它的工作。 调度程序不能抢占正在运行的goroutine,但必须等待它返回到运行时并请求一些服务,如IO,time.Sleep()或runtime.Gosched()。 在inc_x中添加这样的任何东西都会产生预期的输出。 运行main()的goroutine已经在使用fmt.Println来请求一个服务,所以现在这两个goroutine现在可以周期性地向运行时发送消息,它可以做一些公平的调度。
不知道,但我认为 inc_x
占用CPU。 由于没有IO,它不释放控制。
我发现有两件事解决了这个问题。 一个是在程序开始时调用runtime.GOMAXPROCS(2)
,然后它将工作,因为现在有两个服务goroutings的线程。 另一种是在增加x
之后插入time.Sleep(1)
。