异步编程 await 和 async
文章目录
- 前言
- await 和 async
- (1)Thread.sleep() 阻塞当前线程
- (2)没有await 的 Task.Delay()
- (3)有 await 的 Task.Delay()
前言
转载: 方程式sunny
视频教程: 跟着sunny老师学C#
源码: gitee仓库
2025年10月25日 18点52分 ---吃薯片看 T1(Faker竟然还在) 保送8强,顺便记录一下异步编程
await 和 async
- 异步编程是指程序不需要等待任务的完成,而是可以继续执行其他操作。这样做的好处是,避免程序在处理耗时操作(如文件读取、网络请求)时界面被冻结或无响应。
- 当然了,上面这个定义说的根本不是人话,比如任务是什么?怎么继续执行其他操作?其他操作是什么?界面被冻结和无响应又是什么?这些都没有解释清楚嘛。先声明一点,我只讲最基本的原理,为什么只讲最基本的呢?原因是最本质的我也没搞清楚,哈哈,大家凑合看吧。
- 我们平常所说的异步编程,其实就是定义异步方法或者使用异步方法,定义和使用,对,这就是异步编程的全部。
我们先来看怎么看怎么定义一个普通方法:
public void Method()
{Console.WriteLine("这是一个普通方法");
}
好了,上面我们定义了一个普通的方法 Method,正常来说,一个普通方法也能用了呀,为什么还要什么异步方法呢?我们换一个例子。
public void Method()
{Console.WriteLine("这是一个普通方法");Thread.Sleep(5000); //模拟耗时操作
}
上面这个例子模拟了一个耗时操作,当前线程休眠5秒,这会造成什么后果呢?休眠了会阻塞当前线程,可能在控制台上不明显,我们换一个wpf或者winform的UI界面。如果你此时在一个UI界面上点击一个按钮,那么界面就会卡主,得等到5秒的休眠结束,点击按钮的事件才会响应。
- 遇到问题了,那就解决吧!怎么解决呢?有的小伙伴说用多线程技术重新开一个线程,的确可以,但是多线程参数传递、断点调试、全局变量控制等,会出现一系列的问题,使用起来也会很复杂。那有没有其他方法来解决这个问题呢?
- 有,这就是异步编程,异步编程的原理很复杂,但作为程序员,不需要知道这些复杂的原理,我们只要会用就行了。于是呢,微软就发明了
async
和await两个关键字,有了这两个关键字,好办了,编写异步方法就像编写同步方法一样简单。
由于上面的例子是为了我引出我讨论的话题,用的是一些零碎的代码片段,接下来呢我会用一个完整的代码来进一步解释,选的框架C#的wpf,程序运行之后就如下图所示,当点击按钮1时,它下面的文本框文字会发生变化,当点击按钮2时,与它对应的文本框也会发生变化。前端代码和后端代码我都贴上了。

