C#异步编程解析
- 概述
- 异步编程误区:
- async await 和 异步方法的基本使用
- async await 原理
- async 背后的线程切换
- 异步方法不等于多线程
- 为什么有的异步方法没有标注 Async
- 不要使用 Sleep
- CancellationToken
- WhenAll
- 异步其他问题
概述
- 以下伪代码基于 .NET5
本篇文章并不适合小白阅读
倘若想要进行学习请先阅读:CSharp(C#)语言_高级篇(异步编程)【划重点咯】 打下一个良好的基础再进行阅览
文中用到的反编译工具 ILSpy 免费且开源的,可自行下载
异步编程误区:
- 异步编程是多线程
- 异步编程可以提高系统运行效率
async await 和 异步方法的基本使用
//static async Task Main(string[] args)
static void Main(string[] args)
{
string fileName = $"./1.txt";
//StringBuilder stringBuilder = new StringBuilder();
//for (int i = 0; i < 100000; i++)
//{
// stringBuilder.Append("Hello ");
//}
/* 同步方式 */
//File.WriteAllText(fileName, stringBuilder.ToString());
//Console.WriteLine(File.ReadAllText(fileName));
/*
异步方式 需要在方法标识符 添加 Async 并且 返回值必须为 Task 类型
异步方法调用前加 await
*/
//await File.WriteAllTextAsync(fileName, stringBuilder.ToString());
//Console.WriteLine(await File.ReadAllTextAsync(fileName));
// 同步方法中调用异步方法
// 有返回值可以使用 Result 属性 取到异步方法返回值 而不用 await 标识
// 无返回值可以使用 Wait() 方法
//Console.WriteLine(DownloadHtmlAsync("https://www.baidu.com", fileName).Result);
// 有一定的产生死锁的风险
/*
异步 lamdba 表达式
使用 async 将 lambda 表达式 修饰为 异步 lambda 表达式
*/
ThreadPool.QueueUserWorkItem(async (obj) =>
{
while (true)
{
await File.WriteAllTextAsync(fileName, "Hello");
Console.WriteLine("Hello");
}
});
Console.Read();
}
//static async Task DownloadHtmlAsync(string url, string fileName)
//{
// using HttpClient httpClient = new();
// string html = await httpClient.GetStringAsync(url);
// await File.WriteAllTextAsync(fileName, html);
//}
static async Task<int> DownloadHtmlAsync(string url, string fileName)
{
using HttpClient httpClient = new();
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(fileName, html);
return html.Length;
}
async await 原理
async await 是语法糖 最终编译成 “状态机调用”
async 的方法会被 C# 编译器编译成一个类,会主要根据await调用进行切分为多个状态,对 async 方法的调用会被拆分为对 MoveNext 的调用。
用 await 看似是 “等待”,经过编译后,其实没有“wait”
- 源代码
static async Task Main(string[] args)
{
using HttpClient client = new();
string html = await client.GetStringAsync("https://www.baidu.com");
Console.WriteLine($"{html}\n");
string destFilePath = $"./1.txt";
await File.WriteAllTextAsync(destFilePath, "你好 async and await");
Console.WriteLine($"写入内容:{await File.ReadAllTextAsync(destFilePath)}");
}
- 反编译代码
原始 Main 方法反编译代码
// AsyncAwait.Program
using System.Diagnostics;
using System.Runtime.CompilerServices;
[SpecialName]
[DebuggerStepThrough]
private static void <Main>(string[] args)
{
Main(args).GetAwaiter().GetResult();
}
异步 Main 方法源代码编译之后反编译代码
// AsyncAwait.Program
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{
<Main>d__0 stateMachine = new <Main>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.args = args;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
async await 底层 “状态机” 反编译代码
// AsyncAwait.Program.<Main>d__0
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public string[] args;
private HttpClient <client>5__1;
private string <html>5__2;
private string <destFilePath>5__3;
private string <>s__4;
private string <>s__5;
private TaskAwaiter<string> <>u__1;
private TaskAwaiter <>u__2;
private void MoveNext()
{
int num = <>1__state;
try
{
if ((uint)num > 2u)
{
<client>5__1 = new HttpClient();
}
try
{
TaskAwaiter<string> awaiter3;
TaskAwaiter awaiter2;
TaskAwaiter<string> awaiter;
switch (num)
{
default:
awaiter3 = <client>5__1.GetStringAsync("https://www.baidu.com").GetAwaiter();
if (!awaiter3.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter3;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
return;
}
goto IL_009e;
case 0:
awaiter3 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_009e;
case 1:
awaiter2 = <>u__2;
<>u__2 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_014b;
case 2:
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
break;
}
IL_014b:
awaiter2.GetResult();
awaiter = File.ReadAllTextAsync(<destFilePath>5__3).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 2);
<>u__1 = awaiter;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
break;
IL_009e:
<>s__4 = awaiter3.GetResult();
<html>5__2 = <>s__4;
<>s__4 = null;
Console.WriteLine(<html>5__2 + "\n");
<destFilePath>5__3 = "./1.txt";
awaiter2 = File.WriteAllTextAsync(<destFilePath>5__3, "你好 async and await").GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter2;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
goto IL_014b;
}
<>s__5 = awaiter.GetResult();
Console.WriteLine("写入内容:" + <>s__5);
<>s__5 = null;
}
finally
{
if (num < 0 && <client>5__1 != null)
{
((IDisposable)<client>5__1).Dispose();
}
}
}
catch (Exception exception)
{
<>1__state = -2;
<client>5__1 = null;
<html>5__2 = null;
<destFilePath>5__3 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<client>5__1 = null;
<html>5__2 = null;
<destFilePath>5__3 = null;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
异步的 Main 方法 并不是 原始的 Main方法,从上面反编译的代码可以看出来
async 背后的线程切换
await调用的等待期间,.NET会把当前的线程返给线程池,等异步方法调用执行完毕后,框架会从线程池再取一个出来线程执行后续的代码。
- 异步写入大文件,使用
Thread.CurrentThread.ManagedThreadId
查看线程Id
static async Task Main(string[] args)
{
string fileName = $"./1.txt";
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
stringBuilder.Append("Hello ");
}
await File.WriteAllTextAsync(fileName, stringBuilder.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
注意:如果写入内容少,会发生线程Id不变
CLI优化:到要等待的时候,如果发现已经执行结束了,那就没有必要再切换线程,剩下的代码就继续在之前的线程上继续执行。
异步方法不等于多线程
- 异步方法中的代码并不会自动在新的线程中执行,除非手动把代码放到新的线程中执行。
static async Task Main(string[] args)
{
Console.WriteLine($"之前:{Thread.CurrentThread.ManagedThreadId}");
await CalcAsync(5000000);
Console.WriteLine($"之后:{Thread.CurrentThread.ManagedThreadId}");
}
static async Task<double> CalcAsync(int n)
{
/*
Console.WriteLine($"CalcAsync-ThreadId:{Thread.CurrentThread.ManagedThreadId}");
double result = 1;
Random random = new();
for (int i = 0; i < n * n; i++)
{
result += (double)random.NextDouble();
}
return result;
*/
return await Task.Run(() =>
{
Console.WriteLine($"CalcAsync-ThreadId:{Thread.CurrentThread.ManagedThreadId}");
double result = 1;
Random random = new();
for (int i = 0; i < n * n; i++)
{
result += (double)random.NextDouble();
}
return result;
});
}
为什么有的异步方法没有标注 Async
/*
static async Task Main(string[] args)
{
Console.WriteLine(await ReadAsync(1));
}
*/
static void Main(string[] args)
{
Console.WriteLine(ReadAsync(1));
}
/*
static async Task<string> ReadAsync(int num)
{
if (num is 1)
{
return await File.ReadAllTextAsync("./1.txt");
}
else if (num is 2)
{
return await File.ReadAllTextAsync("./2.txt");
}
else
{
throw new ArgumentNullException();
}
}
*/
static Task<string> ReadAsync(int num)
{
if (num is 1)
{
return File.ReadAllTextAsync("./1.txt");
}
else if (num is 2)
{
return File.ReadAllTextAsync("./2.txt");
}
else
{
throw new ArgumentNullException();
}
}
1、async
返回会生成一个类,运行效率没有普通方法高;
2、可能会占用非常多的线程。
只甩手 Task,不 “拆完再装” 反编译上面的代码:只是普通的方法调用。
优点:运行效率更高,不会造成线程浪费。
返回值为 Task 的不一定都要标注 async,标注 async 只是让我们更方便的 await 而已。
如果一个异步方法只是对别的异步方法调用转发,并没有太多复杂的逻辑,那么就可以去掉 async 关键字
static void Main(string[] args)
{
Console.WriteLine($"之前:{Thread.CurrentThread.ManagedThreadId}");
CalcAsync(5000000);
Console.WriteLine($"之后:{Thread.CurrentThread.ManagedThreadId}");
}
static Task<double> CalcAsync(int n)
{
return Task.Run(() =>
{
Console.WriteLine($"CalcAsync-ThreadId:{Thread.CurrentThread.ManagedThreadId}");
double result = 1;
Random random = new();
for (int i = 0; i < n * n; i++)
{
result += (double)random.NextDouble();
}
return result;
});
}
不要使用 Sleep
如果想在异步方法中暂停一段时间,
不要用 Thread.Sleep()
因为它会阻塞调用线程,
而要用 await Task.Delay()
。
CancellationToken
CancellationToken
结构体
None
:空
bool IsCancellationRequested
是否取消
(*)Register(Action callback)
注册取消监听
ThrowIfCancellationRequested()
如果任务被取消,执行到这句就抛异常。
通过 CancellationTokenSource
来创建 CancellationToken
对象
Cancel()
发出取消信号
cts.CancelAfter()
超时后发去取消信号
static async Task Main(string[] args)
{
CancellationTokenSource cts = new();
cts.CancelAfter(1000);
CancellationToken token = cts.Token;
await DownloadAsync("https://www.baidu.com", 100, token);
}
/// <summary>
/// 无取消请求型
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <returns></returns>
static async Task DownloadAsync(string url, int n)
{
using HttpClient client = new();
for (int i = 0; i < n; i++)
{
Console.WriteLine($"{DateTime.Now}:{await client.GetStringAsync(url)}");
}
}
/// <summary>
/// 取消请求型
/// </summary>
/// <param name="url"></param>
/// <param name="n"></param>
/// <param name="token"></param>
/// <returns></returns>
static async Task DownloadAsync(string url, int n, CancellationToken token)
{
using HttpClient client = new();
for (int i = 0; i < n; i++)
{
// 手动处理型 推荐使用
Console.WriteLine($"{DateTime.Now}:{await client.GetStringAsync(url)}");
if (token.IsCancellationRequested)
{
Console.WriteLine("请求被取消");
break;
}
// 抛异常型,请求被终止抛出异常
//token.ThrowIfCancellationRequested();
// 抛异常型,将处理交予别人
//Console.WriteLine($"{DateTime.Now}:{await client.GetAsync(url, token)}");
}
}
ASP.NET Core 开发中,一般不需要自己处理 CancellationToken
CancellationTokenSource
这些,只要做到 能转发 CancellationToken 就转发 即可。ASP.NET Core 会对用户请求中断进行处理。
ASP.NET Core 程序中仅可能的在Action中使用 CancellationToken
以避免浏览器跳转到别的网页服务器还在执行而造成的资源浪费
WhenAll
Task类的重要方法
1、Task<Task> WhenAll(IEnumerable<Task> tasks)
等,任何一个 Task 完成,Task 就完成
2、Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks)
等,所有 Task 瓦纳城,Task 才完成。用于等待多份任务执行结束,但是不在乎他们的执行顺序。
3、FromResult()
创建普通数值的 Task 对象。
static async Task Main(string[] args)
{
string[] files = Directory.GetFiles("./");
Task<int>[] countTasks = new Task<int>[files.Length];
for (int i = 0; i < files.Length; i++)
{
string fileName = files[i];
Task<int> task = ReadCharsCount(fileName);
countTasks[i] = task;
}
int[] counts = await Task.WhenAll(countTasks);
Console.WriteLine(counts.Sum());
}
static async Task<int> ReadCharsCount(string filenName)
{
string s = await File.ReadAllTextAsync(filenName);
return s.Length;
}
异步其他问题
接口中的异步方法:
async 是提示编译器为异步方法中的 await 代码进行分段处理的,而一个异步方法是否修饰了 async 对于方法的调用者来讲没区别,因此对于接口中的方法或者抽象方法不能修饰为 async。
interface ITest
{
Task<int> GetCharCount(string file);
}
class Test : ITest
{
public async Task<int> GetCharCount(string file)
{
string s = await File.ReadAllTextAsync(file);
return s.Length;
}
}
异步与yield:
yield return 不仅能够简化数据的返回,而且可以让数据处理 “流水线化” 提升性能。
static void Main(string[] args)
{
foreach (var item in YieldTest())
{
Console.WriteLine(item);
}
}
static IEnumerable<string> YieldTest()
{
yield return "1";
yield return "2";
yield return "3";
}
在旧版的C#中,async 方法中不能用 yield。从C#8.0开始,把返回值声明为 IAsyncEnumerable
(不要带Task),然后遍历的时候用 await foreach()
即可
static async void Main(string[] args)
{
await foreach (var item in YieldTest())
{
Console.WriteLine(item);
}
}
static async IAsyncEnumerable<string> YieldTest()
{
yield return "1";
yield return "2";
yield return "3";
}
不要同步、异步混用