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.
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.
}
> 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.