7 ms·
So, preemptive green-threads? This seems like a counter-productive and highly complicated re-implementation of what the OS thread model does, but inside the Go
by arghwhat 9y ago
So, preemptive green-threads? This seems like a counter-productive and highly complicated re-implementation of what the OS thread model does, but inside the Go runtime.
I would very much like for goroutines to remain cooperative. It is much simpler, naturally more performant, and quite easy to reason about. A local implementation of preemptive execution seems like a very high price to pay to only handle a few corner-cases.
That is not to say that preemptive scheduling does not have their place: They make a lot of sense at an OS level where independent processes are scheduled, but little sense as an internal green-thread implementation.
- chrisseaton 9y ago> This seems like a counter-productive and highly complicated re-implementation of what the OS thread model does, but inside the Go runtime. But I think you still get advantages such as being able to manage stacks yourself, which is what allows for many more coroutines than native threads.
- jnwatson 9y agoThis is the part I don’t understand. You know you can specify the stack size explicitly in POSIX? I can set the stack size to the same 2k in Go and have less overhead. The only difference is it won’t auto-expand like in Go.
- chrisseaton 9y ago> The only difference is it won’t auto-expand like in Go. Well yeah that’s the difference I was talking about.
- coldtea 9y ago>The only difference is it won’t auto-expand like in Go. Which makes all the difference.
- jakewins 9y agoStacks in Linux grow dynamically, so if you want go-like stacks you just set the max stack size == system memory. See http://man7.org/linux/man-pages/man2/setrlimit.2.html http://man7.org/linux/man-pages/man2/setrlimit.2.html
- chrisseaton 9y ago> Stacks in Linux grow dynamically I think Linux can defer committing memory for the pages of the stack until they're used, but you need to reserve the entire virtual memory in the first place don't you? Otherwise how can you have the ability to dynamically allocate more stack virtual address space to an arbitrary thread, without relocating it? Or if you do relocate how do you update pointers to the stack?
- 4ad 9y agoNo, the stack only grows for the initial thread in a process. Subsequent threads don't have growable stacks. And this is true for virtually every operating system that matters, not just Linux. Note that Go stacks can also shrink.
- jakewins 9y agoI don't think this is correct, do you have a source for this? man PTHREAD_ATTR_SETSTACKSIZE(3) says: > The stack size attribute determines the minimum size (in bytes) that will be allocated for threads created using the thread attributes object attr. and: > A thread's stack size is fixed at the time of thread creation. Only the main thread can dynamically grow its stack. My understanding is that it is referring to virtual memory. The kernel would allocate a giant blob of RAM, [stacksize + heapsize + some other stuff] large. My reading of the manpage above is that the main thread can change this allocation, while other threads are stuck with what they started with. But why would the kernel actually realize the stack portion of the allocation? Surely if I create a 1G stack child thread, it will not realize those stack pages until I actually use them?
- 9y ago
- arghwhat 9y agoThat is already the case for the cooperative green threads that goroutines currently comprise of.
- chrisseaton 9y agoBut the cooperative green threads don't get the advantages of preemption, which is the entire point of the exercise.
- dozzie 9y ago> So, preemptive green-threads? This seems like a counter-productive and highly complicated re-implementation of what the OS thread model does, but inside the Go runtime. It would be indeed a reimplementation of what OS does, but note that such a reimplementation is the very reason why quite straightforward Erlang code can easily handle hundreds of thousands active connections on modest hardware.
- imtringued 9y agoThis is completely wrong. The Beam VM is not pre-emptive. It is cooperative. The trick is that it is not possible to write blocking code in Erlang. There are no for loops. The only way to implement iteration is via recursion. The scheduler will only switch to the next actor only after processing a message in it's entirety. It will not interrupt an actor that is currently processing a message. Preemption is good because it prevents bad processes from starving the rest of the system but it's far from efficient.
- jerf 9y ago"The scheduler will only switch to the next actor only after processing a message in it's entirety. It will not interrupt an actor that is currently processing a message." Have they changed it? Last I knew, processes get a "reduction count", which is basically "the number of opcodes in the VM this process gets to run before it gets suspended". Technically, this is exactly like what Go does, except instead of sticking pre-emption points at function calls, it places them at every opcode, which is to say, quite often multiple times per line of code. Technically, you can still freeze the interpreter (or a thread of it) by calling into some C code that counts as one "reduction" but never returns. In practice, this causes about as much trouble for Erlang programmers as it does Go programmers, namely, "it's something you should know but can literally go your entire career without hitting". (Depends on what kind of code you're writing. Someone trying to use Erlang or Go for heavy numerical processing has a good chance of hitting it, but if you're writing network code, you'd have to reeeaallly work at it in either environment.)
- derefr 9y ago
- mseepgood 9y ago> So, preemptive green-threads? This seems like a counter-productive and highly complicated re-implementation of what the OS thread model does, but inside the Go runtime. You can't easily spawn 10.000 OS threads. > I would very much like for goroutines to remain cooperative. It is much simpler, naturally more performant, and quite easy to reason about. Cooperative is less simple to reason about, as the proposal explains. Preemption fully delivers what the developer already expects of goroutines.
- vidarh 9y ago> You can't easily spawn 10.000 OS threads. I just spawned 10.000 OS threads running separate mRuby instances in each thread on my laptop, to see if it'd cause any problems (because I happened to have an app sitting around that spawns separate threads; note: these are not Ruby threads, mRuby does not have built in threading). None at all. So yes, you can. I didn't check how much memory it'd take, however, or attempt to measure overheads in any way, so I'm not saying there aren't potential issues with it.
- jashmatthews 9y agoSimply using 1,000+ OS threads is often very close to as efficient as using coroutines or an event loop: https://www.slideshare.net/e456/tyma-paulmultithreaded1 https://www.slideshare.net/e456/tyma-paulmultithreaded1
- zzzcpan 9y agoIf your event loop is as slow as 1000+ coroutines or OS threads, you are doing it very very wrong.
- jashmatthews 9y agoAFAIK the main benefit to coroutines is getting to manually manage tiny stacks. For example, in Go, they mark a goroutine's stack as "hasn't executed since the last time we marked", which makes GC with thousands of threads a lot faster.
- jerf 9y agoWhy do you think cooperative goroutines are easier to reason about? In Node, you can say with a straight face that once you start executing a bit of code, you can tell that that code will execute without pre-emption until a fairly obvious point where it stops running. In Go, the runtime inserts pre-emption points basically whenever it feels like it; yes, there are some pragmatic rules about where they may be, but even if you know the rules you can't count on them without intensely studying whether or not certain functions got inlined or not or things like that, and you can have no confidence about what other versions of Go (or other implementations) may do. From what I can see, you could theoretically "reason" about your code if you compile it with the optimizer output on, study it carefully, and understand it intimately, but it would be a dangerously fragile line of reasoning subject to change without notice. And I say all this ignoring the fact that whatever you are reasoning about is almost certainly already a bug according to the spec and something the race detector will scream about. I would not feel confident running a program that has known race conditions in it, but that you feel like you've reasoned via intimate knowledge of the runtime that they can't actually happen. I'd much rather see you just write the code to be concurrently correct anyhow, and then I can do things like "upgrade to the next version of Go" or "cross-compile to another architecture that may do things that violate your fragile reasoning" without stark terror.
- arghwhat 9y agoJust to get things right, I said: > It is much simpler, naturally more performant, and quite easy to reason about That is, easy to reason about, not easier to reason about. Technically, preemptive scheduling is harder to reason about, but the point of it all is that you get to pretend it doesn't exist. Thus, from a high-level perspective, preemptive is easier than cooperative. However, cooperative scheduling is very easy to reason about, like in the case of ECMAScript. In ECMAScript, most API's are async, but you can write "await" to preempt a task and wait for promise completion. In Go, things that cause blocking (I/O, synchronization primitives) automatically preempt.
- jerf 9y ago"However, cooperative scheduling is very easy to reason about, like in the case of ECMAScript." And my point is that cooperative scheduling isn't Platonically easy to reason about. It is easy to reason about for specific, concrete reasons, reasons which do not apply to Go code. It makes no sense in your post to ask for goroutines to stay cooperative, because cooperative is easy to reason about, because the way Go does it is not easy to "reason" about. Which is why you're 99.9999% of the time better off pretending Go is already fully pre-emptive and writing your code that way, which is close enough to 100% to just go ahead and do it, as the effort of detecting when you may be in that less-than-once-per-career case exceeds just writing code to be preemptively-correct. I suppose I'll concede that a very, very narrow reading of your text is technically correct, but in that case, that narrow technical reading consists of facts that make no sense to bring up in this context, so I'm not feeling all that guilty about my slightly more expansive reading. (I also don't even particularly agree that cooperative threading is easy to reason about. In practice I have found that even if you know that your code isn't going to be pre-empted in the middle of a block, that is still very difficult to correctly turn into code that is correct in the face of high concurrency. I actually find it easier to write code with sensible concurrency constructs like channels or actors than to try to write correct code based on lots of assumptions about cooperative multithreading. Or, if you prefer, it is certainly academically easy (not easier, but easy) to reason about but in practice I'd take channels, actors, Clojure/Haskell STM, or any of a bunch of other sensible constructs any day. By which I mean, I do; my skin's in the game here, I'm not just abstractly pontificating.)
- weberc2 9y ago> naturally more performant Is this true? The proposal seems to indicate that preemption is more performant? Or at least it says "no runtime overhead", which is presumably in contrast to the current implementation.