0

Prerequisites:

  1. .NET 6.0
  2. C# 10
  3. NUnit 3.13.3

Context: Try to run an unit test, but run into some kind of thread blocker. The code just stop execution on

value = await getDataToCacheAsync.Invoke();

The last row that can be debugged is

return () => new Task<string?>(() => cacheValue [there]);

Q: It looks like there is some kind of deadlock happened, but it's not clear for me why and how it can be addressed

Unit test:

[Test]
public async Task GetCachedValueAsync_WithDedicatedCacheKey_ReturnsExpectedCacheValue()
{
    const string cacheKey = "test-cache-key";
    const string cacheValue = "test-cache-key";

    var result = await _sut.GetCachedValueAsync(cacheKey, GetDataToCacheAsync(cacheValue));

    Assert.AreEqual(cacheValue, result);
}

private static Func<Task<string?>> GetDataToCacheAsync(string cacheValue)
{
    return () => new Task<string?>(() => cacheValue);
}

The code under test:

public async Task<T?> GetCachedValueAsync<T>(string cacheKey, Func<Task<T?>> getDataToCacheAsync)
                where T : class
{

    // [Bloked here, nothing happens then, I'm expecting that it should return "test-cache-value"]

    value = await getDataToCacheAsync.Invoke(); [Blocked]
                ...
    return value
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
AllmanTool
  • 1,384
  • 1
  • 16
  • 26
  • Related: [Awaiting an empty Task spins forever](https://stackoverflow.com/questions/27298313/awaiting-an-empty-task-spins-forever-await-new-task) – Theodor Zoulias May 08 '22 at 13:40

2 Answers2

2

After

return () => new Task<string?>(() => cacheValue [there]);

was replaces with

return () => Task.FromResult(cacheValue);

it started work

UPD:

It seems like the root cause is that a task should be started directly before awaiting it, in such cases (e.g. Task.Run(...), TaskFactory ... etc.).

Task.FromResult returns already completed task with a result

AllmanTool
  • 1,384
  • 1
  • 16
  • 26
1

As written in the docs generally you should try to avoid creating tasks via constructor:

This constructor should only be used in advanced scenarios where it is required that the creation and starting of the task is separated.

Rather than calling this constructor, the most common way to instantiate a Task object and launch a task is by calling the static Task.Run(Action) or TaskFactory.StartNew(Action) method.

The issue being that task created by constructor is a "cold" one (and according to guidelines you should avoid returning "cold" tasks from methods, only "hot" ones) - it is not started so await will result in endless wait (actually no deadlock happening here).

There are multiple ways to fix this code, for example:

  1. Use Task.Run:
return () => Task.Run(() => cacheValue);
  1. Start created task manually (though in this case there is no reason to. Also as noted by @Theodor Zoulias - it is recommended to specify explicitly the scheduler when calling the Start method. Otherwise the task will be scheduled on the ambient TaskScheduler.Current, which can be a source of some issues):
return () => 
{ 
    var task = new Task<string?>(() => cacheValue); 
    task.Start(); 
    return task;
}
  1. Return a completed task (which I think is the best way in this case, unless you are testing a very specific scenario):
return () => Task.FromResult(cacheValue);
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • 2
    +1. Regarding the second solution, [it is recommended](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2008 "CA2008: Do not create tasks without passing a TaskScheduler") to specify explicitly the `scheduler` when calling the [`Start`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.start) method. Otherwise the task will be scheduled on the ambient `TaskScheduler.Current`, which might not be what you want or expect. – Theodor Zoulias May 08 '22 at 22:47