Async Await Deadlock even when running on Different Context

980 Views Asked by At

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.

1

There are 1 best solutions below

0
jpino On

Well a colleague of mine found the solution to the problem. This turned out to be an issue with Entity Framework and Caliburn.Micro BindableCollection.

The Caliburn.Micro collection fires property changed events on the UI thread whenever the collection changes. When saving the data via the data context Entity Framework was mutating the collection causing it to invoke events on the UI thread. Since the UI was busy waiting for the Task to complete it couldn't invoke the method and...deadlock.

I suppose the moral of the story is understand your 3rd party libraries. After switching to an ObservableCollection the problem went away.