Создание оболочки библиотеки, использующей асинхронный шаблон на основе событий, для использования с Async/Await.

Я использую шаблон async/await во всем своем коде. Однако есть один API, который использует асинхронный шаблон на основе событий. Я читал в MSDN и несколько ответов StackOverflow, что способ сделать это - использовать TaskCompletionSource.

Мой код:

public static Task<string> Process(Stream data)
{
    var client = new ServiceClient();
    var tcs = new TaskCompletionSource<string>();

    client.OnResult += (sender, e) =>
    {
        tcs.SetResult(e.Result);
    };

    client.OnError += (sender, e) =>
    {
        tcs.SetException(new Exception(e.ErrorMessage));
    };

    client.Send(data);

    return tcs.Task;
}

И называется как:

string result = await Process(data);

Или, для тестирования:

string result = Process(data).Result;

Метод всегда возвращается очень быстро, но ни одно из событий не запускается.

Если я добавлю tcs.Task.Await(); непосредственно перед оператором return он работает, но это не обеспечивает асинхронного поведения, которого я хочу.

Я сравнил с различными образцами, которые я видел в Интернете, но не вижу никакой разницы.


person Saqib    schedule 05.12.2015    source источник
comment
Ваши события выходят за рамки, когда ServiceClient удаляется/уничтожается. Это происходит потому, что поток (асинхронная задача) не ожидает завершения. Попробуйте добавить в задачу AutoResetEvent, чтобы она ждала завершения одного из обработчиков событий.   -  person The Sharp Ninja    schedule 05.12.2015
comment
Что делает tcs.Task.Await()? Нет такого метода.   -  person usr    schedule 05.12.2015
comment
Опечатка - Task.Wait(), а не Task.Await().   -  person Saqib    schedule 05.12.2015


Ответы (2)


Проблема заключается в том, что после завершения вашего метода Process ваша локальная переменная ServiceClient имеет право на сборку мусора и может быть собрана до запуска события, поэтому возникает состояние гонки.

Чтобы избежать этого, я бы определил ProcessAsync как метод расширения на тип:

public static class ServiceClientExtensions
{
    public static Task<string> ProcessAsync(this ServiceClient client, Stream data)
    {
        var tcs = new TaskCompletionSource<string>();

        EventHandler resultHandler = null;
        resultHandler = (sender, e) => 
        {
            client.OnResult -= resultHandler;
            tcs.SetResult(e.Result);
        }

        EventHandler errorHandler = null;
        errorHandler = (sender, e) =>
        {
            client.OnError -= errorHandler;
            tcs.SetException(new Exception(e.ErrorMessage));
        };

        client.OnResult += resultHandler;
        client.OnError += errorHandler;

        client.Send(data);
        return tcs.Task;
    }
}

И потреблять это так:

public async Task ProcessAsync()
{
    var client = new ServiceClient();
    string result = await client.ProcessAsync(stream);
}

Редактировать: @usr указывает, что обычно операции ввода-вывода должны сохранять ссылку на того, кто их вызвал, что не так, как мы видим здесь. Я согласен с ним, что такое поведение немного своеобразно и, вероятно, должно сигнализировать о какой-то проблеме с дизайном/реализацией объекта ServiceClient. Я бы посоветовал, если это возможно, посмотреть реализацию и посмотреть, есть ли что-то, что может привести к тому, что ссылка не останется корневой.

