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.
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 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);
}

/// 
/// 无取消请求型
/// 
/// 
/// 
/// 
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)}");
    }
}

/// 
/// 取消请求型
/// 
/// 
/// 
/// 
/// 
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 WhenAll(IEnumerable tasks)等,任何一个 Task 完成,Task 就完成
2、Task WhenAll(params Task[] 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";

}

不要同步、异步混用

你可能感兴趣的:(C#,c#,异步编程,编程思想,理论)