Async/Await и ConfigureAwait - выполнение задачи позже с неизвестной сигнатурой метода
Я пытался найти лучший способ решить эту проблему. Я часами гуглял и наткнулся на несколько хороших ресурсов (большинство из них здесь), но я до сих пор не могу понять, какой ЛУЧШИЙ способ подойти к моей проблеме. Большинство советов, которые я нашел, либо не упоминают ConfigureAwait(false), либо просто имеют одинаковую сигнатуру метода при сохранении выполнения задачи на потом.
Что я хочу сделать, это сначала определить, какой метод запустить в задаче. Затем я хочу запустить другую задачу, а затем определенную задачу после этого. Я отредактировал приведенный ниже код, чтобы сделать его более понятным.
В моей библиотеке есть следующий сценарий (очень упрощенный):
public abstract class BaseClass
{
public int Foo;
}
public class ClassA : BaseClass { }
public class ClassB : BaseClass
{
public int Bar;
}
public async Task ProcessVariable(int variable)
{
BaseClass c = null; // initialized by child class
Task t = null;
switch (variable)
{
case 1:
c = new ClassA(variable); // parse data for ClassA and BaseClass
// potentially some special code here
t = OnClassA(c as ClassA, "foo");
break;
case 2:
c = new ClassB(variable); // parse data for ClassB and BaseClass
// potentially some special code here
t = OnClassB(c as ClassB);
break;
}
// first call different task than the one picked in switch
// can't call it before switch, as variable c is initialized in the switch body
if (c != null)
await OnAnyClass(c).ConfigureAwait(false);
// finally, call the async task we picked inside switch cases
if (t != null)
await t.ConfigureAwait(false);
}
public virtual async Task OnAnyClass(BaseClass c)
{
// this one does something
await SendAsync(c.Foo).ConfigureAwait(false);
}
public virtual async Task OnClassA(ClassA c, string additionalInfo)
{
// this one does nothing, but end user can override
await Task.CompletedTask;
}
public virtual async Task OnClassB(ClassB c)
{
// this one does something
await SendAsync(c.Bar).ConfigureAwait(false);
}
Это работает хорошо в целом. Однако OnClassA и OnClassB выполняются раньше, чем OnAnyClass. Конечно, это ожидаемое поведение, так как метод выполняется мгновенно и возвращает Task. Однако я бы хотел, чтобы OnAnyClass выполнялся раньше остальных. Я много гуглил за подсказку, как к этому подойти. Я столкнулся с несколькими решениями, но я не уверен, какое из них лучше всего подойдет для моего сценария:
1. Используйте конструктор задач
t = new Task(() => OnClassB(c as ClassB));
// and then
t.Start();
await t.ConfigureAwait(false);
Однако при таком способе я не уверен, как будет вести себя код. Я видел несколько примеров, но я только запутался в конце. Получу ли я в полной мере выгоду от ConfigureAwait(false) (что важно для меня, так как это библиотечный код), и все ли ожидающие в исполняемом методе будут вызываться правильно асинхронно?
2. Используйте конструктор задач с асинхронным делегатом
t = new Task(async () => await OnClassB(c as ClassB).ConfigureAwait(false));
// and then
t.Start();
await t.ConfigureAwait(false);
Теперь, при таком подходе я знаю, что ConfigureAwait (false) будет использоваться правильно. Тем не менее, это обернет Task в Task - и поскольку пользователи библиотеки смогут переопределять методы, я предполагаю, что это не очень желательно.
3. Используйте Func
Этот подход был бы великолепен - однако, каждый из методов имеет различную сигнатуру, и я хочу сохранить ее таким образом, чтобы это не сработало для меня, не сделав код еще более запутанным, чем он уже есть.
4. Жду методов в делах
case 1:
c = new ClassA(variable);
await OnAnyClass(c).ConfigureAwait(false);
await OnClassA(c as ClassA, "foo").ConfigureAwait(false);
break;
Такой подход гарантированно работает. Тем не менее, из-за природы моей библиотеки, могут быть десятки случаев - поэтому это приведет к снижению доступности кода и большому количеству дублированных строк кода.
5. Ждите всех методов в конце
case 1:
c = new ClassA(variable);
break;
// and then
if (c != null)
await OnAnyClass(c).ConfigureAwait(false);
if (c is ClassA)
await OnClassA(c as ClassA, "foo").ConfigureAwait(false);
if (c is ClassB)
await OnClassB(c as ClassB).ConfigureAwait(false);
Это также сработало бы наверняка и уменьшило бы количество дублирующих строк по сравнению с подходом № 4, однако этот код все еще очень раздражает в обслуживании.
В худшем случае я могу оставить все как есть, игнорируя порядок - в большинстве случаев это не должно иметь большого значения. Однако, если это возможно, я бы предпочел сохранить порядок. Какой из подходов будет работать лучше? Поскольку это библиотечный код, я требую, чтобы ConfigureAwait (false) работал правильно. Каковы ваши мнения и предложения? Надеюсь, я объяснил свою проблему понятным способом.
Заранее спасибо, и мои извинения, если я упускаю что-то очевидное - я использовал потоки в течение многих лет, и async/await является относительно новым для меня.
2 ответа
Может быть более приемлемый путь, вместо увеличения цикломатической сложности мы можем установить набор карт. Эти карты очень похожи на if
или же switch
с дополнительным преимуществом возможности хранения в сериализованном виде для настройки (хотя не всегда) и, ИМХО, гораздо более чистый процесс.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ClassLibrary1
{
public class ClassFactory
{
private IDictionary<int, Func<int, BaseClass>> ClassMap { get; } = new Dictionary<int, Func<int, BaseClass>>()
{
{1, x => new ClassA(x) },
{2, x => new ClassB(x) }
};
private IDictionary<Type, Func<BaseClass, Task>> ClassInitializeMap { get; } = new Dictionary<Type, Func<BaseClass, Task>>()
{
{typeof(ClassA) , cls => Task.Delay(1000) }, //Do Something with "Foo"
{typeof(ClassB) , cls => Task.Delay(1000) }
};
public async Task ProcessVariable(int variable)
{
var theClass = ClassMap[variable](variable);
await OnAnyClass(theClass).ConfigureAwait(false);
await ClassInitializeMap[theClass.GetType()](theClass).ConfigureAwait(false);
}
public Task OnAnyClass<T>(T anyClass) => Task.Delay(1000);
}
public abstract class BaseClass
{
public int Foo;
}
public class ClassA : BaseClass
{
public ClassA(int variable) => Foo = variable;
}
public class ClassB : BaseClass
{
public ClassB(int variable) => Bar = variable;
public int Bar;
}
}
Я думаю, что это может быть то, что вы после, и это не проверено, поэтому дайте мне знать, если увидите какие-либо упущения или проблемы.
Далее, чтобы получить переменный набор аргументов в ClassA
/ClassB
Методы инициализации, вы можете использовать аналогичный шаблон, который использует AspNet Core при внедрении IOptions
, Другой вариант - сделать это "Bag Style" с Dictionary<Type, Arg>
где Arg
является базовым классом, который содержит аргументы для каждого типа класса. В таком случае Arg
на самом деле это просто обертка вокруг словаря, так как методы инициализации будут знать ключи. Вы можете даже пойти так далеко, чтобы извлечь из базы Arg
в ClassAArg
а также ClassBArg
, Но вы можете проработать детали или дать мне знать, если вам нужен пример.
Попробуй это:
private List<Task> _tasks = new List<Task>();
public async Task ProcessVariable(int variable)
{
BaseClass c = null;
switch(variable)
{
case 1:
c = new ClassA(variable);
_tasks.Add(new Task(async () => await OnAnyClass(c)));
_tasks.Add(new Task(async () => await OnClassA(c as ClassA, "foo")));
break;
case 2:
//...
throw new NotImplementedException();
}
foreach(var task in _tasks)
{
await task;
}
}
new Task()
должен создать задачу, не запуская ее.