Async Await Deadlock даже при работе в другом контексте

Так же, как быстрый предварительный текст, я знаю, что вызывает асинхронные проблемы, ожидающие тупиковой ситуации, но проблема остается. Надеюсь, я просто упустил что-то простое.

У меня есть интересная проблема, где я расширяю функциональность сохранения Entity Frameworks IdentityDBContext. Я расширяю это и переопределяю методы.

int SaveChanges();
Task<int> SaveChangesAsync();
Task<int> SaveChangesAsync(CancellationToken)

Проблема в том, что любой из этих вызовов может вызвать метод интерфейса для объекта, который возвращает ожидаемую задачу. Это возвращает нас к синхронному запуску асинхронного метода. Я принял меры предосторожности, чтобы избежать тупика, но давайте посмотрим код, чтобы вы могли видеть цепочку вызовов.

Ниже вызывается из события нажатия кнопки пользовательского интерфейса. Task.Run() используется, чтобы избежать проблемы взаимоблокировки. На данный момент мы находимся в контексте пользовательского интерфейса, и это то, что он будет блокировать с помощью.Wait()

public override int SaveChanges()
        {
            if (!preSaveExecuting)
            {
                preSaveExecuting = true;
                Task.Run(() => ExecutePreSaveTasks()).Wait();
                preSaveExecuting = false;
            }

            return base.SaveChanges();
        }

Теперь внутри функции ExecutePreSaveTasks() есть следующее (бесполезный код для ясности опущен).

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);
}

Теперь эта часть является кикером. В одном из вышеперечисленных вызовов OnBeforeInsert происходит обратный вызов DataContext для вызова "SaveChangesAsync", который ожидается.

public async Task OnBeforeInsert(RcmDataContext context)
{
    await context.SaveChangesAsync();
    //some more code
}

Затем, наконец, в SaveChangesAsync

public override async Task<int> SaveChangesAsync()
{
    //some code that doesn't even run when this is called

    return await base.SaveChangesAsync();
}

Полный стек вызовов...

ButtonClick()
SaveChanges()
Task.Run(() ExecutePreSaveTasks()).Wait()
-->ValidateFields()
-->await CheckForCallbacks()
---->await object.OnBeforeInsert(this)
------>await SaveChangesAsync()
-------->await base.SaveChangesAsync()

Это ожидание никогда не вернется! Теперь я понимаю, что когда я звоню

Task.Run(Action)

Что я предоставляю новый SynchronizationContext, на котором могут выполняться обратные вызовы. Это гарантирует, что я не получу состояние тупика. На самом деле я отладил и проверил, что перед выполнением Task.Run я нахожусь на DispatcherSynchronizationContext и когда я ожидаю истинного асинхронного вызова в SaveChangesAsync, я нахожусь в контексте ThreadPool (текущий контекст равен нулю). Однако тупик все еще происходит?

Внутренний вызов SaveChangesAsync выполняет какую-то особую логику, которая вызывает это, или мое понимание неверно? Спасибо тем, кто нашел время, чтобы прочитать и попытаться помочь.

PS Я также попробовал ConfigureAwait(false) на всех Задачах, просто чтобы посмотреть, поможет ли это, но не помогло.

1 ответ

Ну, мой коллега нашел решение проблемы. Это оказалось проблемой с Entity Framework и Caliburn.Micro BindableCollection.

Коллекция Caliburn.Micro запускает измененные свойства в потоке пользовательского интерфейса при каждом изменении коллекции. При сохранении данных через контекст данных Entity Framework изменял коллекцию, вызывая события в потоке пользовательского интерфейса. Поскольку пользовательский интерфейс был занят ожиданием завершения Задачи, он не мог вызвать метод и... тупик.

Я полагаю, что мораль этой истории заключается в понимании ваших сторонних библиотек После переключения на ObservableCollection проблема исчезла.

Другие вопросы по тегам