2

As windows user, I know that OS threads consume ~1 Mb of memory due to By default, Windows allocates 1 MB of memory for each thread’s user-mode stack. How does golang use ~8kb of memory for each goroutine, if OS thread is much more gluttonous. Are goroutine sort of virtual threads?

icza
  • 389,944
  • 63
  • 907
  • 827
Ark
  • 1,343
  • 2
  • 12
  • 26

3 Answers3

5

Goroutines are not threads, they are (from the spec):

...an independent concurrent thread of control, or goroutine, within the same address space.

Effective Go defines them as:

They're called goroutines because the existing terms—threads, coroutines, processes, and so on—convey inaccurate connotations. A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.

Goroutines don't have their own threads. Instead multiple goroutines are (may be) multiplexed onto the same OS threads so if one should block (e.g. waiting for I/O or a blocking channel operation), others continue to run.

The actual number of threads executing goroutines simultaneously can be set with the runtime.GOMAXPROCS() function. Quoting from the runtime package documentation:

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

Note that in current implementation by default only 1 thread is used to execute goroutines.

icza
  • 389,944
  • 63
  • 907
  • 827
2

1 MiB is the default, as you correctly noted. You can pick your own stack size easily (however, the minimum is still a lot higher than ~8 kiB).

That said, goroutines aren't threads. They're just tasks with coöperative multi-tasking, similar to Python's. The goroutine itself is just the code and data required to do what you want; there's also a separate scheduler (which runs on one on more OS threads), which actually executes that code.

In pseudo-code:

loop forever
 take job from queue
 execute job
end loop

Of course, the execute job part can be very simple, or very complicated. The simplest thing you can do is just execute a given delegate (if your language supports something like that). In effect, this is simply a method call. In more complicated scenarios, there can be also stuff like restoring some kind of context, handling continuations and coöperative task yields, for example.

This is a very light-weight approach, and very useful when doing asynchronous programming (which is almost everything nowadays :)). Many languages now support something similar - Python is the first one I've seen with this ("tasklets"), long before go. Of course, in an environment without pre-emptive multi-threading, this was pretty much the default.

In C#, for example, there's Tasks. They're not entirely the same as goroutines, but in practice, they come pretty close - the main difference being that Tasks use threads from the thread pool (usually), rather than a separate dedicated "scheduler" threads. This means that if you start 1000 tasks, it is possible for them to be run by 1000 separate threads; in practice, it would require you to write very bad Task code (e.g. using only blocking I/O, sleeping threads, waiting on wait handles etc.). If you use Tasks for asynchronous non-blocking I/O and CPU work, they come pretty close to goroutines - in actual practice. The theory is a bit different :)

EDIT:

To clear up some confusion, here is how a typical C# asynchronous method might look like:

async Task<string> GetData()
{
  var html = await HttpClient.GetAsync("http://www.google.com");

  var parsedStructure = Parse(html);
  var dbData = await DataLayer.GetSomeStuffAsync(parsedStructure.ElementId);

  return dbData.First().Description;
}

From point of view of the GetData method, the entire processing is synchronous - it's just as if you didn't use the asynchronous methods at all. The crucial difference is that you're not using up threads while you're doing the "waiting"; but ignoring that, it's almost exactly the same as writing synchronous blocking code. This also applies to any issues with shared state, of course - there isn't much of a difference between multi-threading issues in await and in blocking multi-threaded I/O. It's easier to avoid with Tasks, but just because of the tools you have, not because of any "magic" that Tasks do.

The main difference from goroutines in this aspect is that Go doesn't really have blocking methods in the usual sense of the word. Instead of blocking, they queue their particular asynchronous request, and yield. When the OS (and any other layers in Go - I don't have deep knowledge about the inner workings) receives the response, it posts it to the goroutine scheduler, which in turns knows that the goroutine that "waits" for the response is now ready to resume execution; when it actually gets a slot, it will continue on from the "blocking" call as if it had really been blocking - but in effect, it's very similar to what C#'s await does. There's no fundamental difference - there's quite a few differences between C#'s approach and Go's, but they're not all that huge.

And also note that this is fundamentally the same approach used on old Windows systems without pre-emptive multi-tasking - any "blocking" method would simply yield the thread's execution back to the scheduler. Of course, on those systems, you only had a single CPU core, so you couldn't execute multiple threads at once, but the principle is still the same.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • Yes, it's look like C# `Tasks`. Tasks are queued like messages and executed by threads from threadpool. When 10 tasks are too short then may be sufficient only one OS thread to execute them all – Ark May 06 '15 at 11:12
  • What approach is faster in practice ? and how much ? – Ark May 06 '15 at 11:16
  • @Ark Each have their pro's and con's - but I haven't really seen a case where you'd see much of a difference. By far the biggest difference is the syntax and usage of each. And I'm not sure, but I think goroutines are always coöperative, while .NET's tasks are a mix of pre-emptive and coöperative. Also, .NET tasks don't really have a stack the way goroutines (or threads) do - they really are just method calls that eventually `return`; they only have a stack when they're executing (and it's the thread's stack). – Luaan May 06 '15 at 11:22
  • @Ark Since goroutines always allocate at least 2 kiB of stack, it's actually possible for `Task`s to be lighter weight in some scenarios. But that's matter for profiling, not guessing. If you have some idea about what you're trying to do, it should be pretty easy to write some prototypes you can performance test directly, and find out which suits you better. – Luaan May 06 '15 at 11:24
  • I like this answer - I was a bit worried when you started to compare goroutines to .NET `Task`s but you managed to not compare them one-to-one which made me happy :) – Simon Whitehead May 06 '15 at 12:03
  • The crucial difference of the Go approach to that of C# is that Go encourages you to only use blocking code for everything and the Go runtime scheduler makes sure goroutines are properly preempted when they are about to really block on some system resource. Hence when writing a goroutine you're writting sequential code and do not reason about any synchronous vs asyncoronous nonsense. – kostix May 07 '15 at 10:21
  • @kostix Actually, that's not a difference. C# Tasks work really the same way - the only difference being that you explicitly use the `await` keyword when you're doing the asynchronous operation. But the method itself still appears entirely synchronous (as long as you avoid shared state, of course). The basic idea is still the same, you just have to understand that C# has to maintain backward compatibility, so you can't just change the blocking methods to suddenly be asynchronous. Goroutines are *not* pre-empted - it's just that blocking operations aren't blocking, they `yield` (coöperatively). – Luaan May 07 '15 at 10:26
  • @kostix I added some clarification to my answer, do you think it helps? – Luaan May 07 '15 at 10:40
0

goroutines are what we call green threads. They are not OS threads, the go scheduler is responsible for them. This is why they can have much smaller memory footprints.

Games Brainiac
  • 80,178
  • 33
  • 141
  • 199