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() должен создать задачу, не запуская ее.

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