65.9K
CodeProject 正在变化。 阅读更多。
Home

ContinueWith 与 await

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.29/5 (8投票s)

2015年8月11日

CPOL

6分钟阅读

viewsIcon

72399

downloadIcon

371

下面将讨论 TPL 的 Task 类中可用的 ContinueWith 函数以及 C# 5.0 中引入的 await 关键字,用于支持异步调用。

TPL 是 C# 4.0 版本中引入的新库,用于提供对线程的良好控制,通过线程并行执行来利用多核 CPU。下面的讨论不是关于 TPL,而是关于 TPL 的 Task 类中可用的 ContinueWith 函数以及 C# 5.0 中引入的 await 关键字,用于支持异步调用。

ContinueWith - 它是 Task 上可用的一个方法,允许在 Task 完成执行后执行代码。简单来说,它允许延续。

这里需要注意的是,ContinueWith 也返回一个 Task。这意味着你可以将 ContinueWith 附加到此方法返回的 Task 上。

示例

public void ContinueWithOperation()
{
   Task<string> t = Task.Run(() => LongRunningOperation("Continuewith", 500));
   t.ContinueWith((t1) =>
   {
       Console.WriteLine(t1.Result);
   });
}

在上面的代码中,新创建的 Task 运行 LongRunningOperation,一旦 Task 执行完成,ContinueWith 就在返回的 Task 上执行操作,并打印 Task 的结果。

ContinueWith 操作的任务默认由线程调度器执行。你也可以提供其他调度器来在上面运行任务,这将在本文稍后讨论。

注意:下面的代码是 Task 调用 LongRunningOperation。这里的 LongRunningOpertion 只是一个示例,在实际程序中,你不能在 Task 上调用长时间运行的操作,如果你想调用长时间运行的任务,则需要传递 TaskCreationOperation.LongRunning

private string LongRunningOperation(string s, int sec)
{
  Thread.Sleep(sec);
  return s + " Completed";
}

await – 这是一个关键字,它导致运行时在新的 Task 上运行操作,并导致执行线程返回并继续执行(在大多数情况下,执行的是应用程序的主线程)。一旦 await 操作完成,它会返回到它离开的地方(即返回调用者,返回保存的状态),然后开始执行语句。

因此,await 会等待新创建的 Task 完成,并在等待的任务执行完成后确保延续。

await 关键字与 async 一起用于在 C# 中实现异步编程。之所以称为异步编程,是因为运行时会在遇到 await 关键字时捕获程序的[状态](https://codeproject.org.cn/Articles/747835/ContinueWith-Vs-await)(这类似于迭代器中的 yield),并在等待的任务完成后恢复状态,以便延续在正确的上下文中运行。

示例

public async void AsyncOperation()
{
    string t = await Task.Run(() => LongRunningOperation("AsyncOperation", 1000));
    Console.WriteLine(t);
}

在上面的示例中,新创建的 Task 调用 LongRunningOperation,当主线程遇到 await 关键字时,执行会中断(即主线程返回到 AsyncOpetion 的调用者)并继续执行。一旦 LongRunningOpertaion 完成,Task 的结果就会打印到控制台。

因此,由于在遇到 await 时保存了状态,流程会在同一上下文中返回,并且 Task 的一个操作就会被执行。

注意:状态包含有关 executioncontext/synchronizationcontext 的详细信息。

因此,从上面可以清楚地看出,Task.ContinueWith 和 await Task 都会等待 Task 完成,并在 Task 完成后允许延续。但是它们的工作方式不同。

