TaskCompletionSource throws "Была сделана попытка перевести задачу в конечное состояние, когда она уже выполнена"

Я хочу использовать TaskCompletionSource обернуть MyService это простой сервис:

public static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();
    //Every time ProccessAsync is called this assigns to Completed!
    service.Completed += (sender, e)=>{ tcs.SetResult(e.Result); };   
    service.RunAsync(parameter);
    return tcs.Task;
}

Этот код работает хорошо в первый раз. Но второй раз звоню ProcessAsync просто обработчик события для Completed это назначить снова (то же самое service переменная используется каждый раз) и, таким образом, она будет выполняться дважды! и второй раз выдает это исключение:

попытка перехода к конечному состоянию задачи, когда оно уже выполнено

Я не уверен, должен ли я объявить tcs как переменная уровня класса, как это:

TaskCompletionSource<string> tcs;

public static Task<string> ProccessAsync(MyService service, int parameter)
{
    tcs = new TaskCompletionSource<string>();
    service.Completed -= completedHandler; 
    service.Completed += completedHandler;
    return tcs.Task;    
}

private void completedHandler(object sender, CustomEventArg e)
{
    tcs.SetResult(e.Result); 
}

Я должен обернуть многие методы с разными типами возвращаемых данных, и таким образом я должен написать потерянный код, переменные, обработчики событий, поэтому я не уверен, является ли это наилучшей практикой в ​​этих сценариях. Так есть ли лучший способ сделать эту работу?

3 ответа

Решение

Проблема здесь в том, что Completed событие возникает на каждом действии, но TaskCompletionSource может быть завершено только один раз.

Вы все еще можете использовать местный TaskCompletionSource (и ты должен). Вам просто нужно отменить регистрацию обратного вызова перед завершением TaskCompletionSource, Таким образом, этот конкретный обратный вызов с этим конкретным TaskCompletionSource больше никогда не будет звонить

public static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();
    EventHandler<CustomEventArg> callback = null;
    callback = (sender, e) => 
    {
        service.Completed -= callback;
        tcs.SetResult(e.Result); 
    };
    service.Completed += callback;
    service.RunAsync(parameter);
    return tcs.Task;
}

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

Однако следует помнить, что вы не можете одновременно выполнять несколько таких операций. По крайней мере, если у вас нет способа сопоставить запросы и ответы.

Похоже, что MyService поднимет Completed событие более одного раза. это вызывает SetResult быть вызванным более одного раза, что вызывает вашу ошибку.

У вас есть 3 варианта, которые я вижу. Измените событие Completed так, чтобы оно вызывалось только один раз (кажется странным, что вы можете выполнить его более одного раза), измените SetResult в TrySetResult поэтому он не выдает исключение, когда вы пытаетесь установить его во 2-й раз (это приводит к небольшой утечке памяти, так как событие все еще вызывается, и источник завершения все еще пытается быть установлен), или отписывается от события ( ответ i3arnon)

Альтернативное решение для i3arnon:

public async static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();

    EventHandler<CustomEventArg> callback = 
        (s, e) => tcs.SetResult(e.Result);

    try
    {
        contacts.Completed  += callback;

        contacts.RunAsync(parameter);

        return await tcs.Task;
    }
    finally
    {
        contacts.Completed  -= callback;
    }
}

Однако это решение будет иметь конечный автомат, сгенерированный компилятором. Это будет использовать больше памяти и процессора.

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