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

C# 异步和Await 编程模型从头开始

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (9投票s)

2016年3月6日

CPOL

6分钟阅读

viewsIcon

42503

用 C# 解释 async await 编程模型。

引言

本文是为希望了解基础知识和一些内部工作原理的普通开发人员,对 asyncawait 关键字的简要介绍。

背景

在开发任何应用程序时,异步编程现在都至关重要,因为它避免了主线程在磁盘 I/O、网络操作、数据库访问等耗时操作上等待。在正常情况下,如果我们的程序需要这些耗时操作的结果,那么我们的代码将一直阻塞,直到操作完成。

使用异步机制,我们可以触发耗时操作,同时执行其他任务。这些耗时操作在不同的线程上执行,完成后会通知我们的主代码,然后我们的代码可以从这里执行后续操作。当我们提到“我们的代码”时,是指处理用户界面或主要处理 Web 请求或服务 UI 的线程。有时,我们自己会编写这些耗时操作。

什么是 async 和 await

简单来说,它们是 .NET 4.5 中引入的两个新关键字,用于轻松编写异步程序。它们在方法级别工作。当然,我们不能让类并行工作,因为它们不是执行单元。


这些关键字是 CLR(.NET 运行时)已知的,还是 TPL(任务并行库)的包装器?如果它们是包装器,那么让语言依赖于用同一种语言编写的库是否明智?

在本文中,我们将尝试找出这些问题的答案。

.NET 异步编程的历史

线程从 .NET 框架一开始就存在。它们是操作系统线程的包装器,使用起来有些困难。然后出现了像后台工作者、async delegate 和任务并行库 (TPL) 等概念,以简化异步编程模型。这些是作为类库的一部分出现的。直到 asyncawait 关键字随 C# 4.0 一起引入,C# 语言本身并没有“开箱即用”的异步编程支持。让我们通过检查这些方法中的每一种来了解 asyncawait 如何帮助我们进行异步编程。

示例

让我们以找出前 N 个数中能被 3 整除的数的阶乘的示例如例。为简单起见,我们使用控制台应用程序。如果我们使用 Windows 应用程序,我们可以轻松地挂接到 async 事件委托处理程序并演示 async 功能。但这无助于我们学习语言特性。

同步代码

public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorial(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}
private void WriteFactorial(int no)
{
    int result = FindFactorialWithSimulatedDelay(no);
    Console.WriteLine("Factorial of {0} is {1}", no, result);
}
private static int FindFactorialWithSimulatedDelay(int no)
{
    int result = 1;
    for (int i = 1; i <= no; i++)
    {
         Thread.Sleep(500);
         result = result * i;
    }
    return result;
}

我们可以看到一个循环,它使用计数器变量从 1 迭代到 5。它检查当前计数器值是否能被 3 整除。如果是,则写入其阶乘。写入函数通过调用 FindFactorialWithSimulatedDelay() 方法来计算阶乘。此方法在示例中将产生延迟以模拟真实工作负载。换句话说,这就是耗时操作。

我们可以很容易地看出,执行是按顺序进行的。循环中的 WriteFactorial() 调用会等待阶乘计算完成。为什么我们必须在这里等待?为什么不能转到下一个数字,因为数字之间没有依赖关系?我们可以。但是 WriteFactorial() 中的 Console.WriteLine 语句怎么办?它必须等待阶乘找到。这意味着我们可以异步调用 FindFactorialWithSimulatedDelay(),前提是有一个回调到 WriteFactorial()。当异步调用发生时,循环可以使计数器前进到下一个数字并调用 WriteFactorial()

线程是我们可以实现这一目标的一种方式。由于线程比普通开发人员更难使用,需要更多知识,因此我们使用了 async delegate 机制。以下是使用 async delegate 重写的 WriteFactorial() 方法。

使用 Async Delegates 使其异步化

早期使用的一个更简单的方法是使用异步委托调用。它使用 Begin/End 方法调用机制。在这里,运行时使用线程池中的线程来执行代码,完成后我们可以获得回调。下面的代码很好地解释了这一点,它使用了 Func 委托。

