一:async
-
介绍
async 关键字用于声明一个异步方法。当你在方法签名前使用 async 关键字时,你告诉编译器这个方法内部可能包含异步操作,并且这些方法通常会包含至少一个 await 表达式。async 方法允许你使用 await 关键字来等待异步操作的完成,而不会阻塞调用线程。 -
方法签名
async 关键字必须出现在方法返回类型之前,方法名之前。例如:public async Task DoSomethingAsync()。 -
返回类型
async 方法通常返回 Task 或 Task<T> 类型,其中 T 是方法的返回类型(如果有的话)。如果方法不需要返回任何值,则可以返回 Task;如果方法需要返回一个值,则可以返回 Task<T>,其中 T 是返回值的类型。此外,从C# 7.0开始,async 方法还可以返回 ValueTask 或 ValueTask<T>,这些类型提供了与 Task 和 Task<T> 类似的功能,但可能具有更低的分配成本。 -
内部实现
async 方法内部可以使用 await 关键字来等待异步操作的完成。await 表达式会暂停 async 方法的执行,直到等待的异步操作完成,然后 async 方法会继续执行,从 await 表达式之后的代码开始。 -
异常处理
在 async 方法中,任何未处理的异常都会自动封装在返回的 Task 或 Task<T> 对象中。调用方可以通过等待这个 Task 并捕获异常来处理这些异常。 -
线程
async 和 await 关键字本身不会创建新线程。它们允许异步操作在后台线程上执行(例如,I/O操作通常会在操作系统提供的线程池线程上异步执行),同时释放调用线程以执行其他任务。 -
上下文捕获
默认情况下,await 会捕获当前的同步上下文(例如,UI线程的同步上下文),并在异步操作完成后在该上下文中继续执行。这意味着,如果你在UI线程中调用一个 async 方法,并使用 await 等待异步操作,那么异步操作完成后,控制权将返回到UI线程。然而,你可以通过配置 await 的行为(使用 ConfigureAwait(false))来避免这种上下文捕获,从而提高性能。 -
命名约定
虽然这不是强制性的,但通常建议将 async 方法命名为以 Async 结尾,以清楚地表明这些方法是异步的。例如,FetchDataFromWebAsync。
二:await
-
介绍
在C#中,await 关键字用于异步编程,它允许你等待一个异步操作(如I/O操作、数据库查询、网络请求等)完成,而不会阻塞当前线程。await 只能在标记为 async 的方法内部使用。
当你使用 await 关键字时,你实际上是在告诉编译器:“我想等待这个异步操作完成,但我不想阻塞当前线程。请在当前操作完成时,以某种方式继续执行我的代码。”线程的控制权会返回给调用者,允许线程池中的当前线程去处理其他任务。直到任务完成,然后执行会继续在 await 之后的代码。 -
不会阻塞线程
await 不会阻塞调用它的线程。相反,它会让出线程的控制权,允许线程继续执行其他任务,直到异步操作完成。 -
保持上下文
await 会保留当前的上下文(如同步上下文),但并不保证会保持在同一个线程上执行,并在异步操作完成后返回到该上下文。这意味着,如果你在UI线程中使用 await,异步操作完成后,控制权将返回到UI线程,从而可以安全地更新UI元素。 -
返回值
await 关键字用于等待一个 Task 或 Task<TResult> 类型的对象。对于 Task,await 会等待任务完成而不返回任何结果;对于 Task<TResult>,await 会等待任务完成并返回任务的结果。 -
异常处理
await 关键字还会处理异步操作中抛出的异常。如果在等待的异步操作中抛出了异常,该异常将在 await 表达式之后被抛出,就像同步代码中的异常一样。
例子1:
例子2:
同时也要注意避免在不需要异步操作的方法中使用async和await,因为它们会增加不必要的开销。
为了更好的理解,我写了一个下载图片的例子,并通过打印来深度理解一下他们的用法:
Task t = TestTaskPoint();
Debug.WriteLine($"TestTaskPoint这个任务的状态:{t.Status}");
// 因为TestTaskPoint里用了await task,所以不会阻塞主线程
Debug.WriteLine("主线程不会被TestTaskPoint阻塞,继续做别的事情");
Task.Run(() =>
{
Thread.Sleep(7000);// 我们等TestTaskPoint全部执行完后看一下状态
Debug.WriteLine($"TestTaskPoint这个任务的状态:{t.Status}");
});
private async Task TestTaskPoint()
{
Debug.WriteLine("TestTaskPoint--{0}", Thread.CurrentThread.ManagedThreadId);
CancellationTokenSource cts222 = new CancellationTokenSource();
CancellationToken token333 = cts222.Token;
Task task = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
token333.ThrowIfCancellationRequested();
Console.WriteLine($"任务进行中 {i}---{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);// 模拟耗时任务,不阻塞线程方式,比如网络下载图片去了
}
Console.WriteLine("任务完成.");
}, token333);
// 此处用来模拟用户3秒后点击了停止下载图片任务
await Task.Delay(3000);// 模拟主线程工作耗时,不阻塞线程方式,不影响主线程取响应UI或等的任务
cts222.Cancel(); // 某个情况下任务被取消,比如这两行代码放在用户点击全部停止按钮事件里
try
{
await task; // 等待任务完成 或 取消
}
catch (OperationCanceledException ex)
{
Debug.WriteLine("捕获到异常---任务被取消:{0}--{1}", ex.Message, Thread.CurrentThread.ManagedThreadId);// 比如提示下载图片中途被中断了
}
Debug.WriteLine("任务完成或取消后做一些其他事情:{0}",Thread.CurrentThread.ManagedThreadId);// 比如图片全部下载成功提示
}
来看一下打印:
最后整理一下:
private void TestAsync()
{
Task t = TestTaskPoint();
Debug.WriteLine($"TestTaskPoint这个任务的状态:{t.Status}");
// 因为TestTaskPoint里用了await task,所以不会阻塞主线程
Debug.WriteLine("主线程不会被TestTaskPoint阻塞,继续做别的事情");
Task.Run(() =>
{
Thread.Sleep(7000);// 我们等TestTaskPoint全部执行完后看一下状态
Debug.WriteLine($"TestTaskPoint这个任务的状态:{t.Status}");
});
}
private void TestAsync2()
{
_ = TestTaskPoint();// 如果不需要TestTaskPoint这个任务的状态,也可以这样写
// 因为TestTaskPoint里用了await task,所以不会阻塞主线程
Debug.WriteLine("主线程不会被TestTaskPoint阻塞,继续做别的事情");
}
private async void TestAsync3()
{
await TestTaskPoint();
Debug.WriteLine("主线程被TestTaskPoint阻塞");
}