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

Я использую шаблон async/await во всем коде. Однако есть один API, который использовал асинхронный шаблон, основанный на событиях. Я читал в MSDN и несколько ответов на Stackru, что способ сделать это - использовать 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 он работает, но это не обеспечивает асинхронное поведение, которое я хочу.

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

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 объект. Я бы посоветовал, если это возможно, посмотреть на реализацию и посмотреть, есть ли что-то, что может привести к тому, что ссылка не останется в корне.

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

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;
}
Другие вопросы по тегам