Live data from Hacker News

Comparison of Rust async and Linux thread context switch time and memory use

github.com

171–180 of 201 posts

Re: Comparison of Rust async and Linux thread context switch time and memory use

#171

Earlier quoted context omitted.

I find your comment about stack traces a bit weird: of course, when all your work is sequential and you can use only threads, you will have a nice stack trace for free, when async stack traces need a lot of support from the tooling. But most of the time you not only use thread, but also several synchronization primitives (locks, channel, etc.) and when doing so, regarding stack trace you are in an even worst situatio…

Maybe if you spray threads around at random :), but in real-world use I find it much easier to pinpoint where the problem occurred, and the path taken to get there. Also, at least with threads you can get the thread ID and/or name. Regarding shared, mutable state - if multiple async "threads" can access that state, then you still need to guard it, but usually with an async-capable means.

> Regarding shared, mutable state - if multiple async "threads" can access that state, then you still need to guard it, but usually with an async-capable means.

Sometimes, but not as often, because the scope of your async function is often the only “shared state” you need.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#172

Earlier quoted context omitted.

Speaking of futures_unordered and similar patterns, I think a part of the "async promise" that has failed is the lack of concurrency for a single user request by default in most languages. That is, the 'easy' path is to write code such as the following (in vaguely C# pseudocode): var p = await GetUserPermission( username ); var c = await GetServerConfig(); var m = await GetMessageOfTheDay(); Assume each await call is…

My teams/company uses it all over, so maybe depends on the context you work in? And FWIW, this explicit form is often unnecessary - if you kick off each task they will run in parallel and then just await each task only when the result is needed, it can look a lot cleaner: var p_t = GetUserPermission(username); var c_t = GetServerConfig(); var m_t = GetMessageOfTheDay(); var foo = isAuthorized(await p_t); // more code…

True, this doesn't work in Rust though, because nothing at all happens before the first time you poll a future, so you need an explicit task (but as other pointed out, it's pretty straightforward thanks to the `join!` macro).

Re: Comparison of Rust async and Linux thread context switch time and memory use

#173

Earlier quoted context omitted.

> Ease of understanding multithreaded code Rust is not Javascript. Using threads is actually a lot simpler in Rust than async/await.

I don’t know about Rust but in every other language I’ve used threads were easy to use and understand, except when it came to some bits like signals, which at least on Linux are no longer a big problem. Main thread runs a hot loop to look for data to process, then hands it off on a queue to a worker thread out of a pool. That thread is then solely responsible for processing the event and passing the result either bac…

Sure, threads are easy to understand. The difficult is when you get a concurrency bug but that can happen with single threaded async/await code anyway.

Also threads are definitely not easy to use in all languages. E.g. C++ gives you very little help (no channels for example), and JavaScript makes starting threads difficult and moving/sharing memory is limited to primitive arrays.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#174
post #158
post #75

> People often see that there's some theoretical benefit of async and then they accept far less ergonomic coding styles and the additional bug classes that only happen on async due to accidental blocking etc... despite the fact that when you consider a real-world deployed application, those "benefits" become indistinguishable from noise. However, due to the additional bug classes and worse ergonomics, there is now le…

I think the more important aspect of that quote is just about performance vs. code. I see many cases where people are hyperoptimizing on whether or not their framework consumes 2 or 15 microseconds per request when the work they are going to do takes 100 milliseconds. If you like the async style better, then fine, use it. Sometimes you win like that, where the thing you like better is also faster. But don't worry so…

you have a good point about http request size. But any high performance framework would not buffer the whole request to later parse it. it will do incremental parsing.

Meaning you don’t need to read all the bytes from the tcp socket before deciding which route to take. And also the handler for that route is given a stream object and will just read as many byte as it need.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#175

Earlier quoted context omitted.

I've been using C# for around 20 years, basically since it was first released. I never personally had any issue with working with threads and locks, finding it simple enough to reason about them, though I understand lots of people felt differently. When async/await first came to C# around 10 years ago, I grumbled because I didn't see the point; I found it much harder to reason about the flow of code, and initially at…

async/await is for the concurrent stuff and threads are for the parallel stuff. Two different things. If your code is I/O-bound, use async/await. If your code is processor-bound thing, use threads.

Async/await paradigms exist in several languages, but with C#, async/await is generally considered the "modern" and unified way to handle both IO bound and CPU bound tasks.

The runtime will generally schedule IO bound tasks to run on the threadpool.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#176

Earlier quoted context omitted.

Sorry, what does tokio have to do with "async"? The default implementation uses posix threads, no?

It's a runtime for running lightweight tasks (`Future`s, async functions) on top of it. What is not async about it? And of course it still needs posix threads. The executor needs to run somewhere, and the only somewhere that an OS offers is a thread.

