理解goroutines

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

Understanding 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中更新数字时打印出该数字)。

我的问题是:为什么该程序只打印一行?

只是为了明确:我故意在这个玩具示例中不使用通道。

英文:

I'm trying to understand concurrency in Go. In particular, I wrote this thread-unsafe program:

<!-- language: lang-go -->

package main

import &quot;fmt&quot;

var x = 1

func inc_x() { //test
  for {
    x += 1
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

I recognize that I should be using channels to prevent race conditions with x, but that's not the point here. The program prints 1 and then seems to loop forever (without printing anything more). I would expect it to print an infinite list of numbers, possibly skipping some and repeating others due to the race condition (or worse -- printing the number while it is being updated in inc_x).

My question is: Why does the program only print one line?

Just to be clear: I'm not using channels on purpose for this toy example.

答案1

得分: 41

关于Go的goroutines,有几点需要记住:

  1. 它们不是像Java或C++的线程那样的线程
  2. Go运行时将goroutines多路复用到系统线程
    • 系统线程的数量由环境变量GOMAXPROCS控制,默认为1。这可能会在将来发生变化
  3. goroutines将控制权交还给它们当前的线程的方式由几种不同的结构控制
    • select语句可以将控制权交还给线程
    • 通道上发送数据可以将控制权交还给线程
    • 执行IO操作可以将控制权交还给线程
    • runtime.Gosched()可以显式地将控制权交还给线程

你看到的行为是因为主函数从不将控制权交还给线程,而是参与了一个忙碌的循环,由于只有一个线程,主循环没有运行的地方。

英文:

There are a few things to keep in mind about Go's goroutines:

  1. They are not threads in the sense of Java's or C++ threads
  1. The go runtime multiplexes the goroutines across the system threads
  • the number of system threads is controlled by an environment variable GOMAXPROCS and defaults to 1 currently I think. This may change in the future
  1. The way goroutines yield back to their current thread is controlled by several different constructs
  • the select statement can yield control back to the thread
  • sending on a channel can yield control back to the thread
  • doing IO operations can yield control back to the thread
  • runtime.Gosched() explicitly yields control back to the thread

The behavior you are seeing is because the main function never yields back to the thread and is instead involved in a busy loop and since there is only one thread the main loop has no place to run.

答案2

得分: 18

根据thisthis,在一个CPU绑定的Goroutine中,有些调用不能被调用(如果Goroutine从不让出给调度器)。如果其他Goroutine需要阻塞主线程(例如fmt.Println()使用的write()系统调用),这可能导致其他Goroutine挂起。

我找到的解决方案是在CPU绑定的线程中调用runtime.Gosched()来让出给调度器,如下所示:

<!-- language: lang-go -->

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()被调用得非常频繁。在初始化时调用runtime.GOMAXPROCS(2)会快上一个数量级,但如果你要做的事情比递增一个数字更复杂(例如处理数组、结构体、映射等),这样做可能会非常不安全。

在这种情况下,最佳实践可能是使用通道来管理对共享资源的访问。

更新:从Go 1.2开始,任何非内联函数调用都可以调用调度器。

英文:

According to this and this, some calls can't invoke during a CPU-bound Goroutine (if the Goroutine never yields to the scheduler). This can cause other Goroutines to hang if they need to block the main thread (such is the case with the write() syscall used by fmt.Println())

The solution I found involved calling runtime.Gosched() in your cpu-bound thread to yield back to the scheduler, as follows:

<!-- language: lang-go -->

package main

import (
  &quot;fmt&quot;
  &quot;runtime&quot;
)

var x = 1

func inc_x() {
  for {
    x += 1
    runtime.Gosched()
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

Because you're only performing one operation in the Goroutine, runtime.Gosched() is being called very often. Calling runtime.GOMAXPROCS(2) on init is faster by an order of magnitude, but would be very thread-unsafe if you were doing anything more complicated than incrementing a number (for example, dealing with arrays, structs, maps, etc).

In that case, best practice would potentially be using a channel to manage shared access to a resource.

Update: As of Go 1.2, any non-inlined function call can invoke the scheduler.

答案3

得分: 8

这是两个事物的相互作用。首先,默认情况下,Go只使用一个核心,其次,Go必须合作地调度goroutine。你的函数inc_x没有让出控制权,因此它垄断了正在使用的单个核心。解除这两个条件中的任何一个都将导致你期望的输出。

说“核心”有点含糊。Go实际上可能在后台使用多个核心,但它使用一个名为GOMAXPROCS的变量来确定调度非系统任务的goroutine的线程数。正如在FAQEffective Go中所解释的,默认值为1,但可以通过环境变量或运行时函数将其设置得更高。这可能会产生你期望的输出,但前提是你的处理器有多个核心。

与核心和GOMAXPROCS无关,你可以给运行时中的goroutine调度器一个机会来完成它的工作。调度器无法抢占正在运行的goroutine,但必须等待它返回到运行时并请求某些服务,例如IO、time.Sleep()或runtime.Gosched()。在inc_x中添加任何类似的内容都会产生预期的输出。运行main()的goroutine已经通过fmt.Println请求了一个服务,因此现在两个goroutine定期让出控制权给运行时,它可以进行某种公平调度。

英文:

It's an interaction of two things. One, by default, Go only uses a single core, and two, Go must schedule goroutines cooperatively. Your function inc_x doesn't yield and so it monopolizes the single core being used. Relieving either of these conditions will lead to the output you expect.

Saying "core" is a bit of a gloss. Go may actually use multiple cores behind the scenes, but it uses a variable called GOMAXPROCS to determine the number of threads to schedule your goroutines which are performing non-system tasks. As explained in the FAQ and Effective Go the default is 1, but it may be set higher with an environment variable or a runtime function. This will likely give the output you expect, but only if your processor has multiple cores.

Independently of cores and GOMAXPROCS, you can give the goroutine scheduler in the runtime a chance to do it's job. The scheduler cannot preempt a running goroutine but must wait for it to come back to the runtime and request some service, such as IO, time.Sleep(), or runtime.Gosched(). Adding anything like this in inc_x produces expected output. The goroutine running main() is already requesting a service with fmt.Println, so with the two goroutines now periodically yielding to the runtime, it can do some sort of fair scheduling.

答案4

得分: 3

不确定,但我认为 inc_x 正在占用 CPU。由于没有 IO,它不会释放控制。

我找到了两个解决方法。一个是在程序开始时调用 runtime.GOMAXPROCS(2),然后它就会工作,因为现在有两个线程为 goroutine 提供服务。另一个是在增加 x 后插入 time.Sleep(1)

英文:

No sure, but I think that inc_x is hogging the CPU. Since there's no IO it doesn't release control.

I found two things that solved that. One was to call runtime.GOMAXPROCS(2) at the beginning of the program and then it'll work since now there are two threads serving goroutings. The other is to insert time.Sleep(1) after incrementing x.

huangapple
  • 本文由 发表于 2012年4月11日 04:10:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/10095751.html
匿名

发表评论

匿名网友

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

确定