4 ms·
There are domains where C# (and F#) productivity stems from similar reasons why writing a game in something that isn't Rust might be more productive without eve
by neonsunset 2y ago
There are domains where C# (and F#) productivity stems from similar reasons why writing a game in something that isn't Rust might be more productive without even sacrificing performance (or, at least, not to the drastic extent).
I can give you an example:
var number = 0;
var delay = Task.Delay(1000);
for (var i = 0; i < 10; i++)
{
Task.Run(() =>
{
while (!delay.IsCompleted)
{
Interlocked.Increment(ref number);
}
});
}
await delay;
How would you write this idiomatically in Rust without using unsafe?
To avoid misunderstanding, I think Rust is a great language and even if you are a C# developer who does not plan to actively use it, learning Rust is of great benefit still because it forces you to tackle the concepts that implicitly underpin C#/F# in an explicit way.
- pdimitar 2y ago> How would you write this idiomatically in Rust without using unsafe? Channels and selects. It's trivial.
- neonsunset 2y agoPlease post a snippet.
- brink 2y agouse std::{ sync::{ Arc, atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Duration, }; fn main() { let num = Arc::new(AtomicUsize::new(0)); let finished = Arc::new(AtomicBool::new(false)); for _ in 0..10 { std::thread::spawn({ let num = num.clone(); let finished = finished.clone(); move || { while !finished.load(Ordering::SeqCst) { num.fetch_add(1, Ordering::SeqCst); } } }); } std::thread::sleep(Duration::from_millis(1000)); finished.store(true, Ordering::SeqCst); }
- neonsunset 2y agoWhat if we want to avoid explicitly spawning threads and blocking the current one every time we do this? Task.Run does not create a new thread besides those that are already in the threadpool (which can auto-scale, sure, but you get the idea, assuming the use of Tokio here).
- brink 2y agoWhat you're asking for is thread parking. Use tokio for that, it's still trivial.
- neonsunset 2y agoI was implying that yes, while it is doable, it comes at 5x cognitive cost because of micromanagement it requires. This is somewhat doctored example but the "decision fatigue" that comes with writing Rust is very real. You write C# code, like in the example above, quickly without having to ponder on how you should approach it and move on to other parts of the application while in Rust there's a good chance you will be forced to deal with it in a much stricter way. It's less so of an issue in regular code but the moment you touch async - something that .NET's task and state machine abstractions solve on your behalf you will be forced to deal with by hand. This is, obviously, a tradeoff. There is no way for .NET to use async to implement bare metal cooperative multi-tasking, while it is very real and highly impressive ability of Rust. But you don't always need that, and C# offers an ability to compete with Rust and C++ in performance in critical paths when you need to sit down and optimize it unmatched by other languages of "similar" class (e.g. Java, Go). At the end of the day, both languages have domains they are strong at. C# suffers from design decisions that it cannot walk back and subpar developer culture (and poor program architecture preferences), Rust suffers from being abrasive in some scenarios and overly ceremonious in others. But other than that both provide excellent sets of tradeoffs. In 2025, we're spoiled with choice when it comes to performant memory-safe programming languages.
- brink 2y agoIt's literally not 5x the cost, it would take me 3 minutes to whip up a tokio example. I've done both. I like C# too, I totally understand why you like it so much. This is not a C# vs Rust argument for me. All I'm saying is that Rust is a productive language.
- whytevuhuni 2y agoThere's a few things here that make this hard in Rust: First, the main controller may panic and die, leaving all those tasks still running; while they run, they still access the two local variables, `number` and `delay`, which are now out of scope. My best understanding is that this doesn't result in undefined behavior in C#, but it's going to be some sort of crash with unpredictable results. I think the expectation is that tasks use all cores, so the tasks also have to be essentially Send + 'static, which kinda complicates everything in Rust. Some sort of scoped spawning would help, but that doesn't seem to be part of core Tokio. In C#, the number variable is a simple integer, and while updating it is done safely, there's nothing that forces the programmer to use Interlocked.Read or anything like that. So the value is going to be potentially stale. In Rust, it has to be declared atomic at the start. Despite the `await delay`, there's nothing that awaits the tasks to finish; that counter is going to continue incrementing for a while even after `await delay`, and if its value is fetched multiple times in the main task, it's going to give different results. In C#, the increment is done in Acquire-Release mode. Given nothing waits for tasks to complete, perhaps I'd be happy with Relaxed increments and reads. So in conclusion: I agree, but I think you're arguing against Async Rust, rather than Rust. If so, that's fair. It's pretty much universally agreed that Async Rust is difficult and not very ergonomic right now. On the other hand, I'm happy Rust forced me to go through the issues, and now I understand the potential pitfalls and performance implications a C#-like solution would have. Does this lead to the decision fatigue you mention in another sub-thread? It seems like it would, so I'll give you that.
- whytevuhuni 2y agoFor posterity, here's the Rust version I arrived at: let number = Arc::new(AtomicUsize::new(0)); let finished = Arc::new(AtomicBool::new(false)); let finished_clone = Arc::clone(&finished); let delay = task::spawn(async move { sleep(Duration::from_secs(1)).await; finished_clone.store(true, Ordering::Release); }); for _ in 0..10 { let number_clone = Arc::clone(&number); let finished_clone = Arc::clone(&finished); task::spawn(async move { while !finished_clone.load(Ordering::Acquire) { number_clone.fetch_add(1, Ordering::SeqCst); task::yield_now().await; } }); } delay.await.unwrap(); https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7c5b950b69c8db2feeafe49864dd1593 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- anarki8 2y agoI am not sure what are you trying to represent with this example, but here is the exact same thing without any unsafe: use rayon::prelude::*; use std::time::{Instant, Duration}; use std::sync::atomic::{AtomicUsize, Ordering}; fn main() { let finish_at = Instant::now() + Duration::from_secs(1); let number = AtomicUsize::new(0); (0..10).into_par_iter().for_each(|_| { while finish_at > Instant::now() { number.fetch_add(1, Ordering::Relaxed); } }); } You can make it even simpler if you would use Mutex instead of atomics. Atomics are more performant though.