Live data from Hacker News

What async promised and what it delivered

causality.blog

291–300 of 317 posts

Re: What async promised and what it delivered

#291

Earlier quoted context omitted.

> Once you write enough code, you'll realize you need synchronization primitives for async code as well. In pretty much the same cases as threaded code. I've been programming for 30 years, including over a decade in JS. You need sync primitives in JS sometimes, but they're trivial to write in javascript because the code is run single threaded and there's no preemption. > What you're trying to do may require IO Its us…

Ok, you've been programming for years. But didn't learn a lot about threads, apparently. > Multithreaded code is often much harder to reason about than async code, because threads can interleave executions and threads can be preempted anywhere. No, green threads / fibres or whatever you want to call them explicitly don't interleave executions. They are a form of cooperative multitasking. Async/await is another form o…

> No, green threads / fibres or whatever you want to call them explicitly don't interleave executions.

If you use them with a multithreaded executor (eg in Go), of course they interleave executions. I suppose all your green threads / fibers could run on a single CPU core. But what's the point? How would that be an improvement over what we have now?

I suppose you could make something similar to async/await but with a yield() operation whenever a call wants to block. This would allow blocking read() and so on. But its basically async/await but without declaring functions as async. And without needing to explicitly await. Await points would be implicit and invisible. But if you do that, any function call you make could yield before returning. As a result, you could no longer easily reason about interleaving. I call foo(). Does it yield to other threads before returning? I have no idea. I could read the code of foo(), but maybe foo will change between minor versions of the library.

This would lead to an avalanche of bugs. Lots of javascript code quietly depends on the lack of interleaving for correctness. Javascript guarantees that while my (non async) function runs, no other code gets executed. Adding threads, even if its via cooperative multitasking, would break that invariant. It would break all sorts of programs which are working correctly today.

> The latter re-invents the universe. [...] Thinking about it, the change is possibly smaller than language changes required by async/await.

Did you write much javascript before async/await and before promises? Javascript at the time was already async. We just implemented async execution through callbacks. ("Callback hell"). Over time, functions tended to go down and to the right. Promises were added as 3rd party libraries. Then promises were standardised. And later, async/await was added as syntax to help you work with promises. Async / await in javascript was an incremental change to give us new syntax to do what we were already doing. JS already had an event loop and promises. Async/await just added syntax.

Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

> The only difference is that one of them clutters your syntax and the other doesn't.

One of them is explicit about where and when a thread blocks. Whether or not something is "blocking" (async) is part of the API. Threading (incl cooperative threading) hides this information. Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.

Re: What async promised and what it delivered

#292

Earlier quoted context omitted.

> Operating systems can grow the amount of real memory allocated to a thread, but never shrink it. Operating systems can shrink the memory usage of a stack. madvise(page, size, MADV_DONTNEED); Leaves the memory mapping intact but the kernel frees underlying resources. Subsequent accesses get either new zero pages or the original file's pages. Linux also supports mremap, which is essentially a kernel version of reallo…

Stack memory is never unmapped until the thread terminates as far as I know. I don’t know of any kernel that does this, for precisely the reason you arrive at by the very last sentence.

It's just normal pages to the kernel. In theory, it's totally possible for the program to munmap some of its own stack's pages if it was sophisticated enough. Typical C programs just aren't capable of it, at least not without great effort.

Re: What async promised and what it delivered

#293

Earlier quoted context omitted.

> Operating systems can grow the amount of real memory allocated to a thread, but never shrink it. Operating systems can shrink the memory usage of a stack. madvise(page, size, MADV_DONTNEED); Leaves the memory mapping intact but the kernel frees underlying resources. Subsequent accesses get either new zero pages or the original file's pages. Linux also supports mremap, which is essentially a kernel version of reallo…

> C programs can't do it because pointers to stack allocated objects may exist. They sure shouldn't exist to the unused region of the stack though; if they do, that's a bug (because anything could claim that memory now). You should be free and clear to release stack pages past your current stack pointer.

High level languages have entire runtime systems dedicated to managing resources like that. My language can allocate, grow, shrink and deallocate stacks dynamically. It has complete visibility into everything, and the stacks themselves are designed to be relocatable and position-independent.

In C it's impossible to even get the stack pointer without dropping to assembly or using compiler builtins. It's hard to know where the stack starts or even how big it is.

Re: What async promised and what it delivered

#294

Earlier quoted context omitted.

Ok, you've been programming for years. But didn't learn a lot about threads, apparently. > Multithreaded code is often much harder to reason about than async code, because threads can interleave executions and threads can be preempted anywhere. No, green threads / fibres or whatever you want to call them explicitly don't interleave executions. They are a form of cooperative multitasking. Async/await is another form o…

> No, green threads / fibres or whatever you want to call them explicitly don't interleave executions. If you use them with a multithreaded executor (eg in Go), of course they interleave executions. I suppose all your green threads / fibers could run on a single CPU core. But what's the point? How would that be an improvement over what we have now? I suppose you could make something similar to async/await but with a…

> I suppose all your green threads / fibers could run on a single CPU core.

yes.

> But what's the point? How would that be an improvement over what we have now?

> But its basically async/await but without declaring functions as async.

You answered your own question: yes, you get what you have now, without all the overhead of async, await, promises and futures.

> But if you do that, any function call you make could yield before returning.

A green thread could be an instance of a particular type, so `input = self.yield()` would fail if you aren't a green thread. So no, not "any function" - just ones that instances of a green thread, or are passed a reference to one.

> Does it yield to other threads before returning?

It could if you pass it an instance to a green thread, otherwise it can't.

> This would lead to an avalanche of bugs.

