Hacker News new | past | comments | ask | show | jobs | submit login
Proposal: Non-cooperative goroutine preemption (github.com/golang)
150 points by mseepgood on March 29, 2018 | hide | past | favorite | 71 comments



The way I understand this, and that I think some other people in this thread misunderstand, is that this is not a change to the Go language in any way.

The way Go works is that you can spawn green threads, and the Go runtime will magically run them concurrently, in such a way that you do not have to worry about many problems usually associated with concurrent programming.

This proposal seeks to modify the way the "magically" part works. Right now, it suggests that what Go does is to 'cooperatively' preempt at function prologues. This change would instead allow the runtime to preempt at a choice of "safe points" the compiler suggests throughout your Go code.

Note that this is a little bit in the gray area between cooperative and non-cooperative. It's not like the runtime will just randomly preempt, it still requires cooperation from the code to have these safe points. But maybe I'm misunderstanding the details.

Anyway, not much changes for the Go programmer. The Go programmer can expect his code to be a little more predictable, at the cost of having his code be a little less performant, this performance cost I think will not be significant, if not fully compensated by the increased parallelism. All the promises the Go runtime makes towards the programmer will still hold.


> This proposal seeks to modify the way the "magically" part works. Right now, it suggests that what Go does is to 'cooperatively' preempt at function prologues. This change would instead allow the runtime to preempt at a choice of "safe points" the compiler suggests throughout your Go code.

That's how it already works, it's just that safepoints only get introduced at function prologues. This is an issue as CPU-heavy functions without any non-inlined function calls mean very few safe points which is problematic for GC and timely scheduling and may even lock up the entire system.

Previous attempts were made to add loop preemption but there are issues with that approach.

The approach in TFA is to make safe points "opt-out", all Go code would be considered safe by default with some "unsafe" regions being excluded. As the author states, this would make Go preemptive rather than cooperative ("I propose that we implement fully non-cooperative preemption").

> Anyway, not much changes for the Go programmer. The Go programmer can expect his code to be a little more predictable

Go programmers can expect their code to be a little less predictable, not more: currently your code is guaranteed to run uninterrupted between two function calls, that will not be the case anymore.

In theory it should not make any difference, in practice it will probably uncover concurrency bug you're currently shielded from by the runtime's behaviour (though likely not that many as you can already have multiple routines running in parallel).


Not just function prologues, but also channel operations. Might sound like a minor nit but it's a pretty important nit.


Fair enough, though I'd assume that to be considered IO and would very much expect to be a place which always yields.


> at the cost of having his code be a little less performant

Where did you read this? The proposal states: "This approach will solve the problem of delayed preemption with zero run-time overhead."


Hopefully this can actually decrease overhead. Go's garbage collector has some short STW pauses that require stopping the world. It you have one thread misbehaving by running a tight loop right now, the rest of the threads will sit there spinning until that one thread reaches a preemption point. With explicit preemption, that period would be much shorter.


The proposal is to turn all go code into safe points.


> Right now, it suggests that what Go does is to 'cooperatively' preempt at function prologues. This change would instead allow the runtime to preempt at a choice of "safe points" the compiler suggests throughout your Go code.

This is incorrect. Go has had preemptive scheduling (instead of cooperative) since at least 1.2 (possibly 1.1). The proposal here is to make it preempt at arbitrary points, not just at certain points.

The points at which it currently can preempt is far greater than simply function prologues, which is why full preemption has very little impact on the end user (it already appears to preempt at essentially arbitrary points).


This reminds me of the safe point mechanism in IBMs J9 JVM to make GC cheaper by shortening the mark phase.


Interesting old thread comparing actors to software transactional memory (STM):

https://www.reddit.com/r/haskell/comments/175tjj/stm_vs_acto...

Seems like pre-emptive actor-based concurrency is simple to implement using STM, but much harder to implement STM with actors.


I'm really excited about this both for the improved long tail latency as well as (what I believe is) a new way of handling this problem.

I'm not aware of another M:N threading model that handles preemption this way with signals. Erlang accomplishes it with reduction budgets and reduced performance.


Pre-emptive runtimes are hard. Inserting yield points judiciously in loops isn't that incredibly difficult, and neither is reducing the cost of a yield point. It's far harder to make sure every machine instruction in every part of the runtime is prepared to be interrupted and have all it assumptions violated by the time it resumes.


> Inserting yield points judiciously in loops isn't that incredibly difficult, and neither is reducing the cost of a yield point.

The Go team has done both over the past few years but we haven't been happy enough with the performance. Hence Austin's proposal.


