【问题标题】:Wrap a callback into async Task将回调包装到异步任务中
【发布时间】:2019-09-12 21:53:52
【问题描述】:

我无法控制来自 API 的以下方法。

public void Start(Action OnReady);

总的来说,我对回调很好,但有时你有触发回调的回调等等。因此,我想将它包装到异步方法中,并且可能还包括取消操作的可能性。像这样的:

await Start(cancellationToken);

这是我想出的:

public Task Start(CancellationToken cancellationToken)
{
     return Task.Run(() =>
     {
           cancellationToken.ThrowIfCancellationRequested();

           var readyWait = new AutoResetEvent(false);

           cancellationToken.Register(() => readyWait?.Set());

           Start(() => { readyWait.Set(); }); //this is the API method

           readyWait.WaitOne();
           readyWait.Dispose();
           readyWait = null;

           if(cancellationToken.IsCancellationRequested)
           {
                APIAbort(); //stop the API Start method from continuing
                cancellationToken.ThrowIfCancellationRequested();
            }

      }, cancellationToken);
}

我认为还有改进的余地,但我想到的一件事是这种方法在这种情况下有什么作用?

readyWait.WaitOne(); 

我想编写一个异步方法来不阻塞任何线程,但这正是 WaitOne 所做的。当然它不会因为任务而阻塞调用线程,但是任务是否获得了自己的线程?如果只有任务会被阻止我会很好,但我不想阻止可能在其他地方使用的线程。

【问题讨论】:

  • 一个回调已经是异步的,那么为什么要在异步方法中包装一个异步方法呢?任何事件都不应阻塞。阻塞在主线程中完成。一个事件通常运行最少的代码然后返回。
  • 你可以使用TaskCompletionSource,stackoverflow.com/questions/15316613/…

标签: c# async-await


【解决方案1】:
public Task StartAsync(CancellationToken c)
{
    var cs = new TaskCompletionSource<bool>();

    c.Register(() => { Abort(); cs.SetCanceled(); } );
    Start(() => { cs.SetResult(true); });

    return cs.Task;
}
using (var ct = new CancellationTokenSource(1000))
{
    try
    {
        await StartAsync(ct.Token);

        MessageBox.Show("Completed");
    }
    catch (TaskCanceledException)
    {
        MessageBox.Show("Cancelled");
    }
}

没有必要使用取消标记,因为它可以触发的唯一点是在方法被调用之后和完成之前,此时它是一个竞争条件。

【讨论】:

  • 我认为这对 WaitOne 有效,但是如果 Start 方法需要 10 分钟并且我希望能够中断它,我该如何提前完成此任务?我想我可以在我的 Token.Register 方法中调用 SetResult,但你能解释一下竞态条件吗?
  • @0lli.rocks 有一种方法可以支持取消令牌,但问题是,您调用了一个不支持令牌的外部例程 Start,即使您取消了您的 StartAsync 包装器,Start 方法将继续运行并最终调用您传递的操作,这可能是非常不需要的。
  • @0lli.rocks 不,它没有。 Abort 请求终止在Task.Run 中运行的线程。此线程已从Start 返回。我们不知道Start 内部会发生什么,但显然它会产生一个自己的工作线程,最终将调用 Action;您不会终止该工作线程。此外,调用Abortnever a good idea
  • 我认为该方法的名称令人困惑。它不是 Thread.Abort 它是 API 中的另一种方法。我将在问题中澄清这一点。即使名称相似,Abort 也不会停止任何线程(即使在 API 中)它会尝试关闭 Start 启动的连接。
  • @GSerg 为什么不是一个 TCS?
【解决方案2】:

这个问题出奇的棘手。案子很多,每个案子的处理并不总是一目了然。

很明显:

  1. Start 方法可能需要很长时间才能完成。 APIAbort 方法也是如此。
  2. CancellationToken 可以在 Start 方法之前、期间或之后取消,甚至在 OnReady 回调调用之后。
  3. 不应在Start 之前或期间,或在OnReady 回调调用之后调用APIAbort
  4. 即使在 Start 方法完成之前发生取消,也应该调用 APIAbort

不明显:

  1. 返回的Task 应该在APIAbort 调用之前还是之后完成为取消?
  2. 如果APIAbort 抛出异常怎么办?如果返回的Task 已被取消,如何传播此异常?
  3. 如果Start 抛出OperationCanceledException 异常怎么办?这是故障还是取消?

下面的实现在调用APIAbort 方法之前取消Task,抑制APIAbort 期间可能发生的异常,并处理 OperationCanceledExceptionStart 期间取消。

public Task StartAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
        return Task.FromCanceled(cancellationToken);

    var tcs = new TaskCompletionSource<bool>();

    var cancellationRegistration = cancellationToken.Register(() =>
        tcs.TrySetCanceled(cancellationToken));

    var fireAndForget = Task.Run(() =>
    {
        if (cancellationToken.IsCancellationRequested) return;
        try
        {
            Start(() =>
            {
                cancellationRegistration.Dispose(); // Unregister
                tcs.TrySetResult(true);
            });
        }
        catch (OperationCanceledException)
        {
            tcs.TrySetCanceled();
            return;
        }
        catch (Exception ex)
        {
            tcs.TrySetException(ex);
            return;
        }

        // At this point Start is completed succesfully. Calling APIAbort is allowed.
        var continuation = tcs.Task.ContinueWith(_ =>
        {
            try
            {
                APIAbort();
            }
            catch { } // Suppressed
        },  default, TaskContinuationOptions.OnlyOnCanceled
        | TaskContinuationOptions.RunContinuationsAsynchronously,
        TaskScheduler.Default);
    }, cancellationToken);

    return tcs.Task;
}

设置选项TaskContinuationOptions.RunContinuationsAsynchronously的原因是为了避免APIAbortawait StartAsync()之后的代码同步运行(在同一个线程中)的可能性。当我使用async-await 作为延续机制时,我最初运行到this problem

【讨论】:

    【解决方案3】:

    我想把它包装成一个异步方法,也许还包括取消操作的可能性。

    我建议将它们分开。原因是您的“取消”实际上并没有取消Start。它只会取消 wait 以等待 Start 完成。所以在这个级别取消会产生误导。

    您可以使用与pattern for wrapping an event into a Task 类似的方法将委托回调包装到Task

    public static Task StartAsync(this ApiObject self)
    {
      var tcs = new TaskCompletionSource<object>();
      self.Start(() => tcs.SetResult(null));
      return tcs.Task;
    }
    

    现在您可以拨打StartAsync 并取回Task,如果您愿意,可以选择不继续等待:

    var startTask = apiObject.StartAsync();
    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10));
    var completedTask = await Task.WhenAny(startTask, timeoutTask);
    if (completedTask == timeoutTask)
      return;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-06-26
      • 1970-01-01
      • 2023-03-30
      • 2023-03-11
      • 1970-01-01
      • 2016-01-03
      • 2017-07-22
      相关资源
      最近更新 更多