异步编程101
自上而下的C#异步编程介绍
在此博客文章中,我将自上而下地介绍C#中的异步编程。 这篇文章对于C#的新手和对线程和并发有所了解的学生可能会很有用。 我相信自上而下的方法将是理想的,因为它首先将向您展示什么是异步编程,以便您对它的方式和原因感兴趣。
在这个博客中使用的代码可以在GitHub上我这里 。
快速回顾基本原理
延迟和吞吐量
延迟是衡量工作所需时间的单位。 吞吐量是每单位时间完成的工作单位的度量。
并发
并发意味着多个计算同时发生[1]。 它不同于此 stackoverflow答案中解释的并行性:
并发:至少有两个线程在进行时存在的条件。 并行性的一种更通用的形式,可以包括时间切片作为虚拟并行性的一种形式[2]。
并行性:当至少两个线程同时执行时出现的情况[2]。
任务
任务是代表操作的抽象[3]。 此操作可能会或可能不会由一个以上的线程支持,并且可能会或可能不会同时执行。 我们可以使用Tasks来代替直接使用线程,这样我们就不需要手动,显式地实现线程的锁定,联接和同步操作,也不需要直接手动创建或销毁线程。 在C#任务中,默认情况下使用线程池中的线程。 线程池是一种设计模式,可维护多个线程,以便可以重复使用/回收这些线程。 我们可以使用这种设计模式是为了减少重复创建,启动和删除线程的延迟。
同步与异步操作
同步操作将在返回调用者之前完成其工作,而异步操作可能在返回到调用者之后进行其部分(包括全部)工作[3]。
概括这些基本概念的类比
让我们考虑一下自助洗衣店。 如果你把衣服在两台机器在字面上的同时,例如一个洗衣机用一只手,另一洗衣机用另一只手在同一时间 ,那么你就装衣服并行 。 但是,如果您同时在两台洗衣机中装载衣服,使得在给定的时间点一次只将衣服装载在一台洗衣机中,那么您将同时装载衣服。
洗衣机洗一件衣服所花费的时间将是洗衣机的等待时间 。 为了减少等待n次洗衣服所花费的时间,而不是一次运行一台机器,可以同时运行n台机器:在两种情况下,一台机器完成的工作所花的时间相同。区别在于,在后一种情况下,在相同时间量(n个同时洗涤的负载)中完成的工作量更多,即, 吞吐量更多。
为了做所有这些事情,也许您问过前台的那个人宿舍:您当时什么也做不了,您在队列的最前面,给了他两美元的钞票,等待他们归还宿舍。 。 这是一个同步操作。 当洗衣机在运行时,您仍然可以控制-您可以继续加载并在第一台洗衣机仍在运行时开始运行另一台洗衣机-这使洗衣机的运行成为异步操作。
异步编程演示
让我们看一个演示,以了解如何定义和调用异步函数,以及异步编程如何通过提高吞吐量来影响程序延迟。
通过将async
关键字添加到函数定义中,可以定义C#中的async
函数。 可以像其他任何函数一样调用它们。 C#中的异步函数必须返回一个任务或某种类型的任务。 在下面的演示程序中,我们将看到如何启动这些任务,如何同步和异步运行它们,以及如何使用await
关键字停止代码流以等待任务的结果。
虚拟同步和异步功能
让我们实现一个虚拟的异步函数:
private async Task<int> longRunningMultiplyBy10(int number)
//100 delays of 100 ms = 10 seconds
Console.WriteLine("# Completed long running op on for the input number=" + number + " from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " #");
在此函数中,我们使用本机异步Delay
函数,方法是使用await
关键字异步调用该函数,以等待总计10秒钟,然后将输入数字乘以10并返回。 现在,我们将看到同步和异步调用时性能上的差异。 为此,我们将实现输入数字的功能,对每个数字执行虚拟长期运行操作并返回其总和。
同步调用longRunningMultiplyBy10
的函数是:
private int syncMultiplyBy10AndAdd(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
让我们编写一个使用异步编程的函数:
private async Task<int> asyncMultiplyBy10AndAddAwaitImmediately(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
我们可以在这里观察到,在需要访问Task的结果之前,我们不需要await
长时间运行的操作-让我们编写一个函数来反映此结果并观察其性能。
private async Task<int> asyncMultiplyBy10AndAddAwaitAtEnd(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
辅助函数调用伪函数并记录其性能
本部分是不必要的,可能会分散注意力,请跳到下一部分
我们将调用demo
来调用我们的演示函数并按以下方式评估其性能:
private void demo()
Console.WriteLine("##### Testing await immediately #####");
Console.WriteLine("##### Testing await at end #####");
private void demoSyncFunction(Func<int, int, int> asyncDemoFunction)
stopWatch.Start();
}
private void demoAsyncFunction(Func<int, int, Task<int>> asyncDemoFunction)
stopWatch.Start();
int result = asyncDemoFunction(num1, num2).Result;
stopWatch.Stop();
displayRuntime(stopWatch.Elapsed);
private void displayRuntime(TimeSpan ts)
Console.WriteLine("Runtime: " + elapsedTime);
结果
输出如下:
对于功能syncMultiplyBy10AndAdd
private int syncMultiplyBy10AndAdd(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
输出为:
##### Testing synchronous code #####
我们观察到线程1开始执行syncMultiplyBy10AndAdd
。 当它被称为longRunningMultiplyBy10
的执行syncMultiplyBy10AndAdd
被封堵。 然后线程1开始执行长时间运行的操作,该操作异步运行。 随着该函数的异步运行,线程1从执行longRunningMultiplyBy10
的任务中释放出来。 但是,由于syncMultiplyBy10AndAdd
是同步的,因此在longRunningMultiplyBy10
完成时,线程1仍然负责整个执行syncMultiplyBy10AndAdd
。 我们注意到该函数执行耗时20s.08ms 。
现在,当执行asyncMultiplyBy10AndAddAwaitImmediately
函数时:
private async Task<int> asyncMultiplyBy10AndAddAwaitImmediately(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
输出是
##### Testing await immediately #####
我们观察到线程1开始立即执行asyncMultiplyBy10AndAddAwaitImmediately
。 这次的区别是,由于对longRunningMultiplyBy10
的调用是异步的,因此从执行asyncMultiplyBy10AndAddAwaitImmediately
的任务中释放了线程1,并且线程池中的另一个线程Thread 7恢复了执行。 我们注意到该功能花费了20s.04ms 。 这绝对比同步呼叫实现快,并且我们可以直观地看到在现实世界中,延迟的改善将是多么显着。
我们还可以观察到效率低下:我们比需要使用它的结果更早地await
长时间运行的操作的完成。 因此,尽管我们使用线程池通过使用异步编程将执行asyncMultiplyBy10AndAddAwaitImmediately
高效地立即分配给线程的任务,但我们并未利用异步编程的全部功能来允许长时间运行的操作(在本例中为longRunningMultiplyBy10
)与调用者函数的执行(在本例中为asyncMultiplyBy10AndAddAwaitImmediately
)。
当我们解决这种效率低下的问题时,不要await
任务的结果,直到我们需要使用它们并测试我们的功能为止
private async Task<int> asyncMultiplyBy10AndAddAwaitAtEnd(int num1, int num2)
Console.WriteLine("Calling long running Op from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Executing return from Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId);
我们得到以下结果:
##### Testing await at end #####
长期运行的操作一被调用就开始执行。 调用程序功能的执行和长时间运行的操作同时继续进行。 有效的异步编码功能的运行时间仅为10s.04ms :与称为2个长时间运行的操作的同步实现相比 ,其延迟提高了50% 。 由于异步编码的函数操作以允许它们同时运行的方式调用了两个长时间运行的操作,因此可以预期将其提高50%。
什么是异步编程及其工作原理
正如我们现在所看到的,异步编程使我们能够有效地利用线程来并行运行任务。 当调用异步函数时,控件立即返回到调用者函数(异步函数的同步部分完成时),并且调用者和被调用函数的执行同时在单独的线程上继续进行,直到调用者函数必须await
被调用函数的完成。 通过有效地利用线程池将任务实例分配给同时运行的不同线程来提高吞吐量(每单位时间完成的工作),这有助于改善整体程序延迟(程序运行的总时间)。
参考文献
[1] https://web.mit.edu/6.005/www/fa14/classes/17-concurrency/
[2]定义多线程术语Oracle多线程编程指南
[3]第14章: C#5.0的并发性和异步性:Joseph Albahari的权威参考第五版,Ben Albahari(作者)
一个大喊答题节目环节到希尔顿兰格 :他的异步编程和演示异步程序的解释启发了我写这篇博客。
最初于 2019 年7月3日 发布在 deeptanshumalik.com 上。
From: https://hackernoon.com/asynchronous-programming-101-5ac0fa4e84e8