Sure, but I didn't think anything about async functions implied running tasks. Isn't it just syntactic sugar over futures? You certainly don't need to use the tokio runtime in order to use async functions.

So, it's not clear why you'd abandon the async syntax just because you're compute bound.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#177

Earlier quoted context omitted.

async/await is for the concurrent stuff and threads are for the parallel stuff. Two different things. If your code is I/O-bound, use async/await. If your code is processor-bound thing, use threads.

Async/await paradigms exist in several languages, but with C#, async/await is generally considered the "modern" and unified way to handle both IO bound and CPU bound tasks. The runtime will generally schedule IO bound tasks to run on the threadpool.

> The runtime will generally schedule IO bound tasks to run on the threadpool.

Well, that's not correct. Unless you explicitly call Task.Run or Task.Start (or other similar methods) no new thread is created. The compiler generated state machines don't require the threading mechanism to work. In fact the overhead for async/await is mostly the extra code generated for the state machine and error handling. At runtime, there's no thread switching overhead.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#178

Earlier quoted context omitted.

Async/await paradigms exist in several languages, but with C#, async/await is generally considered the "modern" and unified way to handle both IO bound and CPU bound tasks. The runtime will generally schedule IO bound tasks to run on the threadpool.

> The runtime will generally schedule IO bound tasks to run on the threadpool. Well, that's not correct. Unless you explicitly call Task.Run or Task.Start (or other similar methods) no new thread is created. The compiler generated state machines don't require the threading mechanism to work. In fact the overhead for async/await is mostly the extra code generated for the state machine and error handling. At runtime, t…

Yes, I meant using Task.Run; I was simplifying, as I'd assumed (wrongly) you were familiar with async/await from another language.

Otherwise, from memory, the runtime spec doesn't actually guarantee that await won't run on a threadpool thread - it will under certain circumstances.

And then there are further nuances if there is a synchronisation context and ConfigureAwait(false) is used, as the continuation will be scheduled on a threadpool thread.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#179

Earlier quoted context omitted.

I find async/await to be easier to use than alternatives. I see two categories: - A. Async/await - compiler saves and resumes functions. - B. Message based - Golang, Erlang, threads with messaging. With category A, I can use my IDE to jump to every function that is called and easily follow the computation. With category B, all of these connections happen at runtime with messages. When you have a tree of tasks all whi…

This a confused notion. A useful way to think of Go and Erlang is that they automatically and transparently insert async/await each time you call a function that performs I/O. Messaging between different application tasks is completely orthogonal and can have use cases in languages with async/await as well.

I probably should have put the categories as:

A. Implicit messaging using the languages function syntax (async/await).

B. Direct messaging using a message passing feature of the runtime (Erlang, Golang)

Note: I mean "messaging" in the context of a single OS process, that possibly has many threads (so within a single language runtime).

Async/await is still implicit messaging, but it appears like a regular function call - which in my opinion is easier to understand. Using function args/return for input/output is something every developer already knows.

In contrast, Erlang and Golang require you to use some type of messaging feature in addition to functions.

> A useful way to think of Go and Erlang is that they automatically and transparently insert async/await each time you call a function that performs I/O

The part they are missing from async/await is the ability to easily get return values without messaging, and do this recursively for a large tree of functions.

E.g. getting a return value from `go x()` requires messaging, but with async/await you could do `const p = x(); const ret = (await p); // return value received at a later time with no messaging.`

Both of them will require you to create some type of messaging topology to return the values (which makes your program a mixture of (regular functions + messaging features) vs async/awaits "everything looks like a function").

Re: Comparison of Rust async and Linux thread context switch time and memory use

#180

Earlier quoted context omitted.

This a confused notion. A useful way to think of Go and Erlang is that they automatically and transparently insert async/await each time you call a function that performs I/O. Messaging between different application tasks is completely orthogonal and can have use cases in languages with async/await as well.

I probably should have put the categories as: A. Implicit messaging using the languages function syntax (async/await). B. Direct messaging using a message passing feature of the runtime (Erlang, Golang) Note: I mean "messaging" in the context of a single OS process, that possibly has many threads (so within a single language runtime). Async/await is still implicit messaging, but it appears like a regular function cal…

> The part they are missing from async/await is the ability to easily get return values without messaging, and do this recursively for a large tree of functions.

No, they do not. In Elixir for example if I call:

      bytes = File.read!("filename.txt")
`bytes` will have the data returned from the function call immediately, with no need for message passing or awaiting the result. Under the hood, it is still asynchronous evented I/O. If I want to explicitly await for flow control reasons (await all of or one of multiple events) that is available in the stdlib in the `Task` module. E.g.

      t1 = Task.async(fn -> do_this_thing() end)
      t2 = Task.async(fn -> do_this_other_thing() end)

      Task.await_many([t1, t2])
 
You can accomplish most things without ever calling send/receive or writing your own gen_server etc.
Post reply on HN