person Yuval Itzchakov    schedule 05.12.2015
comment
Вы уверены, что операция ввода-вывода не поддерживает рутирование клиента? Обычно все так делают. Кроме того, эта новая версия расширения также не должна ничего укоренять. - person usr; 05.12.2015
comment
Я не понимаю, как операция ввода-вывода укореняет объект, хотя на самом деле я понятия не имею, как работает его объект. Тем более из-за того, что раз он синхронно блокируется на операции, то работает. Моя версия будет root ServiceClient в стеке, так как он await в операции. - person Yuval Itzchakov; 05.12.2015
comment
Переменная стека не используется, поэтому это не корень. Кроме того, он попадает в объекты закрытия кучи, которые также не имеют корней. Стек исчез после этого момента. Нет корней. - person usr; 05.12.2015
comment
@usr client (во втором фрагменте кода) будет поднят на конечный автомат, который сам также является объектом astruct, а не объектом закрытия кучи. В случае его метода Process этого не произойдет, поскольку он инкапсулирует объект client. - person Yuval Itzchakov; 05.12.2015
comment
Когда ожидание достигает структуры, она помещается в коробку, стековый фрейм уничтожается. Теперь никто не собирается рутировать этот объект кучи. Кроме того, даже если кадр стека жил client, сборщик мусора считает его мертвым, как только начинается вызов ProcessAsync. Можно собирать объекты во время работы метода экземпляра. - person usr; 05.12.2015
comment
@usr На самом деле, перечитывая этот пост, Task, раскрытый tcs.Task, должен поддерживать работу конечного автомата, не так ли? Но поскольку в коде OP к тому времени, когда мы достигнем tcs.Task, client уже может исчезнуть. Сделав его методом расширения, объект client также будет захвачен и активен на протяжении всего жизненного цикла конечного автомата. - person Yuval Itzchakov; 05.12.2015
comment
Я до сих пор не верю. Он был помечен как ответ, но я думаю, что ни рутирование не работает (надежно), ни ServiceClient не имеет права на сбор во время ввода-вывода. IO обычно поддерживает жизнь, например, new FileStream(...).WriteAsync(...); никогда не будет прерван на полпути или что-то в этом роде. Все продолжения всегда будут работать. - person usr; 05.12.2015
comment
@usr Что касается ввода-вывода, я тоже не знаю, но что касается рутирования, я не понимаю, почему это не должно работать. Примерно так каждый метод EAP транслируется в асинхронное ожидание. Я попытаюсь воспроизвести упрощенный тест для обоих случаев и посмотреть, будет ли это иметь значение. - person Yuval Itzchakov; 05.12.2015
comment
@Saqib Я был бы рад, если бы вы могли уточнить, повлияло ли это на ваши тесты. - person Yuval Itzchakov; 05.12.2015
comment
Методы EAP обычно укореняются, поэтому они всегда работают. Если ServiceClients не укореняется, я считаю это серьезной ошибкой/упущением при проектировании API. - person usr; 05.12.2015
comment
Какой-то нативный компонент должен в конечном итоге обратиться к нативному коду, чтобы сигнализировать о завершении. Этот обратный вызов для выполнения своей работы должен использовать объект, который запустил/владеет вводом-выводом, и получить доступ к делегату для вызова дальнейших обратных вызовов и событий. Этот шаблон всегда автоматически создает корневую цепочку. Единственная особая осторожность, которую необходимо соблюдать, — убедиться, что делегат, который передается в машинный код, не собран раньше. Но это нужно делать независимо от укоренившихся опасений просто из соображений правильности. Так что это решение тоже вынужденное. - person usr; 05.12.2015
comment
Я не любитель возиться с переменными, пытаясь создать цепочки ссылок. В конце концов, не гарантируется, что все это будет работать, даже если оно работает в текущей среде CLR. Я думаю, что даже статические поля, являющиеся корнями, не гарантируются ECMA. Лучше делать вещи, которые работают по принципу, такие как шаблон ввода-вывода, который я только что описал. Или с помощью GC.KeepAlive. - person usr; 05.12.2015
comment
@usr Как бы вы использовали GC.KeepAlive в случае ОП? Поможет ли это даже с его методом Process? - person Yuval Itzchakov; 05.12.2015
comment
Я не понимаю, как это может помочь здесь. Если весь граф мертв и нет гарантии, что код будет работать в будущем, мы проиграли. GC.KeepAlive должен быть динамически доступным, чтобы гарантировать эффект. Здесь нам понадобится GCHandle. - person usr; 05.12.2015
comment
@usr Я добавил правку к ответу, которая является своего рода отказом от ответственности для всех, кто увидит этот ответ в будущем. - person Yuval Itzchakov; 05.12.2015

Думаю, я должен ответить на это.

public static Task<string> Process(Stream data)
{
    var handle = new AutoResetEvent(false);
    var client = new ServiceClient();
    var tcs = new TaskCompletionSource<string>();

    client.OnResult += (sender, e) =>
    {
        tcs.SetResult(e.Result);
        handle.Set();
    };

    client.OnError += (sender, e) =>
    {
        tcs.SetException(new Exception(e.ErrorMessage));
        handle.Set();
    };

    client.Send(data);

    handle.WaitOne(10000); // wait 10 secondds for results
    return tcs.Task;
}
person The Sharp Ninja    schedule 05.12.2015
comment
Нет, ключевое слово await делает именно это. Он ожидает завершения фоновой задачи. Если вы не хотите ждать, вызовите метод Process без ключевого слова await. - person The Sharp Ninja; 05.12.2015
comment
Я имел в виду использование WaitOne, которое предотвратит возврат метода в течение 10 секунд. await не вступает в игру, пока Process не вернется. - person usr; 05.12.2015
comment
Вы упускаете суть. Если вы используете await, то вы блокируете поток, вызывающий Process. Выполнение блока в потоке не мешает работе пользовательского интерфейса, если вы не используете await! Однако, поскольку вам нужна строка, а строку нужно ждать, у вас действительно нет действительного варианта использования для спора о блокировке. - person The Sharp Ninja; 05.12.2015
comment
@TheSharpNinja Тот факт, что вы думаете, что await блокирует поток, показывает, что вы не понимаете async\await. - person Daniel Kelley; 07.12.2015