Imagine how much simpler and faster a runtime could be if every function was split into a series of non interruptible blocks. Sure, a bit tricky with loops, you'd have to cap them to make sure they are not hogging the system and you don't introduce too much overhead. But small price to pay for not wasting years of man-hours on silly low level stuff and barely achieving usable results.


I'd love to see this implemented, especially that I already enjoy this feature a lot in Elixir/Erlang/BEAM.


The problem with that approach in Go is that BEAM embraces the shared-nothing approach, and the Go runtime is quite promiscuous about sharing mutable data. I am afraid that introducing non-deterministic preemptive stops will just make Go concurrency a mess similar to the mutex/conditionals mess they've been trying to avoid.


"I am afraid that introducing non-deterministic preemptive stops will just make Go concurrency a mess similar to the mutex/conditionals mess they've been trying to avoid."

They already took a few steps inside of Go to try to ensure that concurrency is quite non-deterministic. Even very simple, naive code like

    func main() {
        for i := 0; i < 2; i++ {
            go func(j int) {
                fmt.Println("First", j)
                fmt.Println("Second", j)
            }(i)
        }
        time.Sleep(100*time.Millisecond)
    }
will very frequently produce different results; I just ran that four times and got four different results. This is good, because it tends to bring bugs to the forefront more quickly, rather than having a almost-but-not-quite-deterministic model as has been the case in many other environments.

(Yes, I know sleep is not production-quality here, just trying to keep it simpler than several more lines for a proper waitgroup usage.)

I doubt there's a lot of code written at the Go level that would explode if you suddenly ran it with a pre-emptive runtime that isn't already exploding. As the proposal discusses, there's a lot of issues down at the runtime level, but at the layer of abstraction presented by the language itself I wouldn't expect a lot of problems. Non-zero, and even then, it would still be things that are technically already bugs. And Go code is already running concurrently all the time in the real world. I'm not even sure how I'd construct an example that runs correctly in the current runtime but fails with true pre-emption.


> will very frequently produce different results; I just ran that four times and got four different results. This is good, because it tends to bring bugs to the forefront more quickly, rather than having a almost-but-not-quite-deterministic model as has been the case in many other environments.

I concur, they do this with maps too. It led to discovering a bug in our system where order was being implicitly depended on via a JSON Object -> Map conversion. Go randomized the map key order when iterated over, revealing the logic bug.


> They already took a few steps inside of Go to try to ensure that concurrency is quite non-deterministic

I would not be surprised as they are already proactively ensuring things like map ordering are randomised so that people just can't rely on some deterministic ordering: run the example code here[0] multiple times and it produces not just a seemingly random result (as would be for a typical hash table) but a truly different result for each run.

[0]: https://nathanleclaire.com/blog/2014/04/27/a-surprising-feat...


Yes, map iteration is explicitly randomized for just this thing.


Mutating shared state without synchronization is a bug with or without this proposal. Goroutines already run on potentially different OS threads or processors with the current m:n scheduler implementation.


> I am afraid that introducing non-deterministic preemptive stops will just make Go concurrency a mess similar to the mutex/conditionals mess they've been trying to avoid.

It already is exactly that (Go already does m:n scheduling, so while your routine currently can't get interrupted between two function calls, two goroutines running on different OS threads can manipulate the same mutable state without synchronisation and make a mess of it), it's just that you don't necessarily notice.


Go code is not shared-state safe in the way that, say, Python generators are. And with the -race compile flag, it does a pretty good job of detecting violations so you can fix them.

So I think Go code should be unaffected by this change.


interesting, is the current situation one of the reason Go debugging experience is so poor? Delve debugger is constantly crashing for me, making step debugging extremely hard, especially when debugging tests. If there is one thing that is inferior to Java and C# with Go it's definitely the debugging experience.


Not at all. Non-cooperative goroutines would make it much harder to implement a debugger.

Delve crashing is just related to Delve being buggy. Single-stepping is done at the instruction level, and the task of a Go debugger mostly relates to mapping instructions to source code, and understanding the runtime.

You can debug Go with gdb, but without Go runtime comprehension, all you will see is a bunch of worker threads executing Go runtime code, rather than the individual goroutines of your process.


I've actually never run into a problem debugging go binaries with gdb. Which is weird because everyone's always told me it doesn't work. I'm sure there are edge cases but over the last 5 years or so my experience has been pretty good.

emacs + gud mode is a great experience for go debugging. I've even used it to debug low-level issues like plugin loading and verification.


> I've actually never run into a problem debugging go binaries with gdb.

Are you able to step inside code running in a goroutine? with GDB? Because it's where GDB starts to become useless for debugging.


On Mac, I find lldb is terribly low level. IIRC Linux gcc isn't great either. Even delve will frequently seem to have missing/optimized variables that cannot be printed.


I frequently hit the problem with variables that are <optimized out> when debugging C code using gdb, and it's especially infuriating when I can look at the disassembly and see that the variable is right there in the register file.

Does anyone know whether dwarf simply doesn't support the necessary debug information or whether compilers just don't retain it through the optimization process?


The latter. With quality register allocation, the same register can be used for multiple variables (or unnamed intermediate results) within the span of a few instructions. The inverse is also true: the same variable could find its value in multiple registers.

DWARF appears to support mapping symbol locations to registers, but I’ve never seen a language environment/debugger that takes advantage of it for optimized binaries.


Can someone make an informed guess on how much that would complicate the implementation? And/or how much of a performance impact it would have (for better or for worse)?


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.


Why 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.


Just 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.


"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.)


You definitely shouldn't feel guilty about your "expansive reading", but I do prefer to not have claims I did not make assigned to me. :)

Your comment is rather empty without examples or elaboration. It also seems that all but one point is not actually related to cooperative vs. preemptive multitasking.

You claim that the reasons for cooperative multitasking being simple do not apply to Go code (from your previous post, I assume that you find them to apply to ECMAScript). However, you do not present any reasons for this. Would you mind elaborating?

Go is a little bit more magic than ECMAScript, but ECMAScript is for all intends and purposes a single-threaded language (Web workers exist, but they cannot share resources due to the memory problems it would create, and instead rely on a simple message passing mechanism). This makes it a very different beast from Go (read: simpler).

However, it is not very magic. You can basically just consider all I/O and synchronization primitives to be prefixed with "runtime.Gosched()", or the equivalent yielding function from other green thread implementations.

Elaboration of your comment about the difficulty of turning cooperative code into "correct code in the face of high concurrency" would be also nice. Some types of parallel code can be quite hairy to guarantee correctness of, but I don't see how this is related to cooperative vs. preemptive multitasking.

You are mentioning channels and actors as if that is mutually exclusive from the Go model, which I find odd considering that Go caters specifically to the use of channels and actors. They are also completely detached from the choice of multitasking model, and exist just fine in both preemptive and cooperative environments.

I do quite like the channel/actor model, but that is entirely unrelated to the discussion. I would also find the implication that all other primitives are not "sensible" to be a very, very large claim. And a broken one at that. :)


The go compiler (already) inserts yields in a bunch of places that aren't IO or explicit synchronization points. And it's hard to predict the exact locations they'll be because much of it happens after some optimizations have been made. It is very much already attempting to look preemptive, which is why the authors didn't use the term coroutine for the language feature -- it's an implementation detail.

This proposal is about fixing some of the edge cases where the current implementation doesn't do what the interface is supposed to. It outlines another, simpler approach which just has the compiler add more yields, but apparently the performance impact of that is too high. But it's just a question of perf & implementation complexity, not semantics.


> 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.


> You can't easily spawn 10.000 OS threads.

Of course you can, without any problem at all. It's just more expensive than a green thread (they're full processes with stacks), and unlike green threads, it affects how processing time is sliced. It is, however, much cheaper than people tend to believe.

> Cooperative is less simple to reason about, as the proposal explains.

Cooperative scheduling is very simple to reason about. In the simplest form, you do not give up control unless you use I/O, a synchronization primitive, or explicitly give up control (runtime.Gosched() in Go).

As a Go developer, and as a developer in any other language, this is exactly what I expect from any green thread implementation. That's how M:N scheduling has always been implemented, and how green threads have been implemented in other languages.

Technically, preemptive is harder to reason about, but you save having to reason about it. However, if you expect preemption from goroutines, you have misunderstood the primitive.


> In the simplest form, you do not give up control unless you use I/O, a synchronization primitive, or explicitly give up control (runtime.Gosched() in Go).

This is not how Go works nowadays. There are other preemption points.


That's why I said "in the simplest form".


Yes, but it completely ruins the argument about "it's easy to reason about" ;-)


> You can't easily spawn 10.000 OS threads.

This is what I thought until last week, but it seems it's no big deal. 1M threads starts to be an issue.


> You can't easily spawn 10.000 OS threads.

