News for hackers. Stuff that matters.
Re:Comparison of Rust async and Linux thread context switch ... by masklinn on Friday February 12, 2021 @01:19AM (#26111191)
> So involving "async" is totally the wrong tool for the job.

Sadly with so many things having gone async-first (or only) it’s become difficult not to end up with an async runtime anyway, or not be forced to use an async system. I wanted to build a small web-based tool for local, didn’t really find anything which was not async.

Re:Comparison of Rust async and Linux thread context switch ... by josephg on Friday February 12, 2021 @02:13AM (#26111368)
I’d happily take an async-by-default world over a world where some APIs only exist through blocking calls. A classically threaded program can easily block on a future, but wrapping a blocking call in an otherwise asynchronous program is complicated, expensive and error prone work.
Re:Comparison of Rust async and Linux thread context switch ... by Ciantic on Friday February 12, 2021 @04:46AM (#26112010)
Btw, have you read this: https://async.rs/blog/stop-worrying-about-blocking-the-new-a... async-std allows to run blocking calls without hoops rather efficiently:

   async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
       std::fs::read_to_string(path)
   }
It doesn't have await inside! My mind was blown as I saw that.
Re:Comparison of Rust async and Linux thread context switch ... by Freaky on Friday February 12, 2021 @10:28AM (#26114660)
Read the first sentence.

> This blog post describes a proposed scheduler for async-std that did not end up being merged for several reasons.

I don't think it's a particularly good idea in the first place - it's basically an automatic watchdog-driven block_in_place(). It doesn't remove the problem of blocking in futures, it just limits the damage to the local task rather than blocking the entire executor.

That's fine in the simple case of future-per-task, but it's pretty common to be polling multiple futures concurrently within one, so it's not a general solution.

Re:Comparison of Rust async and Linux thread context switch ... by masklinn on Friday February 12, 2021 @05:09AM (#26112097)
> should a task execute for too long, the runtime will automatically react by spawning a new executor thread taking over the current thread’s work.

That is a super interesting strategy, though obviously only works when you can « afford » a multithreaded scheduler.

Anyway I wonder how they manage this, signals?

Re:Comparison of Rust async and Linux thread context switch ... by Freaky on Friday February 12, 2021 @10:50AM (#26114941)
Nothing so fancy.

Each worker thread runs in a loop executing a queue of jobs. On every iteration it sets an atomic progress flag to true.

The runtime in which it's contained polls its workers every 1-10ms, atomically swapping in false and checking to see if the previous value was also false - if so, it steals its task queue and spins up another worker to execute it.

https://github.com/async-rs/async-std/blob/ceba324bef9641d61...

Re:Comparison of Rust async and Linux thread context switch ... by josephg on Friday February 12, 2021 @07:49AM (#26113068)
> though obviously only works when you can « afford » a multithreaded scheduler.

Yeah, for example in comparison actix-web only uses single threaded workers - one per core. Future in actix-web doesn’t have to be Send or Sync, and I think it’s incompatible with what async-std is doing here. That design is almost certainly one of the reasons actix-web tops phoronix

Re:Comparison of Rust async and Linux thread context switch ... by raggi on Friday February 12, 2021 @05:24AM (#26112163)
It also really doesn't scale. It'll do fine on your average <10 core laptop, but once you get on a multi-package system you're going to find you're constantly thrashing memory because it is making disruptive scheduling decisions and your pooled tasks have poor context locality.
Re:Comparison of Rust async and Linux thread context switch ... by masklinn on Friday February 12, 2021 @04:55AM (#26112034)
> A classically threaded program can easily block on a future, but wrapping a blocking call in an otherwise asynchronous program is complicated, expensive and error prone work.

It’s really not though, at least as long as the parameters and results are Send. For instance Tokio has a spawn_blocking which runs the function on one of the blocking threads it spawns on-demand specifically for that use.

Meanwhile « blocking on a future » requires adding and managing an entire async runtime and its interactions with the rest of the program, and locking up the runtime is a very real possibility.

Re:Comparison of Rust async and Linux thread context switch ... by josephg on Friday February 12, 2021 @07:52AM (#26113088)
I understand the desire to stave off dependencies but managing an async runtime should only be a simple function call or two. How do you end up locking up the runtime with something like that?
Re:Comparison of Rust async and Linux thread context switch ... by throwaway81523 on Friday February 12, 2021 @10:31PM (#26121159)
These days it is possible to eliminate almost all blocking calls in Linux apps. File opening was a long persisting one but io_uring fixes that. Async sockets and file i/o to already-open fd's have been around forever.
Re:Comparison of Rust async and Linux thread context switch ... by darthrupert on Friday February 12, 2021 @01:22AM (#26111203)
You might like Zig's attitude towards this question. Async/sync decision is a single compile-time decision there. The jury's still out whether that's a good idea though.
Re:Comparison of Rust async and Linux thread context switch ... by jorangreef on Friday February 12, 2021 @04:07AM (#26111847)
I recently wrote an IO abstraction over io_uring using Zig's async/await.

Here's how you would do a write()/fsync()/read() with this (https://github.com/coilhq/tigerbeetle/blob/beta/src/io.zig#L...):

  const offset: u64 = 0;
  const bytes_written = try io.write(fd, buffer_write[0..], offset);
  try io.fsync(fd);
  const bytes_read = try io.read(fd, buffer_read[0..], offset);
Other sync functions can use this asynchronous IO completion code in a synchronous style (as this snippet shows) and still get all the zero-syscall and asynchronous performance of io_uring. What this is actually doing under the hood is filling SQEs into io_uring's submission queue ring buffer and then later reading completion events off io_uring's completion queue ring buffer, so it's fully asynchronous in the I/O sense but this hasn't spilled out and leaked over into the control flow. The control flow is as it should be, nice and simple and synchronous.

Beyond this, Zig still allows you to explicitly indicate concurrency with the `async` keyword, for example if you wanted to run multiple async code paths concurrently.

But the crucial part is that Zig's async/await does not force function coloring on you to do all of this: https://youtu.be/zeLToGnjIUM

Pretty incredible on Zig's part to be able to pull this off. Huge kudos to Andrew Kelley. Also, thanks to Jens Axboe and io_uring, what you saw above was first-class single-threaded or thread-per-core, there's no threadpool doing that for you, it's pure ring buffer communication to the kernel and back, no context switches, no expensive coordination. Pure performance. There's never been a better time for Zig's colorless async/await. The combination with io_uring in the kernel is going to be explosive. It's a perfect storm.

Re:Comparison of Rust async and Linux thread context switch ... by jstrong on Friday February 12, 2021 @08:18AM (#26113301)
mio and mio_httpc are options if you're the kind of person like me who finds 'async' worse than event loops.