前端代码:
<Window x:Class="WpfTest.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfTest"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid><Grid.RowDefinitions><RowDefinition Height="88*"/><RowDefinition Height="347*"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition/></Grid.ColumnDefinitions>
<Button Content="按钮1" Click="Button1_Click"/><Button Content="按钮2" Grid.Column="1" Click="Button2_Click"/><TextBox Grid.Row="1" Name="tbx1" Text="按钮1对应的文本"/><TextBox Grid.Row="1" Name="tbx2" Grid.Column="1" Text="按钮2对应的文本"/></Grid>
</Window>
后端代码:
using System.Windows;
namespace WpfTest
{public partial class MainWindow : Window{int i = 0;int j = 0;public MainWindow(){InitializeComponent();}
private async void Button1_Click(object sender, RoutedEventArgs e){Thread.Sleep(5000);//Task.Delay(5000);//await Task.Delay(3 * 1000);
tbx1.Text = "修改了文本框1的内容\n";tbx1.AppendText(j++.ToString());}
private void Button2_Click(object sender, RoutedEventArgs e){tbx2.Text = "修改了文本框2的内容\n";tbx2.AppendText(i++.ToString());} }
}
(1)Thread.sleep() 阻塞当前线程
- 我们运行上面的代码,但我们点击按钮1时,文本框1的文字不会马上变化,5秒后,文本框1的文字发生变化,因为我们
Thread.Sleep(5000)这行代码,让UI线程休眠了5秒。 - 我们再换一个操作,点击了
按钮1后,我们马上点击按钮2,我们发现不仅文本框1的文字没有马上发生改变,文本框2的文字也没有马上改变,5秒之后,我们发现文本框1和文本框2的文字都发生变化了。 - 还是一样的原因,因为这个软件只有一个
UI线程(主线程),当我们先点击按钮1时,UI线程阻塞了,点击按钮2后文本框2文字也就不会马上发生变化,5秒后文本框1和文本框2的文字都发生变比了。 - 通过以上两个小实验,我们得出结论:
Thread.sleep()会阻塞UI线程。
(2)没有await 的 Task.Delay()
- 我们接着来做小实验,注释
Thread.Sleep(5000);同时取消注释Task.Delay(5000);这行代码。运行程序,我们先点击按钮1,我们发现,文本框1的文字马上发生变化,并不是5秒后文本框才发生变化,这是为什么呢? - 原来
Task.Delay()是一个异步方法,它不会阻塞UI线程。事情到这里还没完,我的需求是不阻塞UI线程,同时还要延迟5秒,不阻塞线程的确满足了,延迟5秒显然不满足。 - 原来在调用异步方法时,我们还需要使用在调用异步方法前面使用关键字
await,例如:await Task.Delay(3 * 1000),Task.Delay()是一个异步方法,调用该异步方法时前面要使用await关键字,所以你看到Task.Delay(3 * 1000)前面有一个关键字await。 - 但在这个例子中,我们并没有使用
await,并没有等待异步方法的结果,系统就会直接执行异步方法后面的代码,这里后面的方法就是tbx1.Text = "修改了文本框1的内容\n"; tbx1.AppendText(j++.ToString());所以文本框1的文字马上发生了变化。 - 这个小实验还没完,我们重新运行程序,当点击按钮1时,我们又马上点击按钮2。我们可以看到,文本框1的内容马上发生变化,文本框2的内容也是立刻变化。
- 我们也得出了一个小结论:调用异步时不使用
await的确不会阻塞UI线程,但是延迟的功能没有实现。
(3)有 await 的 Task.Delay()
- 最后一个小实验,我们在调用异步方法时使用关键字
await,注释Task.Delay(5000);取消注释//await Task.Delay(3 * 1000);当我们运行程序时,点击按钮1,我们发现文本框1的内容没有立刻发生变化,5秒之后,文本框1的内容才发生变化,延迟功能的确是实现了,但会不会阻塞,我们还得继续实验。 - 重新运行程序,先点击
按钮1,然后立刻点击按钮2。我们发现文本框2的内容马上发生变化,但是文本框1的内容是5秒后才发生变化的,并且我们发现点击按钮1后,马上疯狂点击按钮2,整个UI界面并没有卡顿, 延迟功能实现了,不阻塞UI界面也实现了,妙呀。 - 事情到这里似乎所有的问题都得到了圆满的解决,但我们还没有讨论如何定义一个异步方法。
我们来写一个异步方法:
public async Task Myfun()
{Console.WriteLine("hello, Mr sunny!");
}
-
虽然这个例子使用了
async关键字,但实际上它并没有实现异步操作,因为没有任何耗时操作。Task是返回类型,与普通方法返回类型void、int、float等类似,Task是一个泛型类型。例如,普通方法返回void时,异步方法返回Task;普通方法返回int时,异步返回类型为Task<int>。 -
为了使这个方法成为真正的异步方法,我们需要添加
await:
public async Task Myfun()
{await Task.Delay(3 * 1000);Console.WriteLine("hello, Mr sunny!");
}
- 现在,这个方法可以异步执行。当我们在UI线程调用
Myfun时,即使内部有3秒的延迟,UI线程仍然可以继续处理其他操作,不会阻塞界面。 - 那么,为什么不阻塞
UI线程呢?当UI线程调用Myfun并执行到await Task.Delay(3 * 1000);时,系统发现有await,便将控制权返回给调用者(即UI线程)。此时,Console.WriteLine("hello, Mr sunny!");这行代码不会立即执行,因为它在await之后。 - 虽然
await后面的代码在await执行时被“暂停”,但并不是永远不执行。await Task.Delay(3 * 1000);会创建一个3秒的定时器,这个定时器由系统的线程池管理。定时到达后,系统会通过事件机制将控制权返回给之前的上下文,通常是UI线程。 - 具体说怎么找到之前没有运行完的代码,别问我,我也没有搞清楚,反正能找到没有执行完的代码。
- 需要强调的是,3秒的延迟是由系统管理的,而不是
C#代码直接控制的。如果主线程忙碌,系统会从线程池中选择一个空闲线程来处理这些后续的代码执行。
