0

I'm trying to understand golang architecture and what "lightweight thread" means. I've already read something, but want to ask question to clarify it.

Am I right if I'll say what "go" keyword under the hood just puts following function in queue of inner thread pool, but for user it looks like creation of thread?

  • 2
    Goroutines are not threads. A goroutine can be assigned to one thread for a while, then to another thread. Goroutines are concurrently running functions. A single-thread single-core machine can run many goroutines. – Burak Serdar Aug 31 '22 at 22:32
  • @BurakSerdar, thanks for answering, I never called goroutine a thread. Can you please clarify something for me. If on 1 core 1 thread machine running two goroutines and one of them blocks thread by reading long file for example (not async). Will it block second goroutine? Why if no? Thanks in advance – YuriyKortev Aug 31 '22 at 22:43
  • 2
    This Go FAQ answer might help: https://go.dev/doc/faq#goroutines – Ben Hoyt Aug 31 '22 at 22:53
  • @BenHoyt thanks, but I still have question. Why need to move tasks from one thread to another by some planner instead of using some global queue like in thread pools, so threads can manage tasks by themselves? – YuriyKortev Aug 31 '22 at 23:00
  • I don't know enough low-level Go runtime details to be able to answer that, sorry (and I don't really know what a "global queue like thread pools" means). I do know that Go's runtime does a very good job with this, and you can have hundreds of thousands, even millions, of goroutines active at once. – Ben Hoyt Aug 31 '22 at 23:09
  • 2
    Go runtime has a scheduler that assigns goroutines to threads and decides which ones run when. – Burak Serdar Aug 31 '22 at 23:10
  • @BurakSerdar, I can't understand how it can be preempted if single thread process blocked by system call to read long file – YuriyKortev Aug 31 '22 at 23:24
  • 1
    Blocking system calls are given their own threads, and do not count towards GOMAXPROCS – JimB Aug 31 '22 at 23:29

1 Answers1

4

This is copied from the Go FAQ:

Why goroutines instead of threads?

Goroutines are part of making concurrency easy to use. The idea, which has been around for a while, is to multiplex independently executing functions—coroutines—onto a set of threads. When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked. The programmer sees none of this, which is the point. The result, which we call goroutines, can be very cheap: they have little overhead beyond the memory for the stack, which is just a few kilobytes.

What's lacking here is the definition of thread. If we resort to Wikipedia, we find:

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, ...

but that's just a description of, well, the same thing that a goroutine is. The problem here is that the word thread tends to refer to kernel thread and/or user thread (both defined on that same Wikipedia page) and these threads are heavier-weight than the goroutine threads. Which brings us right back to this:

I'm trying to understand golang architecture and what "lightweight thread" means ...

To cut to the chase, this means "lighter than the OS-provided ones". That's really all it means. There are OS-provided threads (on multiple OSes on which Go runs), but they generally do too much and cost too much to switch between so Go provides its own language-level ones that it calls "goroutines" that are much lighter.

From comments:

Why need to move tasks from one thread to another by some planner ...

This is an implementation detail, which involves another aspect of the OS-provided kernel threads:

I can't understand how [a goroutine] can be preempted if single thread process [is] blocked by [a] system call to read [a] long file

The current Go runtime goroutine / thread / processor scheduler (see What is relationship between goroutine and thread in kernel and user state and note that there have been more than just the current implementation) predicts that some system call will block, and makes sure to assign that system call its own OS-level kernel thread (see also JimB's comment). These threads do not count against the GOMAXPROCS setting. This is in fact sometimes a problem, as it's possible for the Go runtime to try to spin off more threads than the OS allows: it might be nice if there were a system-call-thread-pool here (though there are also obvious problems with this).

So, the current runtime creates up to GOMAXPROCS kernel-style OS-level threads and uses those to multiplex up to that many goroutines onto the CPUs, but creates extra kernel-style OS-level threads whenever it wants to. As the blog post linked in the question above notes, the P entities act as queues to hold goroutines (Gs) on a per-processor basis for localized cache lookup (remember that on some systems, especially NUMA ones, it's expensive to reach out "across" CPUs: the scheduler is still willing to do this, but won't do it too often, for some definition of "too often").

Earlier versions of the current scheduler required explicit yields (runtime.Gosched()) calls or various other runtime operations to cause a switch from the current goroutine to some other goroutine. See What exactly does runtime.Gosched do? for example. In Go 1.14, some OSes provide automatic goroutine preemption; see Will Go's scheduler yield control from one goroutine to another for CPU-intensive work?

torek
  • 448,244
  • 59
  • 642
  • 775