C#异步编程解析

概述

  • 以下伪代码基于 .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";

}

不要同步、异步混用

posted @ 2026-10-05 18:06  JacePan  阅读(9)  评论(0)    收藏  举报