C# 异步编程(await、async&Task)

发布时间 2023-05-29 21:48:13作者: Swbna

视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili

异步编程

为什么要异步编程?

几个误区:(1)异步不一定能提高效率(2)异步不是多线程

image.png

传统多线程异步开发太麻烦。
C#关键字:asyncawait。【async、await不等于“多线程”】

async、await的基本使用

“异步方法”:用async关键字修饰的方法
1)异步方法的返回值一般是Task,T是真正的返回值类型,Task。惯例:异步方注名字以Async结尾。
2)即使方法没有返回值,也最好把返回值声明为非泛型的Task。
3)调用泛型方法时,一般在方法前加上await关键字,这样拿到的返回值就是泛型指定的T类型
4)异步方法的“传染性”:一个方法中如果有await调用,则这个方法也必须修饰为async

示例:

// 同步
class Program {
    static void Main(String[] args) {
        string path = @"E:\Biao\MyProjects\test.txt";
        File.WriteAllText(path, "hello");
        string content = File.ReadAllText(path);
        Console.WriteLine(content);
    }
}

// 异步
class Program {
    static async Task Main(String[] args) {
        string path = @"E:\Biao\MyProjects\test.txt";
        await File.WriteAllTextAsync(path, "Hello,你好");
        string content = await File.ReadAllTextAsync(path);
        Console.WriteLine(content);
    }
}

编写异步方法

示例:编写有返回值的和无返回值的异步方法

class Program {
    static async Task Main(String[] args) {
        string url = "https://www.baidu.com";
        string file = @"E:\Biao\MyProjects\test.txt";
        int len = await DownLoadUriResult(url, file);
        Console.WriteLine("ok " + len);
    }

    // 无返回值
    static async Task DownLoadUrl(string url,string filename) {
        using (HttpClient client = new HttpClient()) {
            string content = await client.GetStringAsync(url);
            await File.WriteAllTextAsync(filename, content);
        }
    }

    // 有返回值
    static async Task<int> DownLoadUriResult(string url, string file) {
        using (HttpClient client = new HttpClient()) {
            string content = await client.GetStringAsync(url);
            // string content = client.GetStringAsync(url).Result; // 与上一句效果一下,但避免
            await File.WriteAllTextAsync(file, content);
            return content.Length;
        }
    }
}