ContinueWith 和 await 之间的区别

  1. 保存状态以在执行上下文中返回

    ContinueWith 不保存任何状态,除非提供了调度器,否则使用 ContinueWith 附加的延续操作会在默认线程调度器上运行。

    await – 遇到此关键字时,状态会被保存,并且一旦 await 所在的 Task 完成,执行流就会加载保存的状态数据并开始执行 await 之后的语句。(注意:状态包含有关 executioncontext/synchronizationcontext 的详细信息。)

  2. 将完成的 Task 结果发布到 UI 控件

    下面是使用 ContinueWith 和 await 在 UI 控件上显示已完成 Task 结果的示例。

    ContinueWith

    考虑下面的代码,它会在 UI 标签上显示已完成 Task 的结果。

    public void ContinueWithOperation()
           {
             CancellationTokenSource source = new CancellationTokenSource();
             source.CancelAfter(TimeSpan.FromSeconds(1));
             Task<string> t = Task.Run(() => LongRunningOperation("Continuewith", 500
    ));
    
                t.ContinueWith((t1) =>
                {
                    if (t1.IsCompleted && !t1.IsFaulted && !t1.IsCanceled)
                        UpdateUI(t1.Result);
                });
    }
    
    private void UpdateUI(string s)
           {
             label1.Text = s;
    }

    注意:LogRunningOperation 已在上面提供。

    当上述代码被执行时,会出现以下运行时异常。

    此异常发生是因为 Continuation 操作 UpdateUI 操作在不同的线程上运行,在这种情况下,线程将由默认线程调度器(ThreadPool)提供,它没有任何关于要在其上运行的 Synchronization context 的信息。

    要避免异常,必须传递一个线程调度器,该调度器将数据传递到 UI SynchronizationContenxt。在下面的代码中,TaskScheduler.FromCurrentSynchronizationContext() 方法传递了与 UI 相关的线程调度器。

        t.ContinueWith((t1) =>
                {
                    if (t1.IsCompleted && !t1.IsFaulted && !t1.IsCanceled)
                        UpdateUI(t1.Result);
                }, TaskScheduler.FromCurrentSynchronizationContext());

    await

    考虑下面的代码,它会在 UI 上显示完成结果。

    public async void AsyncOperation()
    {
        try
        {
           string t = await Task.Run(() => LongRunningOperation("AsyncOperation",
                            10000));
           UpdateUI(t);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    使用 await 编写的代码在将数据发布到 UI 控件时不会引发任何异常,await 不需要特殊代码。这是因为正如在差异的第 1 点中所讨论的,在遇到 await 时,会保存包含 SynchronizationContext 信息的状态数据。

    因此,如果要将数据发布到 UI,await 是一个不错的选择,因为不需要额外的护理/代码来将数据发布到 UI。

  3. 处理异常和取消

    请考虑下面的代码示例,用于 ContinueWith 和 await 方法处理异常和取消的任务。

    ContinueWith

    下面是如何使用 ContinueWith 处理异常/取消的示例代码。

    public void ContinueWithOperationCancellation()
           {
                CancellationTokenSource source = new CancellationTokenSource();
                source.Cancel();
    
                Task<string> t = Task.Run(() =>
                    LongRunningOperationCancellation("Continuewith", 1500,
                        source.Token), source.Token);
    
                t.ContinueWith((t1) =>
                {
                    if (t1.Status == TaskStatus.RanToCompletion)
                        Console.WriteLine(t1.Result);
                    else if (t1.IsCanceled)
                        Console.WriteLine("Task cancelled");
                    else if (t.IsFaulted)
                    {
                        Console.WriteLine("Error: " + t.Exception.Message);
                    }
                });
    }

    在上面的代码中,通过使用 TaskStatus 处理了取消/异常。另一种方法是利用 TaskContinuationOptions.OnlyOnRanToCompletion

    t.ContinueWith(
        (antecedent) => {  },
         TaskContinuationOptions.OnlyOnRanToCompletion);

    await

    下面是如何使用 await 处理异常/取消的示例代码。

    public async void AsyncOperationCancellation()
           {
             try
             {
               CancellationTokenSource source = new CancellationTokenSource();
               source.Cancel();
               string t = await Task.Run(() =>
                   LongRunningOperationCancellation("AsyncOperation", 2000, source.Token),
                       source.Token);
                    Console.WriteLine(t);
             }
             catch (TaskCanceledException ex)
             {
                    Console.WriteLine(ex.Message);
             }
             catch (Exception ex)
             {
                    Console.WriteLine(ex.Message);
             }
    }

    上面的代码不使用 Task 状态,如 continuewith。它使用 try ..catch 块来处理异常。要处理取消,需要一个带有 TaskCanceledException 的 catch 块。

    因此,从上面的示例来看,我的观点是,当使用延续时,异常/取消处理会以非常干净的方式完成。

    下面是 LongRunningOperationCancellation 方法的代码。

    private string LongRunningOperationCancellation(string s, int sec,
    CancellationToken ct)
           {
             ct.ThrowIfCancellationRequested();
             Thread.Sleep(sec);
             return s + " Completed";
    }

    以上三种情况显示了 ContinueWith 和 await 之间编码的差异。但下面的情况显示了为什么 await 比 ContinueWith 更好。

  4. 复杂流程

    考虑下面的函数,它用于计算数字的阶乘

       public KeyValuePair<int, string> Factorial(int i)
            {
                KeyValuePair<int, string> kv;
                int fact = 1;
                for (int j = 1; j <= i; j++)
                    fact *= j;
                string s = "factorial no " + i.ToString() + ":" + fact.ToString();
                kv = new KeyValuePair<int, string>(i, s);
                return kv;
            }

    上面的函数计算数字的阶乘,并将 KeyValuePair 返回给调用者以显示计算结果。

    现在的问题陈述是,上述函数用于计算 1 到 5 的数字的阶乘。

    ContinueWith

    下面是计算 1 到 5 之间数字阶乘的代码。下面代码的期望是计算阶乘,按顺序显示它,并在完成后在控制台上打印“Done”消息。

    public void ContinueWithFactorial()
           {
                for (int i = 1; i < 6; i++)
                {
                    int temp = i;
                    Task<KeyValuePair<int, string>> t = Task.Run(() => Factorial(temp));
                    t.ContinueWith((t1) =>
                    {
                        KeyValuePair<int, string> kv = t1.Result;
                        Console.WriteLine(kv.Value);
                    });
                }
                Console.WriteLine("Done");
    }

    代码执行后,你会发现“Done”消息立即打印出来(即先打印),然后数字的阶乘显示在屏幕上。另一个问题是数字的阶乘没有按顺序显示。下面是代码执行的输出。

    因此,为了解决上述代码的问题,需要像这样重构代码。

    public void FactContinueWithSeq(int i)
    {
                Task<KeyValuePair<int, string>> t = Task.Run(() => Factorial(i));
                var ct = t.ContinueWith(((t1) =>
                {
                    KeyValuePair<int, string> kv = t1.Result;
                    int seed = kv.Key;
                    if (seed < 6)
                    {
                        Console.WriteLine(kv.Value);
                        seed++;
                        FactContinueWithSeq(seed);
                    }
                    else
                    {
                        Console.WriteLine("Done");
                        return;
                    }
                }));
    }

    上面的函数将被这样调用:p.FactContinueWithSeq(1)。

    在上面的代码中,为了保持顺序,需要一个接一个地触发 Task。为此,一旦一个 Task 完成其执行,就会在 Continuation 中使用 ContinueWith 方法再次调用函数。这就像在进行递归调用。

    并且要在最后显示“Done”消息,需要检查函数的种子。这会检查种子值是否增加到 6。

    但现在需要连接到 FactContinueWithSeq 完成后的 continuation,为此需要这样做:

            TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
            public Task<string> FactAsyncTask { get { return tcs.Task; } }
            public void FactContinueWithSeqAsync(int i)
            {
                Task<KeyValuePair<int, string>> t = Task.Run(() => Factorial(i));
                var ct = t.ContinueWith(((t1) =>
                {
                    KeyValuePair<int, string> kv = t1.Result;
                    int seed = kv.Key;
                    if (seed < 5)
                    {
                        Console.WriteLine(kv.Value);
                        seed++;
                        FactContinueWithSeqAsync(seed);
                    }
                    else
                    {
                        tcs.SetResult("Execution done");
                    }
                }));
            }

调用上述函数

            p.FactContinueWithSeqAsync(1);
            Task<string> t = p.FactAsyncTask;
            t.ContinueWith((t1)=> Console.WriteLine(t.Result));

在上面的代码中,TaskCompletionSource 用于实现为阶乘计算的完成提供 continuation 的代码。

因此,需要编写大量代码才能获得预期的结果,即按顺序计算阶乘,等待计算,并在计算完成后在控制台上打印“Done”消息。

await

现在,使用 await 相同代码可以这样完成:

public async void AwaitWithFactorial()
{
   for (int i = 1; i < 6; i++)
   {
       int temp = i;
       Task<KeyValuePair<int, string>> t = Task.Run(() => Factorial(temp));
       await t;
       Console.WriteLine(t.Result.Value);
      }
      Console.WriteLine("Done");
 
}

上面的代码简单明了,不需要进行大量重构,而使用 ContinueWith 进行相同操作则需要。

摘要

从上面 ContinueWith vs await 的区别/比较可以看出,在许多场景下使用 await 非常有帮助。

但也有一些场景,不需要更复杂的处理,并且错误/取消处理也很简单,在这种情况下,continuewith 是有用的。但这种情况非常罕见。

始终选择简单、容易和清晰的解决方案是明智的,对此,我的建议是始终选择 await 而不是 ContinueWith

© . All rights reserved.