
C++ vs. Rust: an async Thread-per-Core story - lukastyrychtr
https://medium.com/@glaubercosta_11125/c-vs-rust-an-async-thread-per-core-story-28c4b43c410c
======
GolDDranks
What library are you using? The pattern you are trying to achieve should be
achievable in principle. This pattern is commonly called "scoped" threads (or
"scoped" async tasks in this case) in the community.

The problem might be that if the executor you are using doesn't guarantee in
its type signature, that the task you are trying to launch doesn't outlive the
lexical scope there, the compile gets rightfully angry. There are some
executors that guarantee that; see here: [https://docs.rs/async-
scoped/0.4.1/async_scoped/](https://docs.rs/async-scoped/0.4.1/async_scoped/)

------
ncmncm
This article was remarkably well written and reasoned. I would welcome more.

The author is correct that C++'s greatest weakness, and one that can never be
fixed, is that other code can do things that violate the model your code
depends on. It is not enough to have your own discipline, the rest of the
system needs to observe the same discipline at interfaces. And there are many
possible, correct disciplines to choose from.

Rust imposes one of the possible disciplines for memory-object management that
enforces one set of non-negotiable tradeoffs and costs. You the programmer are
relieved from choosing a discipline or discovering what was chosen, and from
discovering that two subtly different ones were used in different parts of the
system.

Memory-object management is not the only place where systems need a consistent
discipline, but others can often be referred to it; and every program needs at
least the one, besides any others.

Arguably the reason Common Lisp never took off is that practically nothing can
be assumed about any other part of a system. C++ has enough forced consistency
to drive adoption, but might better have had more. C++ has a lot more tooling
than Rust does to capture semantics in libraries, so the library is the
natural boundary for a discipline, and it is not unusual to find more than one
safely co-existing, but it is also not unusual to find them not safely
encapsulated, and in conflict.

Ultimately there is no substitute for sound design. Rust's limits help ensure
a consistent design, but the cost in abstraction power is high.

~~~
pierrebai
Personally, I've written a few multi-threading implementations at various
places and my accumulated wisdom is that any data received by threads has to
be passed by values or be constant. Anything else is a lot of risk for little
value.

I've also come to the conclusion that async/future are a bad idea for multi-
threading. They're fine for managing mono-thread asynchronous events, but
using them as a poor man's thread pool is begging for trouble. Among the
problems are:

    
    
       - you do not know if the execution will be synchronous or not.
       - you often don't know and have no control on how many threads will be spawned.
       - it makes following the flow of data and execution much harder.
    

What I've come to rely on is the trinity of:

    
    
       - a thread provider (non-committal name, we all know it's a thread pool behind the scene), which provides the threads of execution.
       - a work item provider, which provides the input data to the algorithm.
       - a worker provider, which provide workers that receive a work item, execute an algorithm and produces a result.
    

Then something gets executed when all three are united. For the curious, the
reason to have a work item separate from worker is that it allows the worker
to be long-lived and re-used and, most importantly, allows containing heavier
data. For example, a cache. The work item are light and are just the necessary
data needed to process one... work item.

I've also sometimes divided the work item into two parts: a work item and a
context. The work item is the changing data to be processed while the context
is the almost constant data, and can be shared. (For example, configuration
data.)

~~~
Mic92
That sounds a lot like how the linux kernel manages it with its workqueues
data structure, which gets consumed by kworker.

------
oconnor663
There should be a way to define an async executor API that works like
Crossbeam's scoped threads API, so that the executor lives inside a fixed
scope and is guaranteed to clean up any outstanding tasks before the end of
that scope. Then it would be safe to accept references to longer-lived values
on the stack. My guess is that there's not much demand for such an API outside
of toy examples, though. (Does anyone use `crossbeam::scope` in production?
It's a beautiful and important concept, but it seems to occupy an awkward
performance point of "I care enough to worry about heap allocations, but not
enough to use a thread pool.") EDIT: Ah, just saw GolDDranks' similar comment.

~~~
dathinab
There are, but you often run into limitations in praxis.

For example you can take a look at tokio::LocalSet::{block_on,run_until} which
require neither 'static nor Send or Sync. But then spawn_local does require
'static.

Same is true for smol where we have smol::block_on,
smol::{Executor,LocalExecutor}::run and here too we have the limitation that
calling spawn on a LocalExecutor requires static.

------
maxlybbert
That’s an incredibly long-winded way to say “don’t think of reference counting
as a (potentially-avoidable) cost; think of it as an _investment_.” Then you,
too, can be happy, as long as you don’t think too much about it.

~~~
kbenson
Sometimes the response you get from someone after _telling_ them something and
_showing_ them something are different.

Those who already agree with the point probably think it long-winded. Those
who would argue the point if told it probably appreciate the nuance the
context provides.

------
muststopmyths
Man, that c++ code triggers my hatred of lambdas. Instead of a hopefully
descriptive function name, whose code I can peruse separately from trying to
understand the enclosing code, I have to understand every goddamn line in real
time.

lambda more than 5 lines should be a compile error.

Why not move C++ towards readability instead of enabling laziness in the guise
of features ?

~~~
pkaye
Do you hate the C++ syntax for lambdas or the concept of lambdas?

~~~
muststopmyths
The concept of anonymous blocks of code in the middle of a flow annoys me. I
fully appreciate that sometimes they're useful, but like everything else in
C++ there is a way to misuse them horribly and you will run across code bases
that do it eventually.

------
zelly
Or, you know, actual garbage collection (generational, JVM-style)

~~~
bonzini
If you compare Cassandra and ScyllaDB, the garbage collection causes serious
problem with 99.9% latency.

~~~
pjmlp
Only because Java is yet to support value types.

In languages like Nim and D, where all C++ allocation mechanisms are available
alongside a GC heap, there are plenty of ways to improve performance.

