0
点赞
收藏
分享

微信扫一扫

C#异步编程解析


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";

}

不要同步、异步混用


举报

相关推荐

0 条评论