It doesn't. Cooperative multitasking is at least 1/2 a century old at this point. The bugs you're imagining will happen mostly aren't an issue. To the extent they do happen, it's because someone hasn't thought about two control flows modifying the same data structure. Yes, that happens, but it happens in all single threaded code - async included. It's why we hate side effects. It's what Rust famously prevents with its borrow checker even in the face of side effects. It's not avoided by async. The explicit colouring does not help to prevent it - it's just overhead.

FWIW the one issue cooperative multitasking does often introduce is that they can take a long time to execute, so other cooperative tasks don't run in a timely fashion. Exactly the same thing can happen with async of course. It's not usually a problem in browsers, but in embedded solutions where cooperative multitasking is commonly used, it's a real issue because they are often real time. Ask me how I know.

> Javascript guarantees that while my (non async) function runs, no other code gets executed.

This remains true. You are getting confused by your mental model of threads as a form of concurrency. There is no concurrency going on there. Semantically it is near identical to async / await. The principle difference is in async / await, the program is explicitly creating each stack frame on the heap using manually allocated objects. In addition to the mental overhead that creates, it slower than using a real stack like green threads do. But now for the truly bizarre twist. Can you guess how modern javascript engines get around that speed issue? Wait for it .... they create an explicit stack ... that looks like what green threads would use anyway! And as a wonderful side effect - you get real stack back traces again. The irony is almost palpable. https://v8.dev/blog/fast-async

> Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

I agree the ship has sailed at this point. The rest of the assertions you make there are wrong.

This assertion stands out: frozen websites. Can you tell me how they are going to block? There are no blocking calls in javascript now. The things you would await on now would be passed a green thread handle. But the javascript scripts events called from the DOM have no green-thread handle, so they can't block.

> Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.

You don't. You've just been conditioned to think that because you've never done it any other way. But the reality is people have been using cooperative multitasking for a long, long time. It pre-dates threads and async. The issues and bugs you are proclaiming would happen don't arise.

Re: What async promised and what it delivered

#295

Earlier quoted context omitted.

> C programs can't do it because pointers to stack allocated objects may exist. They sure shouldn't exist to the unused region of the stack though; if they do, that's a bug (because anything could claim that memory now). You should be free and clear to release stack pages past your current stack pointer.

There isn’t any operating system or compiler that does this today, and it probably isn’t worth it to pursue. Enlarging the stack via page fault is really expensive, so you would need really advanced heuristics to prevent repeatedly unmapping/remapping those pages. The correct tool for myriad of small tasks is coroutines / green threads / async tasks, so why spend any energy optimizing threads for that purpose instead…

In the general case it's absolutely not worth it. In the context of "you want a large number of OS threads, and are willing to go to some effort", it's theoretically something you'd want to do; suppose the startup for a thread is measurably a high water mark for stack usage, after startup the steady state stack usage won't exceed 20% of that high mark, and you'd like as many threads/stacks as possible.

Coroutines / green threads / async tasks will all do this too, but there's something to be said for using/relying on the system scheduler instead of bringing your own in in addition.

Re: What async promised and what it delivered

#296

Earlier quoted context omitted.

> C programs can't do it because pointers to stack allocated objects may exist. They sure shouldn't exist to the unused region of the stack though; if they do, that's a bug (because anything could claim that memory now). You should be free and clear to release stack pages past your current stack pointer.

High level languages have entire runtime systems dedicated to managing resources like that. My language can allocate, grow, shrink and deallocate stacks dynamically. It has complete visibility into everything, and the stacks themselves are designed to be relocatable and position-independent. In C it's impossible to even get the stack pointer without dropping to assembly or using compiler builtins. It's hard to know w…

I do agree with this, but just to be clear (for others), you don't need any runtime managing resource lifecycles to know that there shouldn't be pointers into free memory, such as the currently unused portion of the stack.

Re: What async promised and what it delivered

#298
post #232

Earlier quoted context omitted.

You wrote: > The job wouldn’t have been done. They would have needed threads. And mutexes. And spin locks. And atomics. And semaphores. And message queues. And - in my opinion - the result would have been a much worse language. My point is that you do need mutexes, spin locks, etc with async as well, given that you have a multi threaded platform. So no, we have basically 2x2 stuff we are talking about with very diffe…

> My point is that you do need mutexes, spin locks, etc with async as well, given that you have a multi threaded platform. No, Javascript isn't a shared-memory multi threaded platform. It would only need mutexes and so on if we added threads to javascript, as the comment I was replying to suggested should have happened: > At any step in that sequence, the language could have introduced green threads and the job would…

That's why I brought up C# as an example of a language with multi threaded async. But Kotlin is another example.

Re: What async promised and what it delivered

#299
I think that Java has a really good solution to offer: Virtual Threads. They share the same interface with native threads and free developers from the burdens of async programming. You only need to take care of some things like accidental thread pinning (through legacy code) and the use of ThreadLocal, but otherwise this complexity is hidden.

https://docs.oracle.com/en/java/javase/21/core/virtual-threa...

Re: What async promised and what it delivered

#300
post #266

Earlier quoted context omitted.

You assumed > Likely more efficient than half the async runtimes out there. The benchmark shows the opposite: 2 (multithreaded async runtime) vs 7 (threads) * 10^8 ns per request for 2k requests/s. > non-linear scaling problems. oh, look closely, the relative gap increases with #requests/s

By TFA's own quote: "This means you probably shouldn’t put too much trust into the results I measured or base important decisions on the outcomes." Your level of self-assured snark is way out of proportion to both the article and chart I'm looking at. Props to the person who wrote the article. Your use of it however, is dubious.

It's more appropriate than your snarks based on no data
Post reply on HN