The main thing I want for Async Rust is the ability to not use it.
What I'm doing involves a virtual world viewer with maybe a dozen threads. Some are compute bound. Some are talking to the GPU. Some are waiting for I/O. The normal situation is about 2 or 3 CPUs of work. Sometimes more.
I don't want an async model, which assumes you're I/O bound, interfering with keeping all those CPUs busy. Already I've had to switch from using "reqwest", which now seems to always bring in tokio, to "ureq", a minimal HTTP client. The way to do HTTP requests used to be with "hyper", but that became a lower level for "reqwest", and then "tokio" was slipped in underneath to make it "async". It always uses async, even if you make a blocking request.
"Async" is a specialized tool for web servers with very large numbers of clients. Outside of that niche, it's seldom needed.
Your comment touches on a few misconceptions I see a lot.
Firstly, `reqwest` exposes both an async and a synchronous API, allowing the developer to choose which one to use. They are largely interchangeable code-wise. [1]
Secondarily, and more broadly, async is possible to opt out of. You must understand that most web and network related libraries will be async by default for performance, because people who write in Rust and people who write web servers typically care greatly about performance. This is the intersection of those two groups. That being said, there are options outside of that ecosystem. [2]
If you truly want to use an asynchronous library without migrating your application to run entirely on an async runtime like tokio, you can run it inside of a synchronous function without much trouble. I've put together a playground link for you. [3]
"reqwest" exposes a synchronous API, but it's doing async stuff underneath. If you turn on logging, you can see 30 or so async events associated with a single HTTP client side request. It's starting up and shutting down a polling thread just to make one HTTP client request.
You must understand that most web and network related libraries will be async by default for "performance". That's what scares me - async contamination of the low level Rust ecosystem. The async enthusiasts have to be kept in check to prevent breaking Rust as a systems language.
I had understood your concern as wanting to avoid the complexity of asynchronous code execution in your codebase, I did not realize your concern is about writing very low level systems code. In that case, you are doing the right thing: libraries like ureq, minreq, Isahc, curl, and more all offer what you want.
It is unclear to me what you mean by keeping the community "in check". There are a lot of people who rely on and enjoy the async story, and they will continue to produce code that improves that story. Simultaneously, there are people who do not need that, and they are not hindered by this. People will build what they want and need. You've just picked some libraries from some of the biggest async contributors in the community and requested that they be kept in check so that you don't have to switch to a synchronous alternative, of which there are plenty.
It's not that "low level". It's that it doesn't fit the model of "mostly waiting for the network". Here are some of the things I have going on:
- Incoming events UDP packets from multiple servers. These arrive at the client and go into a queue for processing.
- Refreshing the 3D window. This ties up one thread almost full time. At the beginning of each frame, it reads queued events that tell it what needs to change in the GPU. The rest of the time it feeds the GPU.
- Some incoming events require querying external HTTP servers to retrieve assets. When the results come back, they include compressed items which have to be decompressed, processed, and turned into GPU-ready textures or meshes. This is prioritized by how important it is to display that object right now, based on the viewpoint. So there are priority queues along with multithreading.
There's more, but you get the idea.
What's so great about Rust is that you can do stuff like this without crashing all the time, spending your life in the debugger, or recopying big objects to safely pass them around.
The previous implementation, in C++, looked a lot more like an "async" model. It had lots of "coroutines", mostly running off a single thread. It also had a few things running as independent threads because they were so CPU-intensive. It was very prone to short stalls that annoyed players. This happened because something had to do more work than expected and briefly stalled out the coroutine system. The killer in async systems is the subroutine that is usually fast but sometimes slow. So I've seen this problem done in "async" style, and it didn't work well.
I've previously done robotics work which had many asynchronous tasks. I've used QNX for that, and I've used ROS. There, you have a lot of intercommunicating processes, which works but has more overhead.
None of this maps well to an "async" model.
Part of the problem here may be that I've done a lot of multi-thread programming and am used to it. It's an alien approach to programmers who came up from Javascript. That's a big fraction of the web backend crowd.
(Personally, when I have to do a web service, I write it in Go. That's the use case for which Go is designed. It has the libraries for that, and the goroutine concept is well matched to that task.)
> I've previously done robotics work which had many asynchronous tasks. I've used QNX for that, and I've used ROS. There, you have a lot of intercommunicating processes, which works but has more overhead.
I wonder, why is this kind of system a bad fit for the async model? Is it because all threads must be reliably preemptable?
Doesn’t matter who he is, if he’s mad that libraries made for and by web and network developers are using a concurrency model that works well for their applications, he should use different libraries.
It does matter -- he was one of the first "network developers". He published RFC 896 over 35 years ago; he has more experience on this topic than almost anyone.
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
This is not the strongest plausible interpretation of what he said --
He's not asking for people to not develop async code. He's asking for them to not hide it in synchronous code.
If you're expecting a blocking system call, and actually get a brand new background thread that's polling, it's quite reasonable to be frustrated.
>If you're expecting a blocking system call, and actually get a brand new background thread that's polling, it's quite reasonable to be frustrated.
It really isn't if the documentation doesn't outright say that it's single threaded and not thread safe. For a lot of simpler use cases where you just want to ship a thread-safe API (e.g. application does not have its own thread pool) then it just makes sense in a lot of cases to use some kind of automatic thread pooling. The caller does not have to know or care how the internal state machine is implemented.
If you have implemented your own thread pool it seems you should know enough to dig down enough to the lower layers to where you can get to that blocking syscall, or least to the point where you can strip off the O_NONBLOCK flags yourself.
Let me try to rephrase this in a way that doesn't pin the blame on "async enthusiasts" as people, and see if you agree:
Many years ago, well before Rust 1.0, Rust used its own M:N threading system, used segmented stacks, had it's own libuv-based event loop, etc. Also, it had garbage collection built into the language.
These were removed before 1.0, which made Rust a lot better as a systems language: you could reliably embed it into non-Rust programs, you could reliably interoperate with non-Rust libraries that didn't expect to be moved around threads, you didn't need to care about starting up the GC (or handling GC pauses), etc.
This was a good decision for Rust, and it turns out most of the things people wanted to do with these features could be done outside it - e.g., the borrow checker avoided the need for pervasive GC. (Though almost certainly not intentional, one side effect is that it distinguished Rust from Go: Go is great for standalone programs that need lightweight concurrency, but it's very bad at being embedded into other code and not the best choice if you're mostly calling FFI libraries.)
First, it would be good for Rust to stick to that decision. Rust should not regain a pausing GC in the standard library - similarly, it should not regain a thread manager in the standard library.
Second, it would be good for Rust libraries to work within the spirit of that decision. That's a lot harder, because part of the expectation when those features were removed was that some needs - notably around event-based processing - would be met by third-party libraries. As I understand it (and I might be totally wrong!), it was honestly a bit of luck that the borrow checker worked as well as it did and was ready at the right time, and the expectation was that someone would add a GC library and it would be widely used. However, it's very good that no widely-used GC library sprung up. In the same vein, it would be good for there to be no widely-used third-party thread manager library.
This might be hard, possibly requiring a borrow-checker-level miracle, but it's worth aiming for. And if there has to be a thread manager (or a garbage collector), it should not be part of core Rust.
Would you agree with that phrasing?
--
Incidentally, why does reqwest start up a polling thread when called in sync mode? Can't it do the polling on the main thread? (Or in other words, async programming doesn't imply multithreaded programming. I actually sort of expect that async programming is better suited to single-threaded programming with an event loop, because if you're okay with threads, you may as well just write synchronous code on threads! So either there is something subtle and very interesting here, or there's an easy fix, or I'm misunderstanding something badly.)
I think this rephrasing misses the mark a bit on the original concern. GP explicitly states that he wants Rust-the-language to remain as it is - close to the metal, no GC, with minimal runtime and 1:1 threading. The concern is indeed with the libraries/ecosystem. We are not quite there yet, but it is not hard to imagine that in a few years somebody who asks how to do some simple task in a blocking fashion will be met with replies in the vein of "well, that's not idiomatic", "why don't you use async", or "there was a library for that but it is now kind of unmaintained". All because the main effort of the community went into supporting and maintaining the async mode.
If the reqwest library indeed spawns a thread for every request, it is a good example of that dynamic. Sync mode kind of works but is clearly a second-class citizen and works in a suboptimal fashion. And if you want to peek under the hood to debug it you still have to deal with the async machinery in all its gory detail.
So that gets at my second question. I would expect that if async mode is working well, it specifically avoids needing libraries to spawn a thread.
Or put another way - when Rust removed M:N threading and also shipped out of the box with no event handling support after removing librustuv, the recommendation was that libraries should use threads to handle concurrency and make blocking calls on each thread, and modern OSes make threads perform well, so why not. Isn't the whole point of revisiting async to avoid that answer?
I have the same use cases of wanting Rust to be a close-to-the-metal language with a minimal runtime that you can safely plop in place of any C code, and it seems to me that the way to do that is to get the async story to be so good that people start saying "Well, that's not idiomatic" and "That approach was common but the libraries are all unmaintained" to libraries that spawn threads. What am I missing? Why are we associating "more async" with "more threads" instead of "remain on the calling thread and use an event loop"?
That doesn't seem to be related to async? I don't know the details of rust's async implementation but that sounds like a problem with your application's setup -- you should be able to have a single threaded async executor that uses an event loop, or in simple cases, just calls poll/select directly?
To put it another way, it's unfortunate that particular synchronous API is implemented using threads, but there is nothing about async that implies one way or another that a synchronous method will be implemented using threads -- I've seen plenty of (questionable) C functions that do similar things like using pthread_create and then pthread_join immediately after to fake a blocking task.
Er? No, the point is that threads are what you want for cpu-bound tasks. Async does not deal well with long running cpu intensive jobs that hog the cpu without yield points.
But regardless, the GP post was not taking about matrix math, it seems it was talking about sending an HTTP request and waiting for a response, which is something that actually is I/O bound on the TCP socket.
The systems that use it as a native threading model are obsolete, but there's also this sentence there:
>Cooperative multitasking is used with await in languages with a single-threaded event-loop in their runtime, like JavaScript or Python.
There's no reason rust can't have an executor that does the same, and you only use that within the event loop on your one or two HTTP worker threads. If you're waiting in a thread for an HTTP request to return, that's never going to be CPU-bound. I still am failing to see what the problem here is besides a complaint about some rust crate only supporting a multi-threaded executor, which again is a different problem than whether it's done with async futures or not. One could just as easily write some C code that forces the use of threads.
Those languages are also well known for not handling multiple cpu bound threads well. (And for that matter, it's simply wrong about Python, which uses native threads, but locks very heavily: you need to write native code to use more than one core effectively from one process.)
The goal is to NOT do what you're suggesting. It's a holdover from when native threads were much more expensive than they are today, and multiple cores on a single cpu were rare.
Complexity kills code. Being able to reason about what your code is doing, is FAR more valuable to me than async. Having tokio act as my runtime and switch tasks as it sees fit will be debug hell.
The problem I see is the current async story is opt-out. It's use async or go find something else. Async should be opt in. As in, the code works regardless of an async runtime, async is added magic if you want it, but it will run like normal single threaded code if not.
Yes, in fact, you cannot even use async Rust without writing your own executor or bringing one in via a library. It is very, very much opt in. That was a hard constraint on the design.
However, I think what the parent is getting at is the feeling of the total package, not the technical details. If every library you want to use is async, you can't really "opt out" exactly, even if technically the feature is opt out.
An executor is required, in name or in spirit. Every async system has software that does this. Most language runtimes that do simply give you no choice in the matter.
Rust is a language that does things different to other languages because it is a better way. I challenge you to do the same with Async. There is a different better way.
> The main thing I want for Async Rust is the ability to not use it.
This is what many of us want. Sadly this doesn't seem to be a use-case many in rust are interested in supporting as a first class citizen (from what I've read at least).
I remember seeing someone being down-voted into oblivion for the mere suggestion of some sort of basic fallback executor in the standard library.
Just to be clear, you are saying that many 3rd party libraries in the ecosystem are not interested in "supporting as a first class citizen", right? Because as for the language itself and the standard library, they are committed to support both as first-class citizens.
Popol is designed as a minimal ergonomic wrapper
around poll, built for use cases such as peer-to-peer
networking, where you typically have no more than a
few hundred concurrent connections. It’s meant to be
familiar enough for those with experience using mio,
but a little easier to use, and a lot smaller.
You want libraries to be able to offer implementations that are polymorphically async-or-not (perhaps using higher-kinded types). That's how I'm used to working in Scala.
This is also a problem over in Python-land. One of the interesting approaches is automatically generating non-async code from async code by editing out the async annotations (at the source level). The Trio folks call this "bleaching" the code, in reference to the "What color is your function?" blog post https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... complaining about how the common approach to async in JavaScript divides the world into "red" (async) and "blue" (sync) functions that interoperate poorly - you can bleach your code if it's the wrong color.
https://github.com/urllib3/urllib3/issues/1323 is a discussion of this. "Solution: we maintain one copy of the code – the version with async/await annotations – and then a little script maintains the synchronous copy by automatically stripping them out again. It's not beautiful, but as far as I can tell all the alternatives are worse.
In Rust, you don't need to do this. If you want a blocking version of a non-blocking function, you spin on poll. Several async libraries ship a block_on function that does just that. If you can't do that, but don't want to bring in a proper big-boy executor; you can spin off another thread, have it block, and join it when it's done. (or use something like crossbeam's scoped threads)
Colored functions are a problem in JavaScript because it has to share an event loop with the rest of the browser. For Node.js and Python, that is less of an issue, but those languages are also either practically or actually single-threaded, which also means async behavior is contagious. Rust does not have this problem, at least not until you're writing WASM programs with it (in which case, you're restricted to sharing one thread with the rest of the browser again).
Well, 'Animats expresses a desire in another comment for functions not to go off and spawn a new thread.
I do somewhat agree with that desire - I don't think that this is a problem for performance, but since most of my use of Rust is dropping it into existing C code, I think that spawning a thread is likely to have annoying visible side effects on the calling code, which might not be expecting me to do that.
Using a proper, single-threaded executor and running it on the current thread seems like it would work, yes. (To be fair, I also feel like just having the sync version of a Python API call "trio.run(self.equivalent_async_api)" would probably also work, and I don't totally follow why that's insufficient....)
> "Using a proper, single-threaded executor and running it on the current thread seems like it would work, yes. (To be fair, I also feel like just having the sync version of a Python API call "trio.run(self.equivalent_async_api)" would probably also work, and I don't totally follow why that's insufficient....)"
Why do I need all this other bloat just to run a function? Why can't I just run the function?
I'm not sure exactly what you're asking, but I think it's either answered by the "What color is your function?" article I linked above, or by the answer that this is exactly why unasync exists and why I suggested that approach is worth considering, or by the answer that you can, in fact, just run the function and the "bloat" (which is just syntactic bloat - note that performance is generally going to be better!) is taken care of behind the scenes by a wrapper that calls an executor for you.
yes sorry, those were rhetorical questions. Your point about asking you fail to see why not using a blocking executor to deal with the async code. My problem is with needing the executor at all. I must have skipped a couple of you previous pints in this thread. Apologies about that...
Maybe we should start trying to think about async as being something can use if they want and ignore if they want. Code being async compatible rather than async required.
How would this work? (I do really think this is the right model, I'm just trying to figure out what that model is, exactly. :) )
Let's say I have code like this, in Python asyncio:
class ShardedDBClient:
async def query(self, key):
tasks = [self.query_shard(key) for shard in self.shards]
results = await asyncio.gather(*tasks)
for partial_result in results:
if key in partial_result:
return partial_result[key]
return None
How do you run this without an executor?
The obvious way to make it not be "async required" is to say, we get rid of the async/await keywords - but what do you do with that "await asyncio.gather" instruction? Do you call each of those callbacks serially?
Generally, even in Rust (perhaps especially in Rust), I would expect this to use some OS facility for waiting on multiple sockets (possibly even just boring select(), but preferably epoll/kqueue) to send a bunch of database requests out in parallel and then wait on all their sockets to handle responses as they arrive. I would expect that even if my own code doesn't involve async/await at all.
which creates an asyncio executor just to run that one function.
This is going to be a lot faster than querying those shards one at a time! And it also can semantically change how the library behaves - imagine that there's a timeout parameter, and I set a 100ms timeout. I probably mean that to be 100ms for the entire operation, not 100ms per request, but I probably also don't expect my calls to always fail if each query takes 10ms and there are more than 10 shards.
The downside is that this library is quietly using asyncio without you knowing. But how exactly is that a downside? I already expect the library to be using select/epoll/kqueue without me knowing. And in a language like Rust, the executor should basically compile out - it should be a "zero-cost abstraction" compared to writing the event-handling code by hand.
As long as the async code doesn't depend on calling itself concurrently it should be straightforward to simply execute it on the current thread, right? (Basically using an executor that has 1 thread, the current thread.)
And it'd be great to check and optimize away all this at compile time.
It's a problem if your function to query a database or whatever instantiates a bunch of futures and yields between them when you're just going to block. Given that Rust is designed for environments that are so limited that they can't afford garbage collection, that stuff is proportionately pretty costly.
That would have to be determined at the discretion of the implementer of the library. Rust is after all capable of being used to write code that will run in a context where there is no kernel and thus no syscalls.
I don't get it - what library? I may have an `async` function and this is part of the Rust language, not tied to any library. Can I make it into a blocking function without 100% CPU?
"async/await" is just syntactic sugar for a function that returns a Future plus a state machine at yield points. You need a library (executor) to run that Future (execute that function).
The Rust standard library's block_on uses a global ThreadPool, and the docs recommend using a LocalPool if you need finer grained control.
So, to answer your question it depends on the executor (the thing that implements block_on).
So for anyone reading for the correct details: the block_on I was thinking of is part of the futures crate, which is not in std, it's not an "official library".
> "the curse of knowledge: the folks working on Async Rust tend to be experts in Async Rust. We've gotten used to the workarounds required to be productive, and we know the little tips and tricks that can get you out of a jam"
I like the format of the github Rustlings repo for exposing common beginner pitfalls and pointing developers in the right direction. For the uninitiated, Rustlings is a repo containing a collection of broken code examples that the developer has to fix along their journey to enlightenment. I feel that it would be beneficial to have an "Advanced Rustlings", say Rusteenager ;), for more advanced topics like tricky borrow checker situations and async rust. These examples would be similar to the PR's mentioned in the blog post but may get better exposure imo.
I like the approach that steps back and looks at usage scenarios. Not everything has to be solved by changing the language itself; it may be a matter of better tooling or teaching materials.
For example, the recent "async doesn't work" blog post was centered around confusion between `fn()` pointer and `Fn()` trait, and misuse of temporary `&mut` where `Arc<Mutex>` was required. If the compiler was smart enough to suggest these fixes, then the whole blog post could have been "async works fine, just needed a couple of lines changed as indicated".
That wasn't confusion, that was the point. If there's confusion it's in people having different conceptions of what async language constructs should provide, which color their criticisms and rationales.
Is there any thought to including a default executor in the standard library? I think it's kind of an obstacle for beginners when the language/stdlib provide all the tools to write async code, but not to run it.
I saw discussed in this talk the intent in allowing developers to provide their own executor based on the specifics of their use case:
https://youtu.be/NNwK5ZPAJCk?t=1107
This makes sense to me; however, I feel like the defacto at this point is that most people are using tokio. Is there a possibility that a default executor could be provided, and allow developers to override it with a custom library should they chose?
I know lots of Rust newcomers have an expectation that the standard library should be all they need, and dependencies should be avoided, but that's not Rust. In Rust dependencies are good, you're supposed to use them. If something can be implemented well outside of std, it should probably remain outside std.
The problem is that a standard library is a heavy burden for a language, and a huge risk for its longevity (think 40 years from now). It promises that the first stable release of any feature will work forever, and never change. It's an unrealistic promise for anything non-trivial.
In other languages it often played out like this:
1. std added a feature,
2. it turned out that it wasn't the best API, but it couldn't be fixed,
3. people fed up with the poor std API wrote a replacement,
4. everyone has to be reminded "don't use the std version, use the replacement instead" forever.
I would worry this would end up like Python, where there is a default async executor in the standard library (asyncio) which was previously developed outside of the standard library (Tulip), but a) it's changed a fair bit between versions, even recently, as they figured out better ways to do things and b) there's been significant work on differently-structured executors (Curio and Trio, notably) as third-party libraries. As an end user of Python, I want to basically entirely use Trio, but there's a fair amount of async Python out there that's been written to assume asyncio.
(There is, at this point, an abstraction library called "anyio" which can run on either an asyncio or Trio event loop, but anyio's model is most strongly influenced by Trio's, because Trio's model is more structured - which means anyio couldn't have been written before Trio was developed and well-received.)
Given that Python has a strong "batteries included in the standard library" approach and Rust has a strong "we shipped a really good package manager with the language, and even the C 'int' type lives outside the standard library, in a package maintained by the Rust core team" approach, it seems like it would be very weird for Rust to repeat the mistake (at least in Rust's worldview) of shipping a default executor in the standard library that would turn into a de facto standard.
And given that Rust does have a really good package manager, adding a third-party dependency is pretty straightforward and doesn't seem like too much of an obstacle. (Frankly, the same is also true of Python, and I would certainly tell a beginner to make a virtualenv and pip install Trio. But since it didn't have it since day one, there's more of a cultural expectation to make the standard library useful out of the box.)
I really really hope this never ever happens. Tokio is great and all, but enshrining it as the default would have a stifling effect and impose a lot of design decisions all over the place.
I do hope that it becomes more possible to write async code that is portable between executors. If there were more standard traits around the most common elements of an executor it'd make things a lot better imo.
It wasn't suggested that it be Tokio, it was suggested that it be a minimal one, that could get you started without needing to make big choices before you even begin. (And yes, portability would have to be a part of that story.)
I took the post mentioning the de-facto normalization of tokio as implying that the default could be tokio, or at least based on tokio. Maybe I misinterpreted, but that was my reading of the post I was replying to.
That said, I think no matter how minimal it is it would still be a mistake. If anything, I think the only thing that would make sense to include is basically a "not-really-async" executor to appease things like the sibling thread where people want to be able to incorporate async code into their sync codebase without spawning a reactor thread. But that would also require the ability to genericize libraries over executors (and would obviously come with some significant caveats around potential deadlocks).
Correctly if I'm wrong, but IIRC low-level futures are very dependent on executor? I think you actually told me that, but that was back in futures 0.1 era.
You're probably thinking of IO streams that ultimately have their wakers woken via epoll - they do this by expecting there to be some IO reactor running the epoll loop that they can register their fd+waker with. They don't directly depend on a specific executor, just on the presence of a specific IO reactor. For something specific like tokio's IO streams, it would require tokio to provide its IO reactor as a standalone thing that could be run independent of using its `Runtime` executor.
“Leaf” futures often are, yes. This term (coming from like, a tree) is a tad more descriptive than “low level” IMHO. We have yet to achieve a fully agnostic solution everywhere. That’s part of what work in this area is trying to figure out, as I understand it.
> That’s part of what work in this area is trying to figure out, as I understand it.
Yes, there have been a couple of ideas floating around, and a couple of older RFCs that needed more revision, such as boat's `#[global_executor]`, and discussion about an extension to task::Context that would allow futures to interact with their environment (timers, etc). There is a lot of work going on in this area, but nothing concrete yet.
I hope not. Then it would be hard to turn async off.
My general position on this is that all-async (like Javascript) is OK, and all-threaded is OK, and all green threads (like Go's goroutines) are OK. But those concepts do not play well together in the same program.
GHC is ok at mixing OS threads and green threads. You launch an OS thread if you want it to make blocking system calls etc. But from the user perspective it's just like a "normal" (green) thread, uses the same mutexes etc.
> These stories are not fiction. They are an amalgamation of the real experiences of people
This claim bothers me. It might be a nitpick, but I think it's an important one. For a given amalgamation, it is fiction in a non-trivial way that there was a person who literally had all the experiences in the blog posts or tweets etc. that inspired that character. Truth values of facts are almost always dependent on the relationship to other facts.
It would be perfectly respectable to simply say that your user stories are rigorously based on real-world experience, which is a good idea BTW, without claiming that they are "nonfiction". That's a needless epistemological liability.
I admire the passion, but I’m not sure why I would want Rust at all, not just async.
When I want memory safety, async-await, I/O performance, easy multithreading, I write C#. When I want performance of CPU bound code, or lots of integration with native libraries/APIs, I write C++. Sometimes I want both in the same software, compile C++ code into a DLL (or shared library on Linux), and consume it from C#.
I don’t have experience with Rust in production software, but based on what I know and my limited experience with the language, it’s worse than the above combo. At least for the projects I usually work on.
For pieces where performance doesn’t matter too much (often 70-80% of codebase), or for I/O heavy code, or for the stuff that’s in the standard library of .NET (serialization, compression, cryptography, xml, json, zip, etc.), C# is way easier to write and debug than Rust. Tooling is awesome. Even large projects build in seconds i.e. built/test cycle is really fast.
For CPU bound or native interop-heavy pieces, C++ is also way easier than Rust. Intel and ARM only support their SIMD intrinsics for C. OS vendors only support their GPU APIs for C (most of them) or C++ (Direct3D). Many libraries I use are only available for C and/or C++. Using C++ for these pieces is the path of least resistance by a large margin, i.e. saves lots of development costs.
The interop between the two adds some friction, but not too much, .NET was designed for easy native interop from the very first version. When I want to expose objects instead of [in addition to] just functions, there’re COM interfaces. At least in my experience, usability of C# brings way more profits compared to the losses from the interop across languages.
> I am not sure why you’re downvoted. Maybe it’s because it’s sorta kinda off topic?
It doesn't follow the HN guidelines: "Have curious conversation" and "Comments should get more thoughtful and substantive". It doesn't add to the discussion about Rust's async story.
Could be, but I find this strange as well. I don’t remember a discussion on HN about any programming language at all without people commenting about Rust. Don’t remember them being downvoted.
P.S. As to why I wrote the comment — the article invited the “status quo” stories, so I decided to share my perspective.
I think people want a language with C performance and are willing to sacrifice effort but not safety. Pouring effort into C++ does not guarantee safety. I think that's why something like Rust is desired.
I don't think safety is driving Rust's ascent. For example, WASM has more momentum with Rust than with C++. But WASM is already sandboxed, and does not support threads; so what is Rust bringing to the WASM table?
I think it's a monoculture phenomenon. Rust has a website and a pitch; C++ does not. Rust has a single compiler and one way to do things; C++ is overly diverse. Rust has learned from npm, etc. and has made package management first-class with a single package manager; C++ has not. Rust is just a much more familiar experience for developers coming from other monoculture languages.
Unless you have to interface with a lot of mostly-header/header-only libraries written in C or C++, Rust is just so much more productive to work with than C or C++, even if you don't have to worry about memory unsafety.
How do you find Rust to be so much more productive than C++?
I find C++ more productive. In C++ I mainly fight with template error messages. In Rust, I fight with typechecked generics, inserting & and '_ here or there, adding and deleting imports, refactoring to add Some and Ok - tons of nonsense housekeeping. These features have value but are quite a slog when writing code.
Those "tons of nonsense housekeeping" help me detect and avoid bugs. Memory unsafety is not the only category of incorrect code – a bug is a bug, even if it doesn't enable an attacker to gain access to your system.
Some of the kinds of bugs that are much easier to accidentally write in C++ than in Rust are: use-after-free, use-after-move, out-of-bounds array accesses, data races and other synchronization errors. I want my code to be correct, even if it runs in a sandbox.
I agree. In Rust I write correct code slowly. "Productive" I am not.
But the great strengths of Rust, like memory and thread safety, are blunted in WASM, which is already memory-safe and thread-crippled. So Rust's success in WASM must be due to other factors.
> I agree. In Rust I write correct code slowly. "Productive" I am not.
I'm confused by that statement. Do you not care whether your code works correctly? Do you consider finding and fixing bugs to be separate from writing code?
Rust has lots of nonsense with zero practical benefit. Examples: PhantomData, higher-ranked trait bounds, "upstream crates may add a new impl". I have satisfied the compiler but no bugs were prevented. It's just busywork.
> "upstream crates may add a new impl" I have satisfied the compiler but no bugs were prevented.
Trial-and-error debugging which version causes a bug is busywork, preventing it right when someone introduces the potential problem is at best annoying, but very fast compared to the alternative.
> Trial-and-error debugging which version causes a bug is busywork, preventing it right when someone introduces the potential problem is at best annoying
It's a silly limitation. For example, u64 and u128 are not From<usize> because...well I have no idea. But you can't make them From, because Rust wants to reserve the right to make them From in the future. And you can't write a function that assumes they are NOT From, for the same reason.
So it's pointlessly hard to write generics over integers. I encounter lots of weird holes like this.
I meant that you are not forced to use generics, you can use composition, boxing, dyn, etc. (And unsafe too, but PhantomData seems simpler.)
> For example, u64 and u128 are not From<usize> because...well I have no idea.
Because usize is not known statically, it's target arch dependent. Yes, it's silly, but that's how technical safety works. (Also, just as a silly technical counter example the AS/400 virtual instruction set has 128 bit sized pointers.)
There's no question about the need for more ergonomics. That's what this whole post is about after all. For example in some cases where currently PhantomData is needed the intent of the programmer can be easily and unambiguously figured out, but for this folks need to be at least sufficiently certain that this makes things easier, makes code readable, doesn't constraint later language evolution, etc. (If I understand correctly some associated trait bound enhancements will lead to more readable code, less PhantomData.)
How are Some and Ok nonsense housekeeping? These things are fundamental parts of your program’s logic. Without them you are playing Russian roulette with runtime errors
Sandboxing only secures the boundary between the WASM interpreter and the embedding application (typically the browser). You can still perform significant exploits within the sandbox. See [0]
IIRC, low-level languages need to maintain a shadow stack in the heap because WASM has no native support for stack variable pointers and without ASLR, we're inching dangerously close to classic buffer overflow attacks. Rust still buys you safety in that regard.
Sorry but that's baloney. Web developers are not choosing Rust/WASM because of security concerns with C++/WASM. The whole point of WASM is to enable untrusted code.
Instead I believe they are choosing Rust/WASM because of the Rust ecosystem: familiar package management, tutorials, other resources.
> Pouring effort into C++ does not guarantee safety.
Indeed, but porting to C# does. For many practical applications, modern C# is already a language with C performance. The only large area where C# lags behind is SIMD. They made a good progress in .NET 3 and 5, but still somewhat worse than C/C++ with intrinsics.
Here’s a library which implements media player component for ARM Linux: https://github.com/Const-me/Vrmac/tree/master/VrmacVideo#per... It calls native code to present decoded video frames with GLES, and decode audio with third-party DLLs. Also consumes kernel C APIs like V4L2, ALSA, message queues from librt.so, and a few things from libc.so. Everything else is in C#: file I/O, containers parsing, decoders configuration, multithreading, buffering, A/V sync. The performance is same as VLC player which is written in C.
C++ and Rust have runtimes as well. For C++ that’s normally a DLL like libstdc++ or msvcrt. CLR is larger but still reasonable, for 64-bit Windows VC_redist.x64.exe is 14 MB, dotnet-runtime-3.1.13-win-x64.exe is 25 MB.
> and is also garbage collected by default
By default yes, but that’s avoidable. Modern .NET with their spans, value tuples, ref structs, ArrayPool, etc., make it relatively easy to write C# code which doesn’t allocate much. Or even at all, as you can see on the link in my previous comment.
I strongly disagree. The vast majority of extant .NET APIs encourage or require allocating memory dynamically, thereby requiring GC. Sure, if you rewrite everything that isn't already defined in terms of `Memory` or `Span`, you can do it, but that's a _tall_ order.
Even supposing you still mange to write a whole program that doesn't allocate dynamic memory, or that quickly stabilizes around a finite `ArrayPool`. You're still pulling in all the runtime GC machinery. Further, the minute someone _else_ contributes code, you run the risk of allocating again.
> vast majority of extant .NET APIs encourage or require allocating memory dynamically
A lot of APIs in modern .NET already supports spans. They can be backed by anything, not just GC-managed memory.
For that project, some of these spans are backed by the memory mapped by V4L2 or ALSA API calls. Others are backed by unmanaged buffers allocated on startup with Marshal.AllocHGlobal. For small temporary stuff I use stackalloc, e.g. both mp4 and mkv use tons of variable-length encoded integers.
> the minute someone _else_ contributes code, you run the risk of allocating again.
Good point. On the other hand, someone else contributing code can introduce any bugs at all, not just GC-related performance issues.
> As much as I love C#, _this isn't its niche_.
A while ago some people were saying the same about C, and were instead coding assembly :-)
You can easily forget pending tasks. That’s a single line of code like this:
task.ContinueWith( t => { }, TaskContinuationOptions.OnlyOnFaulted );
However, graceful cancellation is indeed hard. Doable in .NET but not easy, you gonna need to manually pass these cancellation tokens all the way down.
Do you have an example of a language/runtime/etc where the cancellation's easy? It's easy to kill processes from the outside, but even for threads it's borderline impossible without horrible side effects.
I don't. I don't use Rust but I'm quite sad I can't use this system in C# without paying for a stacktrace/exception. Cancellation tokens sure...but many libraries will still throw the cancellation exception.
Indeed, you need a reasonably capable OS kernel to use .NET runtime. And enough RAM. And I wouldn’t want .NET for hard realtime use cases.
Even in embedded, for some products the tradeoff is good enough. A while ago I’ve shipped embedded software that uses .NET Core and runs on RK3288, worked quite well for us.
BTW, do you have good experience with Rust on bare metal STM32? I would expect STM-supported C toolset and libraries to be generally better?
I find the Rust embedded-hal to be quite good. STM's HAL code is a horrible inefficient mess, though their LL library isn't bad. I'd much rather use Rust's hal crates and probe-run.
That said, CubeMX's configuration tool is very nice. But that's used (hopefully) once at board bringup and never again, so not worth sticking with C for.
So when we dive into C++ to write a small DLL/.so to be consumed by those languages, it is basically for doing exactly the same that would be a bunch of unsafe code blocks in Rust.
Right, but the goal and promise is to get C perf in Rust _without_ the unsafe blocks. Rust may or may not make good on that goal but the idea is why people are interested.
95% of people who think they want Rust should just be using OCaml or similar, IMO.
That said, Rust should mostly be as easy to write and debug as C# - if it's not, that's a problem to be fixed. And interop between two languages causes more subtle issues that you don't notice - your architecture ends up distorted, refactoring across the interop boundaries is painful. I used to work on mixed C++/Python projects and thought I was getting the best of both worlds - it was only when I didn't have to do that that I noticed how much it had actually slowed me down.
And of course any nontrivial piece of real-world C++ code is undefined behaviour. So having a memory-safe replacement for that part is a huge improvement.
I agree, but it’s very expensive to fix. Visual Studio is a de-facto standard in many areas for a reason.
> interop between two languages causes more subtle issues that you don't notice
I have some experience with python. C interop does work, but the usability is not OK. The languages and runtimes are just too different, which caused non-trivial amount of boilerplate to integrate the two. C# / C++ integration is much easier.
I remember a few times when I wanted to move that interop boundary, usually in the direction of “less C++”, but did nothing about that because development overhead was too large. And I agree that’s harmful in the long run.
Here’s a famous quote: https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule The idea does apply to all sufficiently complicated programs ever since: MS Office has VBA, game engines have their scripting languages, AutoCAD has LISP, and so on.
I’m pretty sure that if in the future some programming language gonna become good enough to rule them all, that language will be much closer to modern C# than to modern Rust. Rust offloads a huge amount of complexity to developers (being such a developer, I don’t like that) and to the compiler (results in slow builds).
> I agree, but it’s very expensive to fix. Visual Studio is a de-facto standard in many areas for a reason.
Well sure, and in reality IDE support is a big reason I'm using Scala rather than Haskell. But the fact is that there are maybe 2.5 good IDEs going, and that seems like as much as the programming industry can support. So either we accept that the only way to make your programming language experience any good is to get it picked up by one of the handful of giant companies that can fund those IDEs, or we have to be willing to at least start a promising language without an IDE and hope other advantages can make up for that shortcoming for some use cases, and eventually there'll be enough momentum that one of those giant companies will pick it up. I can't see what other way there is to advance the state of the art, unless your position is going to be that C# is perfect and there's no point trying to do better.
> I’m pretty sure that if in the future some programming language gonna become good enough to rule them all, that language will be much closer to modern C# than to modern Rust. Rust offloads a huge amount of complexity to developers
What complexity is that? I'd argue that the trait system is a noticeable improvement over what's available in C#, and not having exceptions makes code significantly easier to understand. On linearity I could go either way - in theory it's more work for the developer, but borrow-checker friendly code styles tend to just be good coding style. Most of the time I write the same code I'd write in C#, Scala, or anything else, and it just work - and when it doesn't either the error messages have been clear and the fix has been easy, or it became clear that I was genuinely very confused about the problem and needed to rethink my whole approach.
(But yeah actually GC is fine 100% of the time and Scala is already the one language to rule them all, people just haven't realised yet)
First and foremost ownership shenanigans. The complexity spill over the entire ecosystem. You can't write a fizz buzz without discovering the standard library has 2 types of strings because of that.
> not having exceptions makes code significantly easier to understand
Both error codes and exceptions have their place. Imagine a streaming parser of some XML/JSON/mpeg4/whatever. There's nothing you can possibly do in the parser to handle socket errors. Exceptions make parser's code more readable, essentially you pretend sockets never fail.
> confused about the problem and needed to rethink my whole approach
No amount of rethinking gonna help when you need a graph data structure in your code. Ownership shenanigans making graphs complicated in Rust, fundamentally so.
> Imagine a streaming parser of some XML/JSON/mpeg4/whatever. There's nothing you can possibly do in the parser to handle socket errors. Exceptions make parser's code more readable, essentially you pretend sockets never fail.
Disagree, because even if you can't handle errors in your code you have to do things like make sure resources are closed correctly. So you end up having to reason about exception safety, which is notoriously error-prone. If you have to have paths for propagating errors through your code, it's better to have them visible where you can reason about them, even if the actual handling has to take place at a different level.
> No amount of rethinking gonna help when you need a graph data structure in your code.
If you need a graph with cycles and you don't have a clear owner for the graph as a whole, sure. That's a pretty rare situation IME.
> you have to do things like make sure resources are closed correctly.
Not all code deals with resources. Streaming parsers normally don't. When you don't open/close any resources, manually propagating errors complicates code for no good reason.
> a graph with cycles and you don't have a clear owner
It's hard in Rust even for acyclic graphs with a clear owner, due to updates. Code that mutates the graph often needs to update multiple nodes at once, but Rust only supports a single writeable reference per object instance.
All practical Rust implementations of graphs, trees and linked lists I saw use unsafe to workaround the fundamental language limitation.
That's OK for linked lists because so simple and a standard library implementation is often enough. Graphs and trees however are very different based on use cases, can't implement just once and call it a day.
> Not all code deals with resources. Streaming parsers normally don't. When you don't open/close any resources, manually propagating errors complicates code for no good reason.
Code doesn't deal with resources until it does. Similarly with everything else that forces you to reason about control flow - you don't care about thread management until you do, you don't care about action logs until you do, you don't care about performance until you do... and from the other side, code doesn't need to be exception-safe until it does. The trouble with this kind of "magic" language feature is that correctness becomes non-compositional: you can take two working pieces of code and put them together and get something that doesn't work.
Downplaying Rust isn’t going to go over well in this thread. I happen to completely agree with you, but I would add or say that Rust is the first new programming language in a very long time that qualifies as technical innovation and not technical churn. Maybe the only one since Java. And I'm a C# guy, I wouldn't want to use anything else, it's Java done right. But standing back and looking at the big list[0], only C, Java and probably Rust qualify as true technical innovation. I'm not saying that's the only thing that matters either. Everything has its contributions, but I'm not sure many rise to my standard of technical innovation (solving a problem), and most are pure churn.
Rust solves a real problem that has plagued software for decades (or so I'm told, I'm not a Rust user), but that can be true while everything else you said is also true. I don't plan on picking up Rust either, and yes a combo like C#/C(++) is incredibly potent, and Rust is ages away from replacing C.
A language ends up being the sum of its parts though. C# is "write once, jobs everywhere". Great serverside platform with .NET 5. Native on the most popular desktop platform, produces iOS apps and games. It's industrial-strength in language design. Good IDE support, good backwards compatibility (or side by side support). And also importantly, like my experience writing Python, I enjoy writing C#.
I do consultancy across Java, .NET and C++ stacks.
There are definitly a couple of things that .NET is catching up with the Java world and could learn from.
Rust is briging ATS and Cyclone ideas into mainstream, and as those ideas prove correct, other languages are improving their type systems to support them as well.
> And I'm a C# guy, I wouldn't want to use anything else, it's Java done right.
It has colored functions. Every async function is turned by the compiler into a class with an embedded FSM. Async is viral in C#, so much so that even main() had to be made async. So, no, it's not Java done right.
Java done wrong? I've always been a Java/C# person, that's the general programming abstraction layer I've spent most of my education and career in and prefer it. I greatly prefer C# (and all that comes with it) over Java. But if C# didn't exist, I would probably prefer Java over all other alternatives. The only reason I'd fall onto Javascript upon C#'s disappearance would be because most of my experience is in web, but definitely not due to the ecosystem or language. So from a language/ecosystem perspective, Java is my 2nd choice, I never hated Java, it's popular for good reasons.
I like C# a lot (internal classes, yield, etc.) except for their async implementation. I believe Java's Project Loom will prove to be the superior solution.
> Downplaying Rust isn’t going to go over well in this thread.
People were downvoting the OP not because they were criticizing Rust, but they were off-topic. The article is about Rust async; OPs comment is about Rust.
Some have suggested that, but I never minded general takes on a given language when the thread is about a specific feature. I think the majority would be with me on this, maybe not, but he did receive upvotes again. There are few enough language version release threads that there's nowhere else for people to share their opinions, for me it's appropriate.
That is also my approach, just put Java into the mix, as I hop between eco-systems depending on the customer requirements.
I see Rust as a very good candidate for kernel code, drivers, embedded hardware where automatic memory management is a no go (MISRA-C, Ada/SPARK), and that is about it.
About normal .NET core, probably asp.net is the most known. stackoverflow.com have recently migrated to aspnet-core but I’m not sure if they migrated from Windows Server yet.
For COM APIs i.e. sharing objects around see this library + demos: https://github.com/Const-me/ComLightInterop It’s only really needed on Linux because the desktop version of the framework has COM support already built-in, but it can be used for cross-platform things just fine, I tested that quite well i.e. not just with these simple demos.
> How do you deal with the managed memory when using the gc from .net
Most of the time, automatically.
When you calling C++ from C#, the runtime automatically pins arguments like strings or arrays. Pinning means until the C++ function returns, .NET GC won’t touch these things. This doesn’t normally make any copies: C++ will receive raw pointers/native references to the .NET objects.
Sometimes you do want to retain C# objects from C++ or vice versa i.e. keep them alive after the function/method returns. An idiomatic solution for these use cases is COM interop. IUnknown interface (a base interface for the rest of COM interfaces) allows to retain/release things across languages.
Don't worry. I've just started to explore Rust (coming from 20 years of embedded C). I was just sad I didn't see bare-metal represented in the examples and status-quo stories.
Ah. We took it into account when building async generally, but it hasn't gotten as much actual usage as folks doing networking, so that's probably why it was overlooked. I'll make sure to ping them about it though... I myself am very interested in this case :)
This is bullshit because of this article:[https://theta.eu.org/2021/03/08/async-rust-2.html]. Everyone is aware of the problems of async. What we need is solutions and not those stories to distract. We want profound changes.
What I'm doing involves a virtual world viewer with maybe a dozen threads. Some are compute bound. Some are talking to the GPU. Some are waiting for I/O. The normal situation is about 2 or 3 CPUs of work. Sometimes more.
I don't want an async model, which assumes you're I/O bound, interfering with keeping all those CPUs busy. Already I've had to switch from using "reqwest", which now seems to always bring in tokio, to "ureq", a minimal HTTP client. The way to do HTTP requests used to be with "hyper", but that became a lower level for "reqwest", and then "tokio" was slipped in underneath to make it "async". It always uses async, even if you make a blocking request.
"Async" is a specialized tool for web servers with very large numbers of clients. Outside of that niche, it's seldom needed.