Asynchronous programming has been a core part of .NET for several years. Many methods are designed to handle Task correctly. For instance, Task.Run has overloads for tasks:
Task Task.Run(Func<Task> action)Task<TResult> Task.Run<TResult>(Func<Task<TResult>> action)
C#
// Task Task.Run(Func<Task>)
// Wait for the inner task to complete
await Task.Run(async () =>
{
await Task.Delay(1000);
Console.WriteLine("demo");
});
// Task.Run(Func<Task<int>>)
// Wait for the inner task to complete, so b = 42
int b = await Task.Run(() =>
{
await Task.Delay(1000);
return 42;
});
More recently, .NET introduced the ValueTask and ValueTask<T> types. These are useful alternatives to Task when the result is likely available synchronously and the method is called frequently enough that allocating a new Task<TResult> per call becomes costly. The BCL contains many methods that return a ValueTask, such as IAsyncEnumerator<T>.MoveNextAsync, IAsyncDisposable.Dispose, and Stream.ReadAsync.
A ValueTask is not a Task, so when you use it inside Task.Run, the compiler resolves Task.Run(Func<TResult>) instead of Task.Run(Func<Task<TResult>>). This means Task.Run will not await the value task.
C#
ValueTask<int> ReturnValueTask() => new ValueTask<int>(42);
// Task<TResult> Task.Run<TResult>(Func<TResult> action) where TResult is ValueTask
ValueTask<int> value = await Task.Run(() => ReturnValueTask());
// So, we need to await the ValueTask<int>
var valueTaskResult = await value;
If your code reads the result of the ValueTask, the issue is obvious because it won't compile. However, when you discard the result, the bug is easy to miss:
C#
// Doesn't compile, so it's easy to spot the issue
int value = await Task.Run(() => new ValueTask<int>(42));
// ⚠ Compile, but don't await the ValueTask
_ = await Task.Run(() => new ValueTask<int>(42));
// ⚠ Compile, but don't await the ValueTask
await Task.Run(() => new ValueTask<int>(42));
// ⚠ Compile, but don't await the ValueTask
ValueTask<int> ReturnValueTask() => new ValueTask<int>(42);
await Task.WhenAll(
Task.Run(() => ReturnValueTask()),
Task.Run(() => ReturnValueTask()));
There are a few ways to fix this. The key is ensuring the ValueTask is properly awaited:
C#
// Ok as it uses Task.Run(Func<Task>)
await Task.Run(() => ReturnValueTask.AsTask());
// Ok as it uses Task.Run(Func<Task>)
await Task.Run(async () => await ReturnValueTask());
// Ok as it awaits the ValueTask (but this is ugly...)
await await Task.Run(() => ReturnValueTask());
Be careful when using ValueTask and Task.Run or other similar methods that are not ValueTask aware!
#Additional Resources
Do you have a question or a suggestion about this post? Contact me!