private void WriteFactorialAsyncUsingDelegate(int facno)
{
    Func<int, int> findFact = FindFactorialWithSimulatedDelay;
    findFact.BeginInvoke(facno, 
                        (iAsyncresult) =>
                                {
                                    AsyncResult asyncResult = iAsyncresult as AsyncResult;
                                    Func<int, int> del = asyncResult.AsyncDelegate as Func<int, int>;
                                    int factorial = del.EndInvoke(asyncResult);
                                    Console.WriteLine("Factorial of {0} is {1}", facno, factorial);
                                }, 
                        null);
}

public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingDelegate(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

计算阶乘没有改变。我们只是添加了一个名为 WriteFactorialAsyncUsingDelegate() 的新函数,并修改了 Main 以在循环中调用此方法。

一旦调用了 findFact delegate 上的 BeginInvoke,主线程就会返回到计数器循环,然后它会增加计数器并继续循环。当阶乘可用时,匿名回调将被触发,并将写入控制台。

缺点

我们没有直接取消任务的选项。此外,如果我们想等待一个或多个方法,会有些困难。

另外,我们可以看到代码块没有被包装成对象,我们需要与 IAsyncResult 对象进行斗争才能取回结果。TPL 也解决了这个问题,它看起来更面向对象。让我们看一下。

使用 TPL 改进异步编程

TPL 在 .NET 4.0 中引入。我们可以将异步代码包装在 Task 对象中并执行它。我们可以等待一个或多个任务完成。可以轻松取消任务等。还有更多内容。下面是用 TPL 重写我们的阶乘写入代码。

private void WriteFactorialAsyncUsingTask(int no)
{
    Task<int> task=Task.Run<int>(() =>
    {
        int result = FindFactorialWithSimulatedDelay(no);
        return result;
    });
    task.ContinueWith(new Action<Task<int>>((input) =>
    {
        Console.WriteLine("Factorial of {0} is {1}", no, input.Result);
    }));
}
public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingTask(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

在这里,我们可以看到第一个任务正在运行,然后它继续执行下一个任务,即已完成的处理程序,该处理程序接收第一个任务的通知并将其结果写入控制台。

缺点

这仍然不是语言特性。我们需要引用 TPL 库来获得支持。主要问题是编写完成事件处理程序的麻烦。让我们看看如何使用 async 和 await 关键字重写它。

语言特性 async 和 await

我们将看到如何使用 asyncawait 关键字重写 TPL 示例。我们使用 async 关键字装饰了 WriteFactorialAsyncUsingAwait 方法,以表示该函数将以 async 方式执行操作,并且可能包含 await 关键字。没有 async,我们就不能 await。

然后,我们等待阶乘查找函数。在执行过程中遇到 await 的那一刻,线程会转到调用方法并从那里恢复执行。在我们的例子中,是计数器循环,然后取下一个数字。被等待的代码使用 TPL 作为其任务执行。像往常一样,它从线程池中获取一个线程并执行它。一旦执行完成,将执行 await 下面的语句。

在这里,我们也**不会**更改 FindFactorialWithSimulatedDelay() 中的任何内容。

private async Task WriteFactorialAsyncUsingAwait(int facno)
{
    int result = await Task.Run(()=> FindFactorialWithSimulatedDelay(facno));
    Console.WriteLine("Factorial of {0} is {1}", facno, result);
}
public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingAwait(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

这避免了对额外回调处理程序的需求,开发人员可以以顺序方式编写代码。

Task Parallel Library 和 async await 关键字的关系是什么?

asyncawait 关键字在内部利用 TPL。更确切地说,我们可以说 asyncawait 是 C# 语言中的语法糖。仍然不清楚?换句话说,.NET 运行时不知道 asyncawait 关键字。

查看 WriteFactorialAsyncUsingAwait() 的反汇编代码。您可以使用 Reflector 或类似工具来反汇编程序集。

何时使用它?

每当我们等待某些内容时,即每当我们处理异步场景时,都可以使用 async 和 await。例如,文件 I/O、网络操作、数据库操作等。这将帮助我们使 UI 响应。

关注点

有趣的问题是:语言是否应该依赖于用它创建的库/类?语言是否应该了解并行编程构造?最后,编译器是否应该修改我们的代码,以便在调试代码时能看到一些新的东西?

历史

  • 2016 年 3 月 6 日:首次发布
© . All rights reserved.