任务完成源代码抛出“尝试在任务已经完成时将任务转换为最终状态”的异常。

19

我想使用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个回答

35

问题在于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;
}

这也将解决当您的服务保留所有这些委托的引用时可能出现的内存泄漏问题。

但您需要记住,除非您有一种匹配请求和响应的方法,否则不能同时运行多个这样的操作。


@HosseinNarimaniRad 不一定。你仍然可以使用带有lambda表达式的本地TCS,尽管这并不简单。请查看我回答中的代码。 - i3arnon
它说在service.Completed-=callback;处无法隐式地将Action<...,...>转换为EventHandler<...>,所以我通过更改类型来修复它。 - Hossein Narimani Rad
它能正常工作,但是 service.Completed -= callback 看起来像是魔法。它是如何将这个新创建的变量与之前分配的变量进行比较并匹配它们的? - Hossein Narimani Rad
@HosseinNarimaniRad 回调函数就像任何其他对象一样是一个实例。它被 lambda 表达式捕获,就像 tcs 一样。它不使用变量,而是使用该变量内部的实例。 - i3arnon

5
看起来MyService会多次触发Completed事件,这导致SetResult被调用多次,进而产生错误。
我看有三个选项。一是将Completed事件改为只被触发一次(完成超过一次似乎很奇怪),二是将SetResult更改为TrySetResult,这样在第二次设置时它不会抛出异常(这会引入小的内存泄漏,因为事件仍然会被调用并且完成源还试图设置它),或者从事件中取消订阅(i3arnon的答案)。

是的,我更喜欢第三个选项。但说实话,我无法弄清楚新实例化的“callback”如何匹配以前的“callback”,并且使用“service.Completed -= callback”将被删除。我认为每次创建新的“callback”时,它都不指向相同的处理程序,因此我认为必须在类主体中声明处理程序。 - Hossein Narimani Rad
1
这是一个变量捕获,就像在lambda内部使用for循环中的i一样,你会遇到同样的“问题”。Lambda内部的变量callback是相同的变量,因此即使尚未初始化,它也可以在lambda内部使用。您是正确的,每次都会创建一个新的回调,但是您保留了对匿名回调的引用,以便稍后取消订阅(而这个“稍后时刻”恰好在回调本身内部)。 - Scott Chamberlain
每次执行 ProcessAsync,这个匿名回调的引用都是相同的吗? - Hossein Narimani Rad
2
@HosseinNarimaniRad 不是的。每次执行该行代码时都会有一个新的引用,就像每次调用该方法时都会有一个新的 CTS(Common Type System)。 - i3arnon
@ScottChamberlain 哦,是的。现在我正确理解这个想法了。感谢您的解释。 - Hossein Narimani Rad

3

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

然而,这个解决方案将会有一个由编译器生成的状态机。它会使用更多的内存和 CPU。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接