I'm not after a solution here, more an explanation of what's going on. I've refactored this code to prevent this problem but I'm intrigued why this call deadlocked. Basically I have a list of head objects and I need to load each ones details from a DB repository object (using Dapper). I attempted to do this using ContinueWith
but it failed:
List<headObj> heads = await _repo.GetHeadObjects();
var detailTasks = heads.Select(s => _changeLogRepo.GetDetails(s.Id)
.ContinueWith(c => new ChangeLogViewModel() {
Head = s,
Details = c.Result
}, TaskContinuationOptions.OnlyOnRanToCompletion));
await Task.WhenAll(detailTasks);
//deadlock here
return detailTasks.Select(s => s.Result);
Can someone explain what caused this deadlock? I tried to get my head round what has happened here but I'm not sure. I'm presuming it's something to do with calling .Result
in the ContinueWith
async
contextThe repo calls are all along the lines of:
public async Task<IEnumerable<ItemChangeLog>> GetDetails(int headId)
{
using(SqlConnection connection = new SqlConnection(_connectionString))
{
return await connection.QueryAsync<ItemChangeLog>(@"SELECT [Id]
,[Description]
,[HeadId]
FROM [dbo].[ItemChangeLog]
WHERE HeadId = @headId", new { headId });
}
}
I have since fixed this issue with the following code:
List<headObj> heads = await _repo.GetHeadObjects();
Dictionary<int, Task<IEnumerable<ItemChangeLog>>> tasks = new Dictionary<int, Task<IEnumerable<ItemChangeLog>>>();
//get details for each head and build the vm
foreach(ItemChangeHead head in heads)
{
tasks.Add(head.Id, _changeLogRepo.GetDetails(head.Id));
}
await Task.WhenAll(tasks.Values);
return heads.Select(s => new ChangeLogViewModel() {
Head = s,
Details = tasks[s.Id].Result
});
Deadlocks occur when multiple tasks or threads cannot make progress because each task is waiting for a lock held by another task that is also stuck.
Wait and Task. Result are blocking and may also cause deadlocks and on top of that they also wrap exceptions in an AggregateException . Now if you are in a situation where you can't use async/await and you have to do sync over async, the preferred way to do it seems to be Task. GetAwaiter().
The await operator doesn't block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.
Is async await concurrent is a good question ie should I be worried about thread safety writing back to the downloadTasks? In this case no. Interestingly we can use Lists , Queues etc.. as behind the scenes it is all thread safe (the same thread!) just state machines and callbacks doing control flow..
The issue is actually a combination of the above. An enumeration of tasks was created where each time the enumeration is iterated, a fresh GetDetails
call. A ToList
call on this Select would fix the deadlock. Without solidifying the results of the enumerable (putting them in a list), the WhenAll
call evaluates the enumerable and waits for the resulting tasks asynchronously without issue, but when the returned Select statement evaluates, it's iterating and synchronously waiting on the results of tasks resulting from fresh GetDetails
and ContinueWith
calls that have not yet completed. All of this synchronous waiting is likely occurring while trying to serialize the response.
As to why that synchronous wait causes a deadlock, the mystery is in how await does things. It completely depends on what you're calling. An await is actually just retrieval of an awaiter via any scope-visible qualifying GetAwaiter
method and registration of a callback that immediately calls GetResult
on the awaiter when the work is complete. A qualifying GetAwaiter
method can be an instance or extension method that returns an object having an IsCompleted
property, a parameterless GetResult
method (any return type, including void - result of await), and either INotifyCompletion
or ICriticalNotifyCompletion
interfaces. The interfaces both have OnComplete
methods to register the callback. There's a mind-boggling chain of ContinueWith
and await calls going on here and much of it depends on the runtime environment. The default behavior of the await you get from a Task<T>
is to use SynchronizationContext.Current
(I think via TaskScheduler.Current
) to invoke the callback or, if that's null to use the thread pool (I think via TaskScheduler.Default
) to invoke the callback. A method containing an await gets wrapped as a Task by some CompilerServices
class (forgot the name), giving callers of the method the above described behaviour wrapping whatever implementation you are awaiting.
A SynchronizationContext
can also customize this, but typically each context invokes on it's own single thread. If such an implementation is present on SynchronizationContext.Current
when await
is called on a Task
, and you synchronously wait for the Result
(which itself is contingent on an invoke to the waiting thread), you get a deadlock.
On the other hand, if you broke your as-is method out to another thread, or call ConfigureAwait
on any of the tasks, or hide the current scheduler for your ContinueWith
calls, or set your own SynchronizationContext.Current
(not recommended), you change all the above.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With