Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Was spinning up a bunch of OS threads not an acceptable solution for the majority of situations? Could we have explored solutions more like Go, where a language-provided runtime makes blocking more of an acceptable thing to do?

I think this is the real fundamental disagreement here (well, at least with the async stuff). (And the comments slightly earlier, regarding "the color of your function".)

The way I think about the various async stories out there (like, all of them, Go's, Rust's, Python's, C w/ OS threads), is that they boil down to roughly three or so primitives: (I haven't formalized this … and someday I should probably write a blog post on it, so it's not going to be perfect.)

  1. separate executions of code (threads, green or not, goroutines, etc.)
  2. selection (the ability to block on **multiple** threads, simultaneously,
     but return after *one* is finished or ready. It's not join().)
  3. cancellation (the ability to interrupt and cancel a thread)
"Was […] OS threads not an acceptable solution?" No: the selection story is terrible (without involving epoll. You'll pretty much have to build this out around conditions, and it's painful), and the cancellation story is close to non-existent (you have to somehow transmit to the thread that it should cancel, and then that thread needs to obey it). In combination, some things are outright impossible: if you're blocking on a socket recv in a thread, how do you get notified if you were cancelled? (You literally can't, with the primitives that most OSes offer, without resorting to hacks, or epoll.) (Some OSes have calls to kill threads. They are basically one-way tickets to UB.)

Golang's solutions get closer, but I don't think they're appropriate for a systems language that cares about low-level performance & memory layout to the degree that Rust does.¹ But even golang struggles here: the cancellation story is missing. Golang's approach here is retroactive: you have a context object, and you must thread it through function calls to anywhere it might be required. If your API didn't think to do that, you're SOL as a consumer, but worse, even as a coder, adding it is going to be a right PITA.

This isn't to say Rust's story is perfect here, either: it is tempting to think that dropping a future is cancellation, and it is very, very close (it provides a decent default). But sometimes cancellation also needs to propagate across an I/O boundary (usually, across a network socket to a server, to cancel a request) and those are async. (I think there is some work being done in Rust with asynchronous drops.)

These problems show up in JavaScript — whose Promise lacks cancellation — and Python (which has cancellation via raising CancelledError; also, Python really, IMO, messed up the terminology. Between futures, coroutines, tasks, and awaitables, there's ~2 real types (futures and tasks), and the rest are … IDK, weird distinctions that are hard to keep straight.)

¹Now, this is the heart of the other part of the article, the half about closures and fn types. Rust cares about ownership (as it allows programs to avoid not just memory issues, but a whole host of bugs resulting from ownership confusion that happen even in memory-safe languages like Python) and it cares about the performance implications of things like dynamic dispatch, or separate allocations for the data associated with the callback that you want to pass. And between trait types & generics, it lets you choose, but that does result in some verbosity. You can also choose Arc, but some of us don't want that in all cases.

(I don't agree with the "color of your function" argument/article, either: async functions in Rust are fundamentally still functions, they just return a Future<T> instead of a T. That distinction is important; we're not just after the T, and the future is something we're going to block/wait on; it is its own type, with its own operations — specifically, and more or less, the ones above! —, that are crucial to how the code functions.)



With standard library support, green threads can be just fine. I've worked with many, many C programs that were nbio and green threads. Cancellation is trivial when you are writing the scheduler! In these cases, I used multiprocessing when I needed parallelism. The answer to "If you're blocking on a socket recv in a thread..." is to not ever do that; the library should provide a green recv that appears to be blocking to the code, but is implemented with non-blocking calls underneath. With the privilege of being the standard library, there are many ways to enforce this.

N to M is much harder to get cancellation right, but again if you write the scheduler, you can define the cancellation points, so you are not beholden to undefined behavior. Instantly cancelling a thread is just not solvable, and for Rust, which cares about low-level performance &c. then long-running computations could explicitly check for cancellation when it is needed. Threads that perform I/O regularly will naturally hit cancellation points.

Cancellation is a huge problem in concurrency, and the usual answer to "how do I cancel a thread" is "you don't want to cancel a thread" which is rather unsatisfying. As you point out, it's not like using async and/or promises automatically solves the problem either; the only way to get it right is to build it in from the start.


Rust async doesn't quite solve the cancellation problem either.

Sure, you can stop polling the future at any time, but that doesn't mean that you left the system state consistent.

Careful thought is needed on every call to await in an async function. It might be the last thing that async function ever does.

That's probably something that should be true in a well-implemented system anyway. After all, processes could be killed at any point.




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

Search: