Just as quick pre-text I am aware of what causes async await deadlock issues but am still having the problem. Hopefully I have just overlooked something simple.
I have an interesting problem where I am extending the save functionality of Entity Frameworks IdentityDBContext. I am extending this and overriding the methods.
int SaveChanges();
Task<int> SaveChangesAsync();
Task<int> SaveChangesAsync(CancellationToken)
The problem is that it is possible for any one of those calls to call an interface method on an object that returns an awaitable Task. This gets back into the whole running an async method synchronously. I have took precautions to avoid the deadlock but lets see some code so you can see the call chain.
The below is called from a UI button click event. Task.Run() is used to avoid a deadlock issue. At this point we are on the UI context and that is what it will block on with the .Wait()
public override int SaveChanges()
{
if (!preSaveExecuting)
{
preSaveExecuting = true;
Task.Run(() => ExecutePreSaveTasks()).Wait();
preSaveExecuting = false;
}
return base.SaveChanges();
}
Now inside of the ExecutePreSaveTasks() function there is the following (useless code omitted for clarity.
private async Task ExecutePreSaveTask(){
ValidateFields(); //Synchronous method returns void
await CheckForCallbacks();
}
private async Task CheckForCallbacks(){
//loop here that gets changed entities
var eInsert = changedEntity.Entity as IEntityInsertModifier;
var eUpdate = changedEntity.Entity as IEntityUpdateModifier;
var eDelete = changedEntity.Entity as IEntityDeleteModifier;
if (eInsert != null && changedEntity.State == EntityState.Added) await eInsert.OnBeforeInsert(this);
if (eUpdate != null && changedEntity.State == EntityState.Modified) await eUpdate.OnBeforeUpdate(this);
if (eDelete != null && changedEntity.State == EntityState.Deleted) await eDelete.OnBeforeDelete(this);
}
Now this part is the kicker. In one of the above "OnBeforeInsert" calls there is a call back to the DataContext to call "SaveChangesAsync" which gets awaited.
public async Task OnBeforeInsert(RcmDataContext context)
{
await context.SaveChangesAsync();
//some more code
}
Then finally in SaveChangesAsync
public override async Task<int> SaveChangesAsync()
{
//some code that doesn't even run when this is called
return await base.SaveChangesAsync();
}
Full call stack...
ButtonClick()
SaveChanges()
Task.Run(() ExecutePreSaveTasks()).Wait()
-->ValidateFields()
-->await CheckForCallbacks()
---->await object.OnBeforeInsert(this)
------>await SaveChangesAsync()
-------->await base.SaveChangesAsync()
This await never returns! Now my understanding is that when I call
Task.Run(Action)
That I am providing a new SynchronizationContext on which the callbacks can run. This will ensure that I do not get a deadlock condition. In fact I have debugged and verified that before I do Task.Run I am on the DispatcherSynchronizationContext and when I await the true async call in SaveChangesAsync that I am on a ThreadPool context (current context is null). However the deadlock still occurs?
Is the internal SaveChangesAsync call performing some special logic that is causing this or is my understanding flawed? Thank you to those who took the time to read and try to help.
p.s. I have also tried ConfigureAwait(false) on all Tasks just to see if it would help and it did not.
await
s in that chain should await back to that thread and not the UI thread so that shouldn't cause a deadlock with theWait
. (famous last words);ConfigureAwait
is probably useful here to avoid switching threads; but not a solution to your problem. Now, it's common to capture a context before a callback is called and use that context when invoking the callback. If that context is the UI context, that would explain the deadlock because the UI is blocked onWait
... – Peter RitchieWait
and disabling the button before the await and re-enabled after the await (inSaveChanges
or the click handler if that callsSaveChanges
) is the recommended approach. – Peter Ritchieawait
is complex and works under specific scenarios w.r.t. UI. None of which are really guaranteed to work if you block the UI. Anything that isn't tied directly to the UI should be usingConfigureAwait(false)
because that code can't know the context and will cause problems in some scenarios without it. – Peter Ritchie