Live data from Hacker News

Async Rust never left the MVP state

tweedegolf.nl

211–220 of 273 posts

Re: Async Rust never left the MVP state

#211

Async seems like an underbaked idea across the board. Regular code was already async. When you need to wait for an async operation, the thread sleeps until ready and the kernel abstracts it away. But We didn’t like structuring code into logical threads, so we added callback systems for events. Then realized callbacks are very hard to reason about and that sequential control is better. So threads was the right program…

> Now language runtimes prefer “green threads” for portability and performance "Green threads" only exist in crappy interpreted languages, and only because they have stop-the-world single-threaded garbage collection.

Go and Java both have green threads, and are not interpreted nor limited to single threaded GC.

Re: Async Rust never left the MVP state

#212
post #64

Earlier quoted context omitted.

There is work happening on keyword generics[0], which would let a function be generic over keywords like `async` and `const`. For now the best option to write code that wants to live in both worlds is sans-io. Thomas Eizinger at Fireguard has written a good article about this[1] pattern. Not only does it nicely solve the sync/async issue, but it also makes testing easier and opens the door to techniques like DST[2] I…

Considering the latest commits and issues in effects-initiative are about 2 years old, the keyword generics initiative seems effectively dead.

Rust uses Zulip for lang-related discussions. The 't-lang/effects' channel is still somewhat active.

Re: Async Rust never left the MVP state

#213
post #167

Earlier quoted context omitted.

> Regular code was already async. When you need to wait for an async operation, the thread sleeps until ready and the kernel abstracts it away Not really. I’ve observed async code often is written in such a way that it doesn’t maximize how much concurrency can be expressed (eg instead of writing “here’s N I/O operations to do them all concurrently” it’s “for operation X, await process(x)”). However, in a threaded wor…

> work stealing executors have long been known to offer significantly lower latency with more consistent P99 than traditional threads. This has been known since forever - in the early 00s Well, we know how to make "traditional threads" fast, with lower latency and more consistent P99 since forever^2, in the early 90s. [1] Sure, we can't convince that Finnish guy this is worthwhile to include in THE kernel, despite si…

There are always trade-offs and there is never one best way to do something.

Stack-based coroutines is one way to do it. A relevant trade-off here is overhead, requiring a runtime and narrowing the potential use-cases this can serve (i.e embedded real-time stuff).

If you don’t care about supporting such use cases you can of course just create a copy of goroutines and be pretty happy with the result.

Re: Async Rust never left the MVP state

#214

Earlier quoted context omitted.

It's very much possible to use rust for a lot of areas with async without needing to be dependent on tokio. I think it's really just the web/server stuff that's entirely tokio dependent. Writing libraries to be executor agnostic is not terribly difficult but does require some diligence which isn't necessarily present in most of the community.

It really depends on the abstraction model of the library. If the library needs to actually read/write a file, it either needs to depend on a runtime or provide some horrific abstraction over the process it will use to do that. This doesn't apply to sync IO libraries which can just use the Standard Library. Web/server frameworks have to bind to a runtime because they have to make decisions about how to connect to a s…

That's the thing though, it's possible but it makes the simple hello world example more tedious. It's totally possible to make an abstraction layer, provide a tokio implementation out of the box but leave the door open for other implementations to slot in. Anyone who's written portable code for non posix systems is used to this experience. Standardization is definitely better but it also has its own share problems as it can limit what's possible. I expect that the decision to delay standardizing on these interfaces too early will end up leading to a better long term design. Especially if major improvements to async are on the horizon and can alter the final shape of that standard.

Re: Async Rust never left the MVP state

#215

Earlier quoted context omitted.

> So threads was the right programming model. It depends on what you are doing. Threads are the right model for compute-bound workloads. Async is the right model for bandwidth-bound workloads. Optimization of bandwidth-bound code is an exercise in schedule design. In a classic multithreading model you have limited control over scheduling. In an async model you can have almost perfect control over scheduling. A well-o…

If this is a classic exercise can you show me the material? Why can’t a scheduler be written which optimizes around IO? What additional information is present in code that has async/await annotations?

Threads are a scheduling model that delegates to the OS scheduler. Async style provides a primitive for creating a custom scheduler but is not a scheduler per se.

To use a custom scheduler you must first disable the existing schedulers your code is using by default for both execution and I/O. That means no OS scheduling. Thread-per-core architectures with static allocation and direct userspace I/O is the idiomatic way to do this regardless of programming language.

Optimal scheduling is a profoundly intractable problem -- it is AI-Complete. A generic scheduler is always going to be deeply suboptimal because a remotely decent schedule isn't practically computable in real systems. A more optimal scheduler must continuously rewrite the selection and ordering of thousands of concurrent operations in real-time. Importantly, this dynamic schedule rewriting is based on a model that can see across all operations globally and accurately predict both future operations that haven't happened yet and any ordering dependencies between current and future operations. A modern system can handle tens of millions of these operations per second, so the scheduling needs to be efficient.

A generic scheduler has to allow for almost arbitrary operation graphs and behavior. However, if you are writing e.g. a database engine, you have almost the entire context of how operations relate to each other both concurrently and across time. The design of a somewhat optimal scheduler that only understands your code becomes computationally feasible. It isn't trivial -- scheduler design is properly difficult -- but you build it using async style.

Re: Async Rust never left the MVP state

#216

I recently started working with Rust async. The main issue I am currently facing is code duplication: I have to duplicate every function that I want to support both asynchronous and blocking APIs. This could be great to have a `maybe-async`. I took a look at the available crates to work around this (maybe-async, bisync), but they all have issues or hard limitations.

In my perspective, an "async" function is already an "maybe-async". The distinction between a a `fn -> void` and `fn -> Future` is that the former executes till its end immediately, whereas the other may only finish at another time. If you want to run an async fn in a blocking manner, you would use a blocking executor.

Re: Async Rust never left the MVP state

#217

Earlier quoted context omitted.

The most promiment example is probably Go with its goroutines, but there are so many more. You can easily spawn tens of thousands of goroutines, with low overhead and great performance.

Goroutines/"fibers"/"green threads" are usually scheduled by the runtime system across a small pool of actual OS threads.

The word "thread" is confusing things. In computer science a thread represents a flow of execution, which in concrete terms where execution is a series of function calls, is typically a program counter and a stack.

There are many ways to implement and manage threads. In Unix-like and Windows systems a "thread" is the above, plus a bunch of kernel context, plus implicit preemptive context switching. Because Unix and Windows added threads to their architectures relatively late in their development, each thread has to behave sort of like its own process, capable of running all the pre-existing software that was thread-agnostic. Which is why they have implicit scheduling, large userspace stacks, etc.

But nothing about "thread" requires it to be implemented or behave exactly like "OS threads" do in popular operating systems. People wax on about Async Rust and state machines. Well, a thread is already state machine, too. Async Rust has to nest a bunch of state machine contexts along with space for data manipulated in each function--that's called a stack. So Async Rust is one layer of threading built atop another layer of threading. And it did this not because it's better, but primarily because of legacy FFI concerns and interoperability with non-Rust software that depended on the pre-existing ABIs for stack and scheduling management.

Go largely went in the opposite direction, embracing threads as a first-class concept in a way that makes it no less scalable or cheap than Rust Futures, notwithstanding that Go, too, had to deal with legacy OS APIs and semantics, which they abstracted and modeled with their G (goroutine), M (machine), P (processor) architecture.

Re: Async Rust never left the MVP state

#218

Earlier quoted context omitted.

I'm not prominent but I disagreed with it at the time and I was wrong.

I’m curious - why were you wrong? It still seems like a wart to me, all these years later. What am I missing?

I'll give my two cents here. I work with Dart daily, and it also uses the `await future` syntax. I can cite a number of ergonomic issues:

```dart (await taskA()).doSomething() (await taskB()) + 1 (await taskC()) as int ```

vs.

```rust taskA().await.doSomething() taskB().await + 1 taskC().await as i32 ```

It gets worse if you try to compose:

```dart (await taskA( (await taskB( (await taskC()) as int )) + 1) ).doSomething() ```

This often leads to trading the await syntax for `then`:

```dart await taskC() .then((r) => r as i32) .then(taskB) .then((r) => r + 1) .then(taskA) .then((r) => r.doSomething()) ```

But this is effectively trading the await structured syntax for a callback one. In Rust, we can write it as this:

```rust taskA(taskB(taskC().await as i32).await + 1).await.doSomething() ```

Re: Async Rust never left the MVP state

#219

Earlier quoted context omitted.

I’m curious - why were you wrong? It still seems like a wart to me, all these years later. What am I missing?

I'll give my two cents here. I work with Dart daily, and it also uses the `await future` syntax. I can cite a number of ergonomic issues: ```dart (await taskA()).doSomething() (await taskB()) + 1 (await taskC()) as int ``` vs. ```rust taskA().await.doSomething() taskB().await + 1 taskC().await as i32 ``` It gets worse if you try to compose: ```dart (await taskA( (await taskB( (await taskC()) as int )) + 1) ).doSometh…

Two spaces before a line make it a code block literal

  This is a code block
HN has never used markdown so the triple-tick does nothing but create noise here.

Re: Async Rust never left the MVP state

#220

Earlier quoted context omitted.

If this is a classic exercise can you show me the material? Why can’t a scheduler be written which optimizes around IO? What additional information is present in code that has async/await annotations?

Threads are a scheduling model that delegates to the OS scheduler. Async style provides a primitive for creating a custom scheduler but is not a scheduler per se. To use a custom scheduler you must first disable the existing schedulers your code is using by default for both execution and I/O. That means no OS scheduling. Thread-per-core architectures with static allocation and direct userspace I/O is the idiomatic wa…

That’s not what I asked.
Post reply on HN