如果同样的功能,既有同步方法,又有异步方法,那么首先使用异步方法。
对于不支持的异步方法怎么办? Wait((无返回值); Result(有返回值)。风险:死锁。【尽量不用】

异步委托
如果需要把一个异步方法放到Lambda表达式,那么这个Lambda表达式添加个async,变成异步Lambda表达式

ThreadPoo1. QueueUserWorkItem(async(obj) => {
	await SomeAsync();
});

async、await原理揭秘

【ILSpy 反编译工具】
image.png

总结
**async **的方法会被C#编译器编译成一个类,会主要根据 await 调用进行切分为多个状态,对 async 方法的调用会被拆分为对MoveNext的调用。
用await看似是“等待”,经过编译后,其实没有“wait"

async背后的线程切换

await 调用的等待期间,**.NET **会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。

验证:

static async Task Main(String[] args) {
    string file = @"E:\Biao\MyProjects\test.txt";
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++) {
        sb.Append("xxxxxxxxxxxxxxxxxxxxxxx");
    }
    await File.WriteAllTextAsync(file, sb.ToString());
    
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

得到结果1和4,说明调用异步方法前后线程变了。当然有可能结果相同,这取决线程池的调度。
image.png

异步方法不等于多线程

异步方法中的代码并不会自动在新线程中执行,除非把代码放到新线程(Task.Run())中执行。

static async Task Main(String[] args) {
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Test();// 耗时操作
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

static async Task Test() {
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

    string file = @"E:\Biao\MyProjects\test.txt";
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++) {
        sb.Append("xxxxxxxxxxxxxxxxxxxxxxx");
    }
    await File.WriteAllTextAsync(file, sb.ToString());
}

结果显示:调用异步方法前的id和调用异步方法时的id都是1。说明了异步方法中不会自动在新线程中执行
image.png

为什么有的异步方法没标async?

对比:

static async Task Main(String[] args) {
    string result = await WriteNoAsync(1);
    Console.WriteLine(result);
}

// 正常的带有async的异步方法
static async Task<string> WriteAsync(int num) {
    if (num == 1) {
        string s = await File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
        return s;
    } else if (num == 2) {
        string s = await File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
        return s;
    } else {
        throw new Exception();
    }
}

// 不带有async的异步方法:Task<string> 直接就返回Task<string>类型的,没有问题。 
static Task<string> WriteNoAsync(int num) {
    if (num == 1) {
        return File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
    } else if (num == 2) {
        return File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
    } else {
        throw new Exception();
    }
}

那么为什么要写不带有async的异步方法?
image.pngimage.png

那么我们什么时候要使用async呢?
image.pngimage.png

不要用Sleep()

如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程;
而要用** await Task.Delay()。**

举例:下载一个网址,3秒后下载另一个。【使用Sleep,界面会卡死,会降低并发】
image.png

CancellationToken结构体和CancellationTokenSource类

CancellationToken用来干什么?
有时需要提前终止任务,比如:请求超时、用户取消请求。
很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。

image.pngimage.png

示例:使用不同方法

static async Task Main(String[] args) {
    string url = "https://www.baidu.com";
    int n = 100;
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(2000);

    //await DownNUrlAsync(url, n);
    //await DownNUrl2Async(url, n,cts.Token);
    await DownNUrl3Async(url, n, cts.Token);
}

// 不设置多少秒自动取消,而是自己设置某些操作来取消。比如下列的按q键,就取消了
/*
static async Task Main(String[] args) {
    string url = "https://www.baidu.com";
    int n = 100;
    CancellationTokenSource cts = new CancellationTokenSource();
    DownNUrl2Async(url, n,cts.Token);
    while (Console.ReadLine() != "q") {

    }
    cts.Cancel();
    Console.ReadLine();
}
*/

// 版本1:不加超时操作
static async Task DownNUrlAsync(string url,int n) {
    using(var client = new HttpClient()) {
        for (int i = 0; i < n; i++) {
            string s = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
        }
    }
}

// 版本2:加超时操作
static async Task DownNUrl2Async(string url, int n,CancellationToken token) {
    using (var client = new HttpClient()) {
        for (int i = 0; i < n; i++) {
            string s = await client.GetStringAsync(url);
            Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
            if(token.IsCancellationRequested) {
                Console.WriteLine("请求被取消");
                break;
            }
            // 不要if-break,直接使用throw,抛出异常。【不建议】
            //token.ThrowIfCancellationRequested();
        }
    }
}

// 版本3:加超时操作 【调用自带CancellationToken的方法:GetAsync】 这样超时的后果是抛出一个异常
static async Task DownNUrl3Async(string url, int n, CancellationToken token) {
    using (var client = new HttpClient()) {
        for (int i = 0; i < n; i++) {
            var resp = await client.GetAsync(url, token);
            string s = await resp.Content.ReadAsStringAsync();
            Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
        }
    }
}

注意:
image.png

WhenAll

Task类的重要方法
(1)Task.WhenAny(lEnumerable tasks) 等,任何一个Task完成,Task就完成。
(2)Task<TResult[]>.WhenAll(params Task[] tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
(3)FromResult() 创建普通数值的Task对象。

示例:

static async Task Main(String[] args) {
    Task<string> str1 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
    Task<string> str2 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
    Task<string> str3 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test3.txt");

    string[] strs = await Task.WhenAll(str1, str2, str3);
    string s1 = strs[0];
    string s2 = strs[1];
    string s3 = strs[2];
    
    Console.WriteLine(s1);
    Console.WriteLine(s2);
    Console.WriteLine(s3);
}

案例:统计一个文件夹下,所有文件的char字符个数

static async Task Main(String[] args) {
    //求一个文件夹下所有文件的字符数
    string path = @"E:\Biao\MyProjects";
    string[] fileNames = Directory.GetFiles(path);
    Task<int>[] countTasks = new Task<int>[fileNames.Length];

    for (int i = 0; i < fileNames.Length; i++) {
        string filename = fileNames[i];
        Task<int> ct = ReadFileCharCount(filename);
        countTasks[i] = ct;
    }

    int[] count = await Task.WhenAll(countTasks);
    int cs = count.Sum();
    Console.WriteLine(cs);
}

// 求一个文件的字符个数
static async Task<int> ReadFileCharCount(string filename) {
    string content = await File.ReadAllTextAsync(filename);
    return content.Length;
}

异步其他问题

接口中的异步方法

async是提示编译器为异步方法中的;
await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。
image.png

异步与yield

复习:yield return不仅能够简化数据的返回,而且可以让数据处理”流水线化“,提升性能。
示例
Test2()执行顺序:yield return "11"——>Console.WriteLine(i)——>yield return "22"——>Console.WriteLine(i)——>yield return "33"——>Console.WriteLine(i)

static async Task Main(String[] args) {
    foreach (var i in Test()) {
        Console.WriteLine(i);
    }

    foreach (var i in Test2()) {
        Console.WriteLine(i);
    }
}

static IEnumerable<string> Test() {
    List<string> list = new List<string>();
    list.Add("11");
    list.Add("22");
    list.Add("33");
    return list;
}

static IEnumerable<string> Test2() {
    yield return "11";
    yield return "22";
    yield return "33";
}

注意
image.png

static async Task Main(String[] args) {
    await foreach (var i in Test3()) {
        Console.WriteLine(i);
    }
}

static async IAsyncEnumerable<string> Test3() {
    yield return "11";
    yield return "22";
    yield return "33";
}

其他

ASP.NET Core和控制台项目中没有SynchronizationContext,因此不用管ConfigureAwait(false)等。
不要同步、异步混用。