Цепочка задач и возврат результата

У меня есть вопрос о цепочке задач с TPL. Он должен работать с Entity Framework Core, поэтому мы не поддерживаем одновременные операции в одном и том же контексте базы данных.

Я хочу быть максимально асинхронным. Ниже приведен код без асинхронности:

Context.Set<T>().Add(entity);
Context.SaveChanges();
return entity;

Я хочу сделать эту асинхронную цепочку методов AddAsync, SaveChangesAsync. Я должен убедиться, что AddAsyncзавершен, прежде чем вызывать SaveChangesAsync.

Моя первая попытка была следующей:

public Task<T> AddAsync(T entity)
{
   return _dbContext.Set<T>()
          .AddAsync(entity)
          .ContinueWith(addTask =>
          {
              _dbContext.SaveChangesAsync();
              return addTask.Result.Entity;
          });
}

Но я не думаю, что это правильно. SaveChangesAsync() не содержится в возвращаемой задаче. Если я позвоню: await AddAsync(myEntity);, я уверен, что SaveChangesAsync() будет готово? Я так не думаю.

Итак, я пытался решить эту проблему, но в итоге получил вложенные задачи, которые не очень удобны для пользователя. Возможное решение будет примерно таким (псевдокод, не компилируется):

return Context.Set<T>()
                .AddAsync(entity)
                .ContinueWith(addTask =>
                {
                    return Context.SaveChangesAsync()
                        .ContinueWith(saveTask =>
                        {
                            return addTask.GetAwaiter().GetResult().Entity;                            
                        });
                });

Есть ли способ добиться такого поведения с помощью TPL? И если да: я просто переписываю await? Это API, который следует использовать во многих проектах. Я хочу сделать API как можно более производительным (по крайней мере, в теории. Я использую его для обучения). Пользователи должны использовать await вне API. Чего я хочу избежать, так это создания узких мест внутри API из-за ожидания. Я не уверен, не является ли ContinueWith просто другой реализацией await. Еще одна вещь, которую я избегаю при использовании ContinueWith вместо await, — это помечать мои методы как async.


person El Mac    schedule 14.07.2018    source источник
comment
...SaveChangesAsync() не содержится в возвращенной задаче. Это. ContinueWith возвращает задачу, отличную от исходной, которая включает в себя продолжение. Но вкратце, async/await специально разработан для обработки продолжений естественным (и менее подверженным ошибкам) ​​способом. Пометка вашего метода async или нет не влияет на вызывающие программы. await и ожидание — разные вещи. Мой совет - забудьте сырой TPL и используйте async/await при реализации таких методов.   -  person Ivan Stoev    schedule 14.07.2018
comment
Я думаю, что если я await вернул задачу из этой ContinueWith, метод SaveChangesAsync() не обязательно будет завершен, потому что он выполняется внутри другой задачи.   -  person El Mac    schedule 14.07.2018
comment
Так и будет, потому что возвращенная задача завершится, когда будут завершены как оригинал, так и продолжения. Тот факт, что вы так думаете, — это еще одна причина использовать async/await :) Потому что да, вы заново изобретаете await. Другими словами, код после await эквивалентен ContinueWith, но в более естественном и оптимизированном для компилятора виде.   -  person Ivan Stoev    schedule 14.07.2018
comment
await не ждать. это означает продолжить, когда это будет сделано. Вы можете найти много полезных объяснений того, как async/await работает в SO и блогах.   -  person Ivan Stoev    schedule 14.07.2018
comment
@IvanStoev спасибо за вклад. Можете ли вы прочитать мой комментарий к ответу Камило и сказать мне, что вы думаете об этом? Я думаю, что могу многому научиться из этого обсуждения. Дело в том, что он все еще ждет, пока он не будет завершен, в то время как я мог бы сначала закончить другую работу, которую я пытаюсь сделать, прежде чем мне нужен результат.   -  person El Mac    schedule 14.07.2018
comment
Пожалуйста. Вы можете взглянуть на Поток управления в асинхронных программах (C#), особенно шаги примера с изображениями и стрелками, показывающими, что происходит. Ваше здоровье.   -  person Ivan Stoev    schedule 14.07.2018


Ответы (1)


Я не уверен, почему AddAsync является частью Entity Framework Core, так как предполагается, что он только добавляет элемент в средство отслеживания изменений (в памяти), поэтому он не выполняет никаких реальных асинхронных операций.
Я бы поступил так. говоря, что вы не должны полагаться на него, поскольку есть драйверы (например, официальный MySQL), которые блокируются при вызове AddAsync.

Если вам нужна реальная асинхронная реализация, просто используйте:

public async Task<T> AddAsync(T entity)
{
    _dbContext.Set<T>().Add(entity);
    await _dbContext.SaveChangesAsync();

    return entity;
}
person Camilo Terevinto    schedule 14.07.2018
comment
Игнорирование AddAsync может быть решением. Причина в том, чтобы позволить генераторам значений получать асинхронный доступ к базе данных, но я мог бы просто игнорировать это для своего проекта. Я хочу избежать await, потому что я думаю, что абстракция слишком высока. Пользователь должен await внутри своего кода. Знаете ли вы, можно ли связать задачи так, чтобы выполнение выглядело так: AddAsync -> SaveChangesAsync -> Task<T>? Это возможно в Java, и я считаю, что это возможно и в C#. - person El Mac; 14.07.2018
comment
@ElMac Я думаю, вы неправильно понимаете, как работают async и await. Пользователь должен await вашего AddAsync, независимо от того, помечен он async и использует await или нет. Тот факт, что AddAsync возвращает Task<T>, заставляет пользователя использовать await для его вызова. Это, безусловно, возможно, хотя на самом деле не полезно для вашего сценария. - person Camilo Terevinto; 14.07.2018
comment
Существуют и другие варианты обработки Задач, кроме прямого использования await. Например, вы можете отложить await на более позднее время в приложении и иметь сложную логику между запуском выполнения задачи и чтением результата. Вы даже можете распространять задачу через несколько уровней вашего приложения без необходимости await. Я придерживаюсь мнения, что await часто используется слишком небрежно. - person El Mac; 14.07.2018
comment
@ElMac Я знаю, я знаю об этом. Тем не менее, я все еще считаю, что вы слишком усложняете эту простую ситуацию. - person Camilo Terevinto; 14.07.2018
comment
@ElMac Настоящие возвращенные асинхронные задачи уже находятся в рабочем (или завершенном) состоянии, поэтому вы не можете отложить их выполнение. Вы можете отложить await, Wait или Result до момента, когда вам действительно нужен результат. Камило прав, что вы неправильно понимаете, как работает async/await. Первый await в методе async — это место, где результирующая задача, представляющая целый метод, возвращается вызывающей стороне. Сама задача может иметь много асинхронных вызовов — это полностью скрыто от вызывающего. Задача завершается с оператором return или с исключением. - person Ivan Stoev; 14.07.2018
comment
@CamiloTerevinto Я решил эту проблему, используя await для AddAsync и для SaveChangesAsync. Я просто думал не об этой конкретной ситуации, а вообще. Как я уже сказал, я просто хочу учиться, я не иду по пути бизнеса. В любом случае спасибо за интересную дискуссию. - person El Mac; 14.07.2018