Understanding goroutines

2019-01-13 09:47发布

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

package main

import "fmt"

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.

4条回答
Rolldiameter
2楼-- · 2019-01-13 10:28

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:

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)
  }
}

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.

查看更多
Ridiculous、
3楼-- · 2019-01-13 10:42

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.

查看更多
爷、活的狠高调
4楼-- · 2019-01-13 10:46

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. They are more like greenlets.
  2. The go runtime multiplexes the goroutines across the system threads
    1. 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.
  3. The way goroutines yield back to their current thread is controlled by several different constructs.
    1. The select statement can yield control back to the thread.
    2. sending on a channel can yield control back to the thread.
    3. Doing IO operations can yield control back to the thread.
    4. 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.

查看更多
我欲成王,谁敢阻挡
5楼-- · 2019-01-13 10:48

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.

查看更多
登录 后发表回答