TaskCompletionSource 抛出 "An attempt was made to transition a task to a final state when it had already completed"

TaskCompletionSource throws "An attempt was made to transition a task to a final state when it had already completed"

我想用 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;
}

这段代码第一次运行良好。但是我调用 ProcessAsyncsecond 只是再次分配 Completed 的事件处理程序(每次都使用相同的 service 变量)并且因此它将执行两次!第二次抛出这个异常:

attempt transition task final state when already completed

我不确定,我是否应该像这样将 tcs 声明为 class 级别变量:

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); 
}

我必须用不同的 return 类型包装许多方法,这样我就必须编写丢失的代码、变量、事件处理程序,所以我不确定这是否是这种情况下的最佳做法。那么有没有更好的方法来完成这项工作?

这里的问题是每个操作都会引发 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 so it does not throw a exception when you try to set it a 2nd time (this does introduce a small memory leak as the event still gets called and the completion source still tries to be set), or unsubscribe from the event ()

i3arnon's 的替代解决方案是:

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;
    }
}

但是,此解决方案将有一个编译器生成的状态机。它将使用更多内存和 CPU.