在这篇博文中,我们将讨论什么是异步代码,并探讨其执行在不同语言中的差异。我们将使用 C# 和 Java 来说明不同的方法。
介绍
让我们想象一下我们正在进行一次平面翻新。我们想粉刷墙壁并安装新地板,但在这之前,我们需要订购油漆和地板,等待交货并移走所有家具,以免损坏过程。
如果我们要编写一个为我们执行的程序,我们可以在异步执行和同步执行之间做出选择。
如果我们同步执行此操作,则顺序如下:
- 第 0 天:下订单购买油漆并等待几天直到交付(2 天)。
- 第2天:下单木地板,等到发货(20天-发货慢)。
- 第 22 天:从您的公寓中移除所有家具(5 天)。
- 第 27 天:粉刷墙壁(5 天)。
- 第 32 天:安装木地板(5 天)。
- 第 37 天:您的装修完成了 :) 总共用了 37 天。
这听起来不太有效。当然,我们可以在不等待油漆交货的情况下下订单购买木地板。或者我们可以在货物到达之前开始搬走家具。或者换句话说,我们可以异步执行许多操作。
该序列可能如下所示:
- 第 0 天:下订单购买油漆(交货需要 2 天)。
- 第 0 天:下订单购买木地板(交货时间为 20 天)。
- 第 0 天:移除家具(5 天)。
- 第 5 天:油漆已经在 3 天前到达 - 让我们粉刷墙壁(5 天)。
- 第 10 天:还要再等 10 天,直到地板交付 - 我们现在(10 天)没有任何事可做。
- 第 20 天:地板终于到了 - 让我们安装它(5 天)。
- Day 25:Aaand done,总共花了我们25天。
更正式地说,在异步执行模型(也称为非阻塞)中,您可以启动任务但不必等待它完成,直到您真正需要结果为止。在同步模型(也称为阻塞)中,您一个接一个地按顺序执行所有操作。
这是对同步和异步执行模型的常见介绍。编程语言以不同方式实现此模型。
我们将探讨如何在 C# 和 Java 中实现此示例并比较它们的模型。
异步执行模型
异步执行并不一定意味着每个异步操作都产生或使用单独的线程——尽管多线程和异步性密切相关,但它们是不同的概念。例如,消息队列是异步处理任务的设计模式之一——它可以使用一个或多个线程(消息将从一个单独的线程提交)。
在 C# 中,根据Microsoft 文档,async / await 关键字不会导致创建额外的线程,因为异步方法不会在它们自己的专用线程上执行。相反,该方法在当前同步上下文中运行,并且仅在线程处于活动状态时才使用它的时间。同步上下文表示代码运行的执行上下文,并且有一些示例是单线程的。
如果同步上下文为空(如下面的控制台应用程序示例中所示),异步操作将使用线程池执行。如果方法到达await关键字并产生执行,则其延续不一定使用相同的线程。
在 Java 中,有多种编写异步代码的方法。我们只会CompletableFuture在这篇博文中讨论(注意:我们不会讨论虚拟线程,它目前作为预览功能可用)。Java 使用单独的线程来执行异步任务,并且不会在 JVM 级别切换上下文。即使您创建了一个单线程执行器 ( Executors.newSingleThreadExecutor()),它仍然是第二个线程(除了主线程之外),如果要求等待执行结果,主线程将被阻塞。
Node.js 中使用了另一种模型,它利用事件循环进行异步操作。您可以在本文中阅读相关信息。
现在让我们看一些 C# 和 Java 中的示例。
C# 示例
下面的代码根据我们在异步范例中的介绍进行改造,如介绍中所述:
internal class Renovation
{
static int Id => Thread.CurrentThread.ManagedThreadId;
static async Task BuyPaint()
{
Console.WriteLine($"{Id} Placing an order for paint at a local shop - it'll be delivered quickly.");
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine($"{Id} Paint delivered!");
}
static async Task BuyWoodenFloor()
{
Console.WriteLine($"{Id} Placing an order for wooden floor. The delivery will take over a month.");
await Task.Delay(TimeSpan.FromSeconds(20));
Console.WriteLine($"{Id} Wooden floor is finally delivered!");
}
static async Task RemoveFurniture()
{
Console.WriteLine($"{Id} Starting to remove the furniture... This will take a few days.");
await Task.Delay(TimeSpan.FromSeconds(5));
Console.WriteLine($"{Id} Removed all furniture from the room!");
}
static async Task PaintingTheWalls()
{
Console.WriteLine($"{Id} Now let's paint the walls - should be fairly quick.");
await Task.Delay(TimeSpan.FromSeconds(5));
Console.WriteLine($"{Id} Walls are painted!");
}
static async Task InstallNewFloor()
{
Console.WriteLine($"{Id} Let's install the floor.");
await Task.Delay(TimeSpan.FromSeconds(5));
Console.WriteLine($"{Id} Floors are installed!");
}
static async Task Main(string[] args)
{
Console.WriteLine($"Is Background thread? {Thread.CurrentThread.IsBackground}");
Console.WriteLine($"Is SynchronizationContext null? {System.Threading.SynchronizationContext.Current == null}");
Task paint = BuyPaint();
Task woodenFloor = BuyWoodenFloor();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} Placed all orders! Let's remove all the furniture from the room...");
await RemoveFurniture();
await paint;
await PaintingTheWalls();
await woodenFloor;
await InstallNewFloor();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} The renovation is done :)");
Console.WriteLine($"Is Background thread? {Thread.CurrentThread.IsBackground}");
}
}
这是输出:
PS C:\Users\darya\VSCode> dotnet run
**Is Background thread?** False
Is SynchronizationContext null? True
1 Placing an order for paint at a local shop - it'll be delivered quickly.
1 Placing an order for wooden floor. The delivery will take over a month.
1 Placed all orders! Let's remove all the furniture from the room...
1 Starting to remove the furniture... This will take a few days.
5 Paint delivered!
5 Removed all furniture from the room!
5 Now let's paint the walls - should be fairly quick.
5 Walls are painted!
5 Wooden floor is finally delivered!
5 Let's install the floor.
5 Floors are installed!
5 The renovation is done :)
**Is Background thread?** True
有一些有趣的事情需要注意:
- 在遇到await关键字之前,C# 永远不会放弃对执行的控制。例如,它首先记录它为绘画和产量下了订单,然后它为木地板和产量记录了相同的订单,然后才开始移除家具。
- 如上所述,它们的同步上下文为空,因此代码使用了线程池。在高峰期,三个方法并行运行 - Main、BuyPaint 和 BuyWoodenFloor。BuyPaint 在线程 #1 上启动,但当它恢复时,它使用线程 #5 并且 main 方法的执行随后也在该线程上继续。
- Main 方法在主线程上启动,但经过几次异步调用后,它记录了最终在后台线程上执行的情况。
Java例子
现在让我们使用 Java 实现相同的示例CompletableFuture:
public class Main {
static int getId() {
return (int) Thread.currentThread().threadId();
}
static void buyPaint() {
try {
System.out.println(getId() + " Placing an order for paint at a local shop - it'll be delivered quickly.");
TimeUnit.SECONDS.sleep(2);
System.out.println(getId() + " Paint delivered!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static void buyWoodenFloor() {
try {
System.out.println(getId() + " Placing an order for wooden floor. The delivery will take over a month.");
TimeUnit.SECONDS.sleep(20);
System.out.println(getId() + " Wooden floor is finally delivered!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static void removeFurniture() {
try {
System.out.println(getId() + " Starting to remove the furniture... This will take a few days.");
TimeUnit.SECONDS.sleep(5);
System.out.println(getId() + " Removed all furniture from the room!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static void paintingTheWalls() {
try {
System.out.println(getId() + " Now let's paint the walls - should be fairly quick.");
TimeUnit.SECONDS.sleep(5);
System.out.println(getId() + " Walls are painted!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static void installNewFloor() {
try {
System.out.println(getId() + " Let's install the floor.");
TimeUnit.SECONDS.sleep(5);
System.out.println(getId() + " Floors are installed!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Executor executor = Executors.newFixedThreadPool(/* nThreads= */ 3);
CompletableFuture<Void> paint = CompletableFuture.runAsync(Main::buyPaint, executor);
CompletableFuture<Void> woodenFloor = CompletableFuture.runAsync(Main::buyWoodenFloor, executor);
System.out.println(getId() + " Placed all orders! Let's remove all the furniture from the room...");
removeFurniture();
paint.join();
CompletableFuture.runAsync(Main::paintingTheWalls, executor).join();
woodenFloor.join();
CompletableFuture.runAsync(Main::installNewFloor, executor).join();
System.out.println(getId() + " The renovation is done :)");
}
}
和输出:
23 Placing an order for paint at a local shop - it'll be delivered quickly.
24 Placing an order for wooden floor. The delivery will take over a month.
1 Placed all orders! Let's remove all the furniture from the room...
1 Starting to remove the furniture... This will take a few days.
23 Paint delivered!
1 Removed all furniture from the room!
25 Now let's paint the walls - should be fairly quick.
25 Walls are painted!
24 Wooden floor is finally delivered!
23 Let's install the floor.
23 Floors are installed!
1 The renovation is done :)
- 我们看到 Java 从
CompletableFuture我们为每个方法定义的线程池中使用了单独的线程。 - 与 C# 不同,异步方法始终在单独的线程上从头到尾完全执行,并且不会产生执行。
- 主线程即使在阻塞时也总是被占用——main 方法总是在主线程上开始和结束。
结论
异步代码是一种强大的模型,如果使用得当,可以提高应用程序的响应能力。我们只是研究了它在不同语言中的工作方式可能有何不同。这是一个很大的话题,我不会试图在一篇博文中涵盖所有细节,但我希望它能够展示看似相同的代码如何根据编程语言以不同的方式执行。