C++ vs. Rust: an async Thread-per-Core story
medium.com
medium.com
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/
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.
Could you give some examples of other possible disciplines?
The point is that you have a lot of choices to make that are only peripherally related to the problem the code is supposed to be solving. Those choices might allow you to tune the performance of operations better to the problem, but they may be just a distraction from the goal.
For most systems, nobody knows how fast it should be able to go, or whether you got close. Almost as often nobody even cares very much, or cares at all. When they don't, it is wasted effort to spend a lot of time and skull-sweat optimizing performance. There, a straitjacket language that offers few choices is best. It lets you get on to the next thing.
Sure reviewing unsafe code isn't perfectly ready often it relies on invariants uphold outside of the unsafe blocks (in the same type).
Still much better then having to review everything ;=)
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.)
So, yes, there can be equally rigorous system-level disciplines that substitute for language-imposed ones, and that can offer much better performance. But it does take a lot of experience to choose well.
Until you load a shared library not written in Rust into the address space.
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.
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.
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 ?
I think that might be a tad reductive. Also, concluding that "people" must do anything now isn't useful. No one will be persuaded by your argument. People that are knowingly writing awful shite aren't bothered by that fact. People unknowningly writing awful shite will assume you're talking about other people.
You can write fine code functionally but for some reason this kind of horror is considered "the right way". Why? since when did people forget about writing for the human not the computer?
There's nothing about functional programming that requires nested lambdas. I like functional programming at least a little, but I don't like nested lambdas. If you want to know why someone considers it "right", you'd have to ask someone who considers it to be.
Strongly agree re: writing for humans. Actual computer architecture seems a lot closer to imperative with goto, so I'm not sure who would argue that functional code is "for computers".
I did say why - it's because it's "functional style" so it should be done that way. I've been told this to my face. Trouble is, it's plain wrong.
> so I'm not sure who would argue that functional code is "for computers".
Agreed. I should not have put that.
AKA quit giving C++ devs new obfuscation techniques...
It is a subjective thing, and I think the permissive approach of the language with regard to lambda complexity is a good thing.
The biggest thing that can be done to make that code example more readable is to use coroutines (which were just introduced in C++20) which will get rid of the lambdas.
[0] My opinion
[1] Exception is where you use lambdas for passing to new control flow structures. Something you can't do with python, although python does finally have a way to do the same thing (I think htis, though I'm a bit rusty so maybe something newer https://www.novixys.com/blog/proper-cleanup-resources-python...)
#include <cstdio>
int main() {
auto square = [](double x) -> double { return x*x; };
printf("%f\n", square(10));
return 0;
}
since 2011No. They don't. Non-constexpr ones can't. And lambdas with captures that outlive their creators are notoriously painful for compilers and libraries to support.
That function, with its implementation fully exposed, is as easily inlined as any other. The struct is moved or copied as needed to keep it live after the originating context evaporates.
What I often find problematic is lambdas that outlive their creators. Especially ones that have lots of captures. Especially especially ones where those captures include objects with complex destructors. It's a common pattern in the code I have to work with, and it constantly results in serious bugs. A far better pattern is to wrap the things that need to outlive the caller in their own object - often a "request" object that should have already existed and had its own separate lifetime - and pass that around instead of passing lambdas.
I'd be perfectly happy with lambdas, even lambdas with captures, that couldn't outlive their creator. Trying to attach one to an object with non-local scope would be a compiler error. We already have objects to wrap up the same bits of data and control flow that a long-lived lambda would have. People should use them.
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.
Especially when the lifecycle cost of the corner case debugging is folded in.
[1] https://sites.google.com/site/lehijujitsu/slow-is-smooth-smo...