Live data from Hacker News

What async promised and what it delivered

causality.blog

111–120 of 317 posts

Re: What async promised and what it delivered

#111
post #59

Earlier quoted context omitted.

> async/await came out of C# (well at least the JS version of it). Not sure if inspired by it, but async/await is just like Haskells do-notation, except specialized for one type: Promise/Future. A bit of a shame. Do-notation works for so many more types. - for lists, it behaves like list-comprehensions. - for Maybes it behaves like optional chaining. - and much more... All other languages pile on extra syntax sugar f…

I knew someone was going to bring up monads that's why I put JS version :) JS took the C# syntax.

Similarly F#'s computation expressions predate C#'s syntax, and there is some evidence that C# language designers were looking at F#'s computation expressions. Since the Linq work, C# has been very aware of Monads, and very slow and methodical about how it approaches them. Linq syntax is a subtly compromised computation expression and async/await is a similar compromise.

It's interesting to wonder about the C# world where those things were more unified.

It's also interesting to explore in C# all the existing ways that Linq syntax can be used to work with arbitrary monads and also Task can be abused to use async/await syntax for arbitrary monads. (In JS, it is even easier to bend async/await to arbitrary monads given the rules of a "thenable" are real simple.)

Re: What async promised and what it delivered

#112
post #100
post #39

How many systems are there that can't just spawn a thread for each task they have to work on concurrently? This has to be a system that is A) CPU or memory bound (since async doesn't make disk or network IO faster) and B) must work on ~tens of thousands of tasks concurrently, i.e. can't just queue up tasks and work on only a small number concurrently. The only meaningful example I can come up with are load balancers,…

Pretty much anything that needs performance and has a lot of relatively light operations is not a candidate for spawning a thread. Context switching and the cost of threads is going to kill performance. A server spawning a thread per request for relatively lightweight request is going to be extremely slow. But sure, if every REST call results in a 10s database query then that's not your bottleneck. A query to a datab…

Server applications don’t spawn threads per request, they use thread pools. The extra context switching due to threads waiting for I/O is negligible in practice for most applications. Asynchronous I/O becomes important when the number of simultaneous requests approaches the number of threads you can have on your system. Many applications don’t come close to that in practice.

There’s a benefit in being able to code the handling of a request in synchronous logic. A case has to be made for the particular application that it would cause performance or resource issues, before opting for asynchronous code that adds more complexity.

Re: What async promised and what it delivered

#113
post #42

Async ruined Rust for me, even though I write exactly the kind of highly concurrent servers to which it's supposed to be perfectly suited. It degrades API surfaces to the worst case :Send+Sync+'static because APIs have to be prepared to run on multithreaded executors, and this infects your other Rust types and APIs because each of these async edges is effectively a black hole for the borrow checker. Don't get me star…

Its interesting how there are so many ways to do various types of concurrency in rust (Hardware-dependent) that don't introduce coloring:

  - Hardware interrupts
  - Threads
  - Thread pools / Rayon
  - GPU (Cudarc, FFI, WGPU etc)
  - SIMD
  - Write firmware for multiple cores
I make use of all of those, but still prefer avoiding Async, for the typical coloring reason. I can integrate the above things into a code base with low friction; Async poses a compatibility barrier.

Re: What async promised and what it delivered

#114

Earlier quoted context omitted.

That will help locally, but for a published API or a callback function where you don't know the callers, it's still going to break people if you change a union type. It doesn't matter if it's inferred or not.

IIRC ZIO solution is actually to return a generic E :> X|Y. Caller providing the callback knows what else is on E, and they're the only one that knows it so only they could've handled it anyway. You still get type inference. Or if you mean that returning a new error breaks API compatibility, then yes that's the point. If now you can error in a different way, your users now need to handle that. But if it's all generic…

If you declare specific error types and callers only write handlers for specific cases, then adding a new error breaks them. If you just declare a base error type in your API, they have to write a generic error handler or it doesn't type check.

In this way, declaring a type guides people to write calling code that doesn't break, provided you set it up that way. It makes things easier for the implementation to change.

Sometimes you do need handlers for specific errors, but in Go you always need to write generic error handling, too.