Of course you can. Go on, compile and run this program:

    #include <pthread.h>
    #include <stdlib.h>

    void *f(void *arg) {
        pthread_mutex_lock((pthread_mutex_t *)arg);
        pthread_mutex_unlock((pthread_mutex_t *)arg);
        return NULL;
    }

    int main() {
        pthread_t threads[10000];
        pthread_mutex_t mutexes[10000];
        for (unsigned i = 0; i < 10000; i++) {
            pthread_mutex_init(&mutexes[i], NULL);
            pthread_mutex_lock(&mutexes[i]);
            pthread_create(&threads[i], NULL, f, &mutexes[i]);
        }
        for (unsigned i = 0; i < 10000; i++)
            pthread_mutex_unlock(&mutexes[i]);
        for (unsigned i = 0; i < 10000; i++)
            pthread_join(threads[i], NULL);
        return 0;
    }
Executes in a second for me, even on macOS.


> 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.


Simply 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


If your event loop is as slow as 1000+ coroutines or OS threads, you are doing it very very wrong.


AFAIK 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.


[Citation needed]


https://github.com/golang/go/issues/12061

"Mark termination does not have to rescan stacks that haven’t executed since the last scan"

With pre-emptive scheduling of OS threads, you potentially dirty a ton more stacks, and have to scan them all for pointers to heap memory. I'm not sure how VMs using lots of OS threads deal with this.


>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.

The "easily" part wasn't referring to feasibility, but to how effortless it is to do AND have performance and scalable code.


> 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.


This 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.


> The only difference is it won’t auto-expand like in Go.

Well yeah that’s the difference I was talking about.


>The only difference is it won’t auto-expand like in Go.

Which makes all the difference.


Stacks 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


No, 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.


I 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?


All the discussion was about virtual memory.

The initial thread in a process, on every operating system that is still relevant today, uses guard pages to grow its virtual memory allocation for the stack segment. Physical memory for the stack segment's pages may or may not be mapped into the process address space (it will usually be mapped). This stack can't shrink, and also once the thread touches a page, it becomes commited memory and it will always stay commited.

Other threads in a process do not use guard pages and use fixed virtual memory allocation for their stacks. Their stacks are fixed in size and can't grow. Just like for the initial thread once a page is touched, it becomes commited memory and it will always stay that way.

In Go, stacks start small and use a variable amount of virtual memory. Go stacks can shrink, freeing both address space and physical memory.


> 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?


That is already the case for the cooperative green threads that goroutines currently comprise of.


But the cooperative green threads don't get the advantages of preemption, which is the entire point of the exercise.


> 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.


This 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.


The BEAM will totally interrupt a process that is currently processing a message. It does so based on a 'reduction' count, roughly equivalent to each function call. Since there is no looping construct, any iteration can be interrupted halfway through.

But this means that the VM is entirely allowed to interrupt you halfway through a call to sorting a list obtained through a message. No qualms about it. You reach you function call limit, you're phased out and put back in the queue.

The three ways I know about to hog a scheduler for yourself are:

- write C code that misbehaves and stalls the scheduler (there is now a dirty scheduler if you want the code to run there)

- hardcode a sequence of arithmetic operations (which are not subject to reduction counts) that can take a very long time to execute -- something that never happens naturally in code

- set your process to the max priority and do busy looping while the rest of the system is at a lower priority. Your process will now have a higher priority and prevent others from running

That's about what you can do. It requires a kind of explicit effort to do and is very easy to spot from afar.


"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.)


Erlang's pre-emption points are specifically inside the implementations of the CALL and RET instructions, not on every opcode. It's just that every stack frame effectively takes O(1) runtime before hitting one or the other of those instructions, because of the lack of primitive loop instructions. (Mind you, there are O(N) instructions—there's cryptographic-hashing BIFs, for example—but they're internally reduction-budgeted, so the scheduler can yield from inside them.)

> 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.

Actually, you can't! (Or, well, at least as long as you've given a moment of thought while coding your NIF you can't.)

Avoiding this used to require explicit calls to the reduction budgeting functions in your native code. But as of recent ERTS releases, you just have to mark your NIF as "dirty" and it'll get offloaded to a separate scheduler-thread-pool that exists just to run dirty (i.e. long CPU-time) NIFs. There's no reason, today, to not just mark your NIF as dirty when you start writing it, and then take the dirty flag off only when you're ready to add the calls to the reduction-budgeting logic.

(If you flip it around in your head, having a dirty flag is essentially the same as having a "clean" flag—or, in other words, an "I know what I'm doing, I've guaranteed this has reduction-budgeting" flag. Which is no more and no less than something like Rust's unsafe{} blocks. To say you can freeze a runtime with unsafe{} code shouldn't be surprising ;) What's more surprising is that you can run native code within ERTS that's not unsafe{} in this sense! ...though, as every Erlang maintainer will tell you, "it's a last resort and you would probably have been fine with a port program, which provides much better isolation.")


<citation needed>


> 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.


They should just make it possible to add safeties manually


isn't that what runtime.Gosched does?




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: