Live data from Hacker News

How to think about async/await in Rust

cliffle.com

111–120 of 268 posts

Re: How to think about async/await in Rust

#111
post #50

Earlier quoted context omitted.

In any sensible API, saveToDisk would return an error status (a Result type in Rust). If you don't check for errors, then probably you didn't care of the data was actually saved or not.

Futures in Rust are annotated with the #[must_use] attribute [1], same as the Result type [2]. This means the compiler will emit a warning (can be upgraded to an error) if you forget to await a future even if it doesn't return anything. [1]: https://doc.rust-lang.org/nightly/src/core/future/future.rs.... [2]: https://doc.rust-lang.org/nightly/src/core/result.rs.html#49...

You don't want the safety of your program to depend on whether the compiler emits a warning or not.

And turning warnings into errors just encourages people to write 'let _ = ...' to get rid of the error.

Re: How to think about async/await in Rust

#112

Earlier quoted context omitted.

> Threads are a resource hog. Not really on any decent operating system, but if they are too heavy, there's still fibers aka green-threads aka stack-switching (which at least on Windows are an operating system primitive - but can be implemented in user code on any system that gives you direct access to the CPU stack and registers). I doubt that the async-await state machine code transformation which 'slices' sequenti…

The async/await model gives you exactly one guarantee: because the yield continuation is second class, at most one stack frame can be suspended, so the the amount of space that needs to be reserved for a task is bounded and potentially can be computed statically. This can be important for very high performance/very high concurrency programs, so I think the upsides can be more than the downsides in something like rust…

> I still do not understand why async was deemed appropriate, for example, in python.

My best guess is that it's because of implementation limitations in CPython and likely other interpreters. StacklessPython is a fork of CPython with real coroutines/fibers/green threads but apparently they didn't want to merge that patch. Very disappointing, because async/await is a nearly useless substitute for my desired usecase (embedded scripting languages with pauseable scripts).

Re: How to think about async/await in Rust

#113
post #4

The article shows a great example of how to implement a state machine with internal delays (do something, wait for a defined time, do something else), which is very useful in a driver or embedded context where you often just have to wait for an external device to be ready. However, it doesn't really address how you'd construct a state machine with an external tick. It's pretty common to have a state machine called at…

What is the difference between „call state machine nextStep() with a fixed timer“ vs „call async fns with a delay“?

You mean calling async functions with internal delays? The delay is defined internally to the async function, rather than externally.

The difference between:

  async fn bla() {
     doWork();

     waitForSecs(x);

     doMoreWork();

     waitForSecs(x);

     lastBit();
  }
  
and

  fn somethingElse() {
    // state is persistent
    match state {
      FirstState => doWork(); state = SecondState;
      SecondState => doMoreWork(); state = ThirdState;
      ThirdState => lastBit(); state = Done;
      _ => ()
  }
is that the first example controls the delay period, while in the second example the caller decides the period. The timing of the first example is also dependent on the execution time of the work functions, while the timing of the second is only dependent on the caller.

The benefit of the second example is that it can be completely synchronous with other parts of the system. You know that when your global tick happens, all the state transitions also happen. If each function manages their own time delays, that's not a given.

Re: How to think about async/await in Rust

#114

Not specific to rust, but I think asynchronous programming in general is a hype. It didn't start because it is so awesome, it started because JS can't do parallel any other way. That's the long and short of it. People wanted to use JS in the backend for some reason. The backend requires concurrency. JS cannot do concurrency. Enter the event loop. Then enter some syntactic sugar for the event loop. And since JS is pop…

> We left behind that paradigm in Operating Systems decades ago, and with good reason. I'm curious, what reason? I grew up on Python and C#, and only know async/await, never done real threading (C# async is threading and coroutines under the hood, Python is just coroutines, single-threaded). I find that way of writing code very elegant, as one can encode points of blocking/switching explicitly. A bit like encoding lo…

> I'm curious, what reason?

In days gone by, processes who got to run on the single core that contemporary CPU had available, had to actively relinquish control of the core back to the kernel.

If a single process refused to do so, e.g. because the program hang, there was nothing the kernel could do about it, and the entire OS was blocked. The scheduler never ran, no other process would get CPU time, the whole thing was dead in the water, and all you could do was kick the "Reset" button (if present) or pull the power cord and reboot.

Obviously, this is a very bad situation for an OS, which runs many processes from many sources. And because of that, we ditched this system, and went on to preemptive multitasking, where control is relinquished back to the scheduler after a time whether the process is okay with that or not.

Async basically re-invented that system in userspace. We have an event loop, and we have processes that actively yield control to it. What happens if a subroutine refuses to do so? There is nothing the event loop can do about that.

And it's really easy for this to happen. All it needs is a single synchronous call, say, to an external datastore, somewhere deep down in the callstack, and the awesome throughput of async goes bye bye.

Re: How to think about async/await in Rust

#115

Earlier quoted context omitted.

Well, not really, because async/await guarantees I don't have to deal with the problem of producer adding data at the same time as consumer is removing the data in this case. In a proper SPSC queue some degree of synchronization is needed.

You stop adding data when the queue is full, you stop popping when it is empty. You need the exact same synchronisation for async, just different primitives.

But that's not synchronization between two concurrent things. I can still reason about queue being full in a sequential way.

   select! {
     _ = channel.readable(), if queue.has_free_space() => read(&mut queue),
     _ = channel.writable(), if queue.has_data() => write(&mut queue),
   }
The point is I can implement `has_free_space` and `has_data` without thinking about concurrency / parallelism / threads. I don't need to even think what happens if in the middle of my "has_free_space" check another thread goes in and adds some data. And I don't need to invoke any costly locks or atomic operations there to ensure proper safety of my queue structure. Just purely sequential logic. Which is way simpler to reason about than any SPSC queue.

Re: How to think about async/await in Rust

#116

Earlier quoted context omitted.

Async is, in many situations, better than traditional threads. Threads are a resource hog. They take a lot of system resources, and so you usually want to have as few of them as possible. This is a problem for applications that could, in theory, support thousands of concurrent connections, if not more. With a basic thread-based model, you need 1 thread per connection, and if you have long-lived connections with infre…

> Threads are a resource hog. Not really on any decent operating system, but if they are too heavy, there's still fibers aka green-threads aka stack-switching (which at least on Windows are an operating system primitive - but can be implemented in user code on any system that gives you direct access to the CPU stack and registers). I doubt that the async-await state machine code transformation which 'slices' sequenti…

Userspace fibers (no clue about Windows fibers) still have the blocking IO problem. If your fiber calls read() but there's no data and read blocks for a few minutes, until the next message is received, no other fibers can be scheduled on that thread in the meantime. With async, the task just gets suspended, something like epoll gets called with info about all the suspended tasks, and the thread unblocks once any task can move forward, not necessarily the one that requested the read. This problem doesn't exist if your pseudo threads have first-class language and runtime support, see goroutines for example.

Re: How to think about async/await in Rust

#117

Earlier quoted context omitted.

You stop adding data when the queue is full, you stop popping when it is empty. You need the exact same synchronisation for async, just different primitives.

But that's not synchronization between two concurrent things. I can still reason about queue being full in a sequential way. select! { _ = channel.readable(), if queue.has_free_space() => read(&mut queue), _ = channel.writable(), if queue.has_data() => write(&mut queue), } The point is I can implement `has_free_space` and `has_data` without thinking about concurrency / parallelism / threads. I don't need to even thin…

As I mentioned else thread, if you do not care about parallelism you can pin your threads and use SCHED_FIFO for scheduling and then you do not need any synchronization.

In any case acq/rel is the only thing required here and it is extremely cheap.

edit: in any case we are discussing synchronization and 'has_free_space' 'had_data' are a form of synchronization, we all agree that async and threads have different performance characteristics.

Re: How to think about async/await in Rust

#118

Earlier quoted context omitted.

Async is, in many situations, better than traditional threads. Threads are a resource hog. They take a lot of system resources, and so you usually want to have as few of them as possible. This is a problem for applications that could, in theory, support thousands of concurrent connections, if not more. With a basic thread-based model, you need 1 thread per connection, and if you have long-lived connections with infre…

Isn't that problem generally easily solved with a thread pool ? (that's what nginx does I believe)

There are use cases where a thread pool doesn't solve your problem. If you're handling a few short-lived connections at a time, it's more than enough, but if you're developing something like a push / messaging / queuing service, with thousands of clients connected for hours at a time and receiving very little data once every few minutes, a thread pool won't help you.

Re: How to think about async/await in Rust

#119

Not specific to rust, but I think asynchronous programming in general is a hype. It didn't start because it is so awesome, it started because JS can't do parallel any other way. That's the long and short of it. People wanted to use JS in the backend for some reason. The backend requires concurrency. JS cannot do concurrency. Enter the event loop. Then enter some syntactic sugar for the event loop. And since JS is pop…

My opinion is the opposite, to the point I would argue that anyone advocating for multithreading for reasons other than executing things in parallel on different cores is extremely dangerous and shouldn't be allowed anywhere near a serious codebase.

I probably wouldn't go as far as saying "extremely dangerous" but I do agree. For the most popular use case (i.e. web services) a thread per core with an event loop in each thread is the best model.

Re: How to think about async/await in Rust

#120

Earlier quoted context omitted.

The async/await model gives you exactly one guarantee: because the yield continuation is second class, at most one stack frame can be suspended, so the the amount of space that needs to be reserved for a task is bounded and potentially can be computed statically. This can be important for very high performance/very high concurrency programs, so I think the upsides can be more than the downsides in something like rust…

> I still do not understand why async was deemed appropriate, for example, in python. My best guess is that it's because of implementation limitations in CPython and likely other interpreters. StacklessPython is a fork of CPython with real coroutines/fibers/green threads but apparently they didn't want to merge that patch. Very disappointing, because async/await is a nearly useless substitute for my desired usecase (…

There is also gevent which is a library only coroutine extension which didn't require any changes to the interpreter itself. I'm also sure it would be easier to maintain and evolve if it was part of python core.
Post reply on HN