(A type variable can do something similar. It forces the implementation to be generic because the type isn't known, or is only partially known.)

Re: What async promised and what it delivered

#115
Having lived through the changes from callback hell, early promises and then async/await I only ever found each step an improvement and the negatives are very minor when actually working with them.

Now function colouring is interesting but not for the reason these articles get excited. Recolouring is easy and has basically no impact on code maintenance. BUT if you need that code path to really fly then marking it as async is a killer, as all those tiny little promises add tiny delays in the form of many tasks. Which add up to performance problems on hot code paths. This is particularly frustrating if functions are sometimes async, like lazy loaders or similar cache things. To get around this you can either use callbacks instead or use selective promise chaining to only use promises when you get a promise. Both strategies can be messy and trip up people who don’t understand these careful design decisions.

One other fun thing is indexeddb plays terribly with promises, as it uses a “transactions close at end of task” mechanism, making certain common patterns impossible with promises due to how they behave with the task system. Although some API designers have come up with ways around this to give you promise interfaces for databases. Normally by using callbacks internally and only doing one operation per transaction.

Re: What async promised and what it delivered

#116

Having lived through the changes from callback hell, early promises and then async/await I only ever found each step an improvement and the negatives are very minor when actually working with them. Now function colouring is interesting but not for the reason these articles get excited. Recolouring is easy and has basically no impact on code maintenance. BUT if you need that code path to really fly then marking it as…

> all those tiny little promises add tiny delays in the form of many tasks.

That depends on the language/framework. In some languages, `await foo()` is equivalent to `Future f = foo(); await f`. In others (e.g. Python), it's a primitive operation and you have to use a different syntax if you want to create a future/task. In Trio (an excellent Python alternative to asyncio), there isn't even the concept of a future at all!

Re: What async promised and what it delivered

#117
post #98

Async is a Javascript hack that inexplicably got ported to other languages that didn't need it. The issue arose because Javascript didn't have threads, and processing events from the DOM is naturally event driven. To be fair, it's a rare person who can deal with the concurrency issues threads introduce, but the separate stacks threads provide a huge boon. They allow you to turn event driven code into sequential code.…

> At any step in that sequence, the language could have introduced green threads and the job would have been done. 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. Multithreaded code is often much harder to reason about than async code, because threads c…

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.

You can't always choose to write straight code. What you're trying to do may require IO, and then that introduces concurrency, and the need for mutual exclusion or notification.

Examples: If there's a read-through cache, the cache needs some sort of lock inside of it. An async webserver might have a message queue.

The converse is also true. I've been writing some multithreaded code recently, and I don't want to or need to deal with mutexes, so, I use other patterns instead, like thread locals.

Now, for sure the async equivalents look and behave a lot better than the threaded ones. The Promise static methods (any, all, race, etc) are particularly useful. But, you could implement that for threads. I believe that this convenience difference is more due to modernity, of the threading model being, what 40, 50, 60 years old, and given a clean-ish slate to build a new model, modern language designers did better.

But it raises the idea: if we rethought OS-level preemptible concurrency today (don't call it threads!), could we modernize it and do better even than async?

Re: What async promised and what it delivered

#119

Earlier quoted context omitted.

I have some test code that runs a comparison of Hyper pre-async (aka thread per request) vs async (via Tokio), and the pre-async version is able to process more requests per second in every scenario (I/o, CPU complex tasks, shared memory). I'll publish my results shortly. I did these as baselines because I'm testing finishing the User Managed Concurrency Groups proposal to the linux kernel which is an extension to pr…

How many concurrent requests?

I'll have to check my work computer on Monday. It was 8 cpu virtual machine on a m1 Mac. the UMCG and normal threads were 1024 set on the server, the Tokio version was 2 threads per core. Just from the top of my head - the I/O bound requests topped out around 40k/second for the Tokio version, 60k/second for the normal hyper version, and 80k/second for the UMCG hyper version.

I'm pretty close to being done - I'm hoping to publish the entire GitHub repository with tests for the community to validate by next week.

UMCG is essentially an open source version of Google Fibers, which is their internal extension to the linux core for "light weight" threads. It requires you to build a user space scheduler, but that allows you to create different types of schedulers. I can not remember which scheduler showed ^ results but I have at least 6 different UMCG schedulers I was testing.

So essentially you get the benefits of something like tokio where you can have different types of schedulers optimized for different use cases, but the power of kernel threads which means easy cancellation, easy programming (at least in rust). It's still a linux thread with an entire 8mb(?) stack size, but from my testing it's far faster than what Tokio can provide, without the headache of async/await programming.

Re: What async promised and what it delivered

#120
post #106

> OS threads are expensive: an operating system thread typically reserves a megabyte of stack space Why is reserving a megabyte of stack space "expensive"? > and takes roughly a millisecond to create I'm not sure where this number is from, it seems off by a few orders of magnitude. On Linux, thread creation is closer to 10 microseconds.

> Why is reserving a megabyte of stack space "expensive"? Because if you use one thread for each of your 10,000 idle sockets you will use 10GB to do nothing. So you'll want to use a better architecture such as a thread pool. And if you want your better architecture to be generic and ergonomic, you'll end up with async or green threads.

> you will use 10GB to do nothing.

You don't pay for stack space you don't use unless you disable overcommit. And if you disable overcommit on modern linux the machine will very quickly stop functioning.

Post reply on HN