Be careful when mixing ValueTask and Task.Run

 
 
  • Gérald Barré

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!

Follow me:
Enjoy this blog?