Be careful when mixing ValueTask and Task.Run
Asynchronous methods have existed for a few years in the .NET ecosystem. Many methods know how to handle Task
. For instance, Task.Run
has overloads to handle tasks correctly:
Task Task.Run(Func<Task> action)
Task<TResult> Task.Run<TResult>(Func<Task<TResult>> action)
// 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. You can use these types instead of Task
when it's likely that the result of its operation will be available synchronously, and when it's expected to be invoked so frequently that the cost of allocating a new Task<TResult>
for each call will be prohibitive. The BCL contains many methods that return a ValueTask
such as IAsyncEnumerator<T>.MoveNextAsync
, IAsyncDisposable.Dispose
or Stream.ReadAsync
.
A ValueTask
is not a Task
, so when you use it in a Task.Run
the compiler resolves the Task.Run(Func<TResult>)
instead of Task.Run(Func<Task<TResult>>)
. This means the value task won't be awaited by the Task.Run
method.
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 needs to get the result of the ValueTask
you will see the issue as it won't compile. But there are multiple cases where you just don't read the value, so you don't easily spot the bug:
// 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 multiple ways to fix the issue. The main idea is to be sure that the ValueTask
is awaited:
// 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
- Understanding the Whys, Whats, and Whens of ValueTask
- Report ValueTask returned by lambda passed to Task.Run
Do you have a question or a suggestion about this post? Contact me!