Go Concurrency – Are Goroutine Pools Just Green Threads?

concurrencygomultithreading

The commentator here offers the following criticism of green threads:

I was initially sold on the N:M model as a means of having event driven programming without the callback hell. You can write code that looks like pain old procedural code but underneath there's magic that uses userspace task switching whenever something would block. Sounds great. The problem is that we end up solving complexity with more complexity. swapcontext() and family are fairly strait-forward, the complexity comes from other unintended places.

All of a sudden you're forced to write a userspace scheduler and guess what it's really hard to write a scheduler that's going to do a better job that Linux's schedules that has man years of efforts put into it. Now you want your schedule to man N green threads to M physical threads so you have to worry about synchronization. Synchronization brings performance problems so you start now you're down a new lockless rabbit hole. Building a correct highly concurrent scheduler is no easy task.

Another critique is here:

A single process faking multiple threads has a lot of problems. One of them is that all the faked threads stall on any page fault.

My question is – are go-lang's goroutines (for a default pool) just green threads? If so – do they address the criticisms above?

Best Answer

I'm only a casual Go user, so take the following with a grain of salt.

Wikipedia defines green threads as "threads that are scheduled by a virtual machine (VM) instead of natively by the underlying operating system". Green threads emulate multithreaded environments without relying on any native OS capabilities, and they are managed in user space instead of kernel space, enabling them to work in environments that do not have native thread support.

Go (or more exactly the two existing implementations) is a language producing native code only - it does not use a VM. Furthermore, the scheduler in the current runtime implementations relies on OS level threads (even when GOMAXPROCS=1). So I think talking about green threads for the Go model is a bit abusive.

Go people have coined the goroutine term especially to avoid the confusion with other concurrency mechanisms (such as coroutines or threads or lightweight processes).

Of course, Go supports a M:N threading model, but it looks much closer to the Erlang process model, than to the Java green thread model.

Here are a few benefits of the Go model over green threads (as implemented in early JVM):

  • Multiple cores or CPUs can be effectively used, in a transparent way for the developer. With Go, the developer should take care of concurrency. The Go runtime will take care of parallelism. Java green threads implementations did not scale over multiple cores or CPUs.

  • System and C calls are non blocking for the scheduler (all system calls, not only the ones supporting multiplexed I/Os in event loops). Green threads implementations could block the whole process when a blocking system call was done.

  • Copying or segmented stacks. In Go, there is no need to provide a maximum stack size for the goroutine. The stack grows incrementally as needed. One consequence is a goroutine does not require much memory (4KB-8KB), so a huge number of them can be happily spawned. Goroutine usage can therefore be pervasive.

Now, to address the criticisms:

  • With Go, you do not have to write a userspace scheduler: it is already provided with the runtime. It is a complex piece of software, but it is the problem of Go developers, not of Go users. Its usage is transparent for Go users. Among the Go developers, Dmitri Vyukov is an expert in lockfree/waitfree programming, and he seems to be especially interested in addressing the eventual performance issues of the scheduler. The current scheduler implementation is not perfect, but it will improve.

  • Synchronization brings performance problem and complexity: this is partially true with Go as well. But note the Go model tries to promote the usage of channels and a clean decomposition of the program in concurrent goroutines to limit synchronization complexity (i.e. sharing data by communicating, instead of sharing memory to communicate). By the way, the reference Go implementation provides a number of tools to address performance and concurrency issues, like a profiler, and a race detector.

  • Regarding page fault and "multiple threads faking", please note Go can schedule goroutine over multiple system threads. When one thread is blocked for any reason (page fault, blocking system calls), it does not prevent the other threads to continue to schedule and run other goroutines. Now, it is true that a page fault will block the OS thread, with all the goroutines supposed to be scheduled on this thread. However in practice, the Go heap memory is not supposed to be swapped out. This would be the same in Java: garbage collected languages do not accomodate virtual memory very well anyway. If your program must handle page fault in a graceful way, if it probably because it has to manage some off-heap memory. In that case, wrapping the corresponding code with C accessor functions will simply solve the problem (because again C calls or blocking system calls never block the Go runtime scheduler).

So IMO, goroutines are not green threads, and the Go language and current implementation mostly addresses these criticisms.