Hacker News new | past | comments | ask | show | jobs | submit login

> Concurrency problems?

I have to admit, while I do enjoy rust in the sense that it makes sense and can really "click" sometimes. For anything asynchronous I find it really rough around the edges. It's not intuitive what's happening under the hood.




Async != concurrency.

One of the major wins of Rust is encoding thread safety in the type system with the `Send` and `Sync` traits.


> Async != concurrency.

Right, but tasks are sharing the same thread which is fine, but when we need to expand on that with them actually working async, i.e non blocking, fire and quasi-forget, its tricky. That's all I'm saying.


The Rust async experience indeed has lots of pitfalls, very much agree there.


s/The Rust/All/


async == concurrency, concurrency != parallelism.


async == concurrency in the same way square == rectangle - that is it's not an associative '==' since there are plenty of rectangles that are not squares.


Rust async isn't all that pleasant to use. On the other hand for normal threaded concurrency Rust is one of the best languages around. The type system prevents a lot of concurrency bugs. "Effortless concurrency" is a tagline the language really has earned.


I really hate async rust. It's really great that rust forces you on a compiler level to use mutexes but async is a disease that is spreading through your whole project and introduces a lot of complexity that I don't feel in C#, Python or JS/TS.


Eh, syntactically async rust is the exact same as C#. It's all task based concurrency.

Now, lifetimes attached to function signatures is definitely a problem.


Not really. C#'s Task/Task<T> are based on background execution. Once something is awaited, control is returned to the caller. OTOH, Rust's Future<T> is, by default, based on polling/stepping, a bit like IEnumerable<T> in C#; If you never poll/await the Future<T>, it never executes. Executor libraries like Tokio allow running futures in the background, but that's not built-in.


How do you imagine async works otherwise? Also, in case you misunderstand how polling works in practice in rust, it's not polling in the traditional web development sense where it polls every 5 ms to check if a future is completed (although you can do this if you want to for some reason). There are typically "wakers" that are "awoken" by the os when data is ready and when they are "awoken" then they poll. And since they are only awoken by the OS when the information is ready it really never has to poll more than once unless there are multiple bundled futures.


I don't want to "well actually" the "well actually", but I think you missed the word syntactically.

> C#'s Task/Task<T> are based on background execution. Once something is awaited, control is returned to the caller.

Async/await in any language happens in the background.

What happens during a Task.Yield() (C#)? The task is yielded to the another awaiting task in the work queue. Same as Rust.

> OTOH, Rust's Future<T> is, by default, based on polling/stepping,

The await syntax abstracts over Future/Stream polling. The real difference is that Rust introduced the Future type/concept of polling at all (which is a result of not having a standard async runtime). There is a concept of "is this task available to proceed on" in C# too, it's just not exposed to the user and handled by the CLR.


> Task.Yield()

In c# you probably never call yield.


Yield in C# is frequently used for the same reasons as in Rust, although implementation details between fine-grained C# Tasks and even finer grained Rust Futures aggregated into large Tasks differ quite a bit.

Synchronous part of an async method in C# will run "inline". This means that should there be a computationally expensive or blocking code, a caller will not be able to proceed even if it doesn't await it immediately. For example:

    var ptask = Primes.Calculate(n); // returns Task<ulong[]>
    // Do other things...right?
    // Why are we stuck calculating the primes then?
    Console.WriteLine("Started.");
In order for the .Calculate to be able to continue execution "elsewhere" in a free worker thread, it would have to yield.

If a caller does not control .Calculate, the most common (and, sadly, frequently abused) solution is to simply do

    var task = Task.Run(Primes.Calculate);
    // Do something else
    var text = string.Join(',', await task);
If a return signature of a delegate is also Task, the return type will be flattened - just a Task<T>, but nonetheless the returned task will be a proxy that will complete once the original task completes. This successfully deals with badly behaved code.

However, a better solution is to instead insert `Task.Yield()` to allow the caller to proceed and not be blocked, before continuing a long-running operation:

    var ptask = Primes.Calculate(n); // returns Task<ulong[]>
    // Successfully prints the message
    Console.WriteLine("Started.");


    static async Task<int[]> CalculatePrimes(int n)
    {
        await Task.Yield();
        // Continue execution in a free worker thread
        // If the caller immediately awaits us, most likely
        // the caller's thread will end up doing so, as the
        // continuation will be scheduled in the local queue,
        // so it is unlikely for the work item to be stolen this
        // quickly by another worker thread.
    }


It was just an example. In practice, you're right.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: