However, I think the way the language has implemented `async` is going to cause a riff in the usage. Because they didn't standardize the executors interface, you're essentially forced to using a single runtime (Tokio) since library developers would have to code for each runtime. Also, because of the async keyword, library developers would also have to make a different sync/async of each function they want to declare.
These are not small issues and will only continue to get worse as people are forced to choose one route, leaving others to use a different language.
The one saving grace with Rust is if everyone decides to say "screw async" and just builds synchronous APIs, then we use something like [May](https://github.com/Xudong-Huang/may) for green threading.
> Kidding, I don't have a plan.
:|
This is a program with async in general, no? Go avoids this by just having everything be async, but that seems unsuitable for Rust since you won't have control over when your code executes and what thread it executes on, not a problem for applications but it would seem to rule out things like kernel modules and firmwares, as well as require every type to be Send + Sync (which ultimately means the language must be garbage collected).
In Python for example I've written async and sync versions of a library, and some magic testing that makes sure both implementations return the same results.
> These are not small issues and will only continue to get worse as people are forced to choose one route, leaving others to use a different language.
Honestly this doesn't seem like a problem to me. I think there's room for many languages in this world. Rust is a wonderful language but it isn't a "one true language." People should choose a different language if it suits them better. That's no more an indictment of Rust than it's an indictment of Python when I embed Rust in a web app - that's not a place where Python shines.
No. If you are an async program calling a sync library, you can use `tokio::task::spawn_blocking`, and Tokio will run your sync function on a worker thread that's allowed to block https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.ht...
The `reqwest` library offers a sync API by just wrapping its own async API: https://docs.rs/reqwest/0.11.12/src/reqwest/blocking/client....
For libraries that don't do this convenience wrapping, you can just create an async runtime like `tokio::Runtime` and use a function like `block_on`: https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.ht...
Since all Rust programs have sync entrypoints, there is always some way to call async functions from sync functions. It's not like C# or Go where the async runtime is a singleton built deep into the language. You always have control of it.
In practice, if I'm doing anything with networking, I always end up wanting async, so I prefer to have the runtime and use spawn_blocking for sync APIs.
Not everyone wants to pull in an async runtime just to run make a call to an API.
My whole point is async puts a huge burden on library authors that will force users to diverge in their approaches and that will ultimately hurt the community
To me C++ pretty well follows the "make simple things easy/safe, and hard things possible" philosophy. I'm glad this it still does support low level stuff, but the use cases for that are minimal and I do wish there was a compiler flag to reject full C backwards compatibility and only support a modern safe subset, so that projects could protect themselves from junior developers shooting themself in the foot.
It's a shame that C++ doesn't provide mt-safe versions of STL types in the standard library, although trivial to wrap them yourself (1 line of code per method, using std::lock_guard).
Are all Rust data types mt-safe? Does unsafe mode provide faster unprotected versions if you need those?
Yes, but not in the sense you probably think, given your second sentence:
> Does unsafe mode provide faster unprotected versions if you need those?
Some data types can be safely shared between threads, and some cannot. Rust checks at compile time if you try and use a non-thread safe data structure from multiple threads, and if you do, will give you an error. So in that sense, all of them are safe, yes.
You don’t use unsafe to get access to non-thread safe data structures, you may have both kinds, and the compiler checks you use them correctly.
Is there any provision for building your own thread-safe types (e.g. a structure composed of other types) out of non-thread-safe types and mutexes, and if so how does that work in terms of compile-time errors ?
To my knowledge, no. If you want to push to a vector from multiple threads, you "wrap" the vector in a `Mutex`. The difference between C++'s std::mutex or std::lock_guard and Rust's Mutex, is that the compiler refuses to let you touch the data protected by the mutex unless you have a lock acquired on the mutex.
> Is there any provision ...
Yes!
1. In Rust, types can implement "traits".
2. There's two traits that control thread safety: `Send`, and `Sync` Basically, any type that "implements the trait" `Send` is safe to send across threads. And a reference of any type that implements `Sync` is safe to to send across threads.
3. `Send` and `Sync` are "auto-traits", which means that if you make a data structure out of primitives that all implement `Send`, your data structure will also implement `Send`, same for `Sync`.
4. There are a bunch of thread-safety primitives that you can use (like `Arc` (atomic reference counters), `Mutex`, ..etc) to build thread safe data structures.
> how does that work in terms of compile-time errors?
The compiler will not let send a type across threads if it doesn't implement `Send`, and it won't let you send a reference to a type if the type doesn't implement `Sync`!
That way, if you avoid using the `unsafe` keyword, and the compiler agrees to compile your code, you can be sure you won't have data races!
In Rust, there are traits (you might know them as interfaces or protocols from other languages) called Send and Sync which tell the language whether or not something can be send to a different thread, or shared between threads.
Vec<T> is Send but not Sync - you could pass ownership of it to a different thread, surrendering your access to it in the process, but two threads couldn't share access to it. Almost everything is Send, unless it contains references to some thread local state.
Mutex<Vec<T>> is Sync - you can share it between threads. Basically if you take the lock, you'll get back a smart pointer to your Vec<T>.
These traits are generally implemented automatically; you can implement them on a type yourself, using unsafe, but you'd only do that if you were writing your own synchronization primitives or thread safe data structures.
So; the compiler is able to infer which types are and are not thread safe, and what flavor of thread safety it has. It's then able to use that to check for thread safety violations at compile time.
There's more to it than that, in particular it's possible to share a read-only reference to a Vec<T> between threads (with a compile time guarantee that no one has a mutable reference to it), but I'd refer you to the book[1] or to other articles on HN of you wanted the specifics.
I had C/C++ in college and spent most of my career in front-end.
In comparison to the Cs Rust at least tries to get out your way. Stuff generally compiles and when it doesn't, it tells you what might be wrong using human language.
I mean, it's the first time I've seen a compiler error start with "perhaps..." to tell you what steps could be taken to fix it.
LLVM-based compilers such as the rust compiler and Clang (C++) tend to be way better than g++ in this regard, although Clang competition has forced g++ to be a little better in recent years.
Personally, I don't think it's worth all that just to avoid a garbage collector, but it's not like there are that many decent compile languages with GC currently on the hype train.