Be careful when mixing ValueTask and Task.Run

 
 
  • Gérald Barré

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

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 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:

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 multiple ways to fix the issue. The main idea is to be sure that the ValueTask is 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!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub