找回密码
 立即注册
快捷导航

[.NET] 【杨中科】11-21 学习笔记

[复制链接]
妙笔生花 2023-9-5 05:12:29 | 显示全部楼层
本帖最后由 妙笔生花 于 2023-9-5 05:15 编辑

11、异步编程1

误解:

  • 会提高系统的运行效率?
  • 就是多线程?

什么是异步编程?

举例:服务员点菜;

  • 同步:你叫服务员,服务员拿菜单给你站着等待你点菜,你点完菜后给服务员拿到后厨;
  • 异步:你叫服务器,服务员拿菜单给你,服务员先去忙了,等你点完菜后,再叫服务员拿好点好的菜单到后厨;
    • 你叫服务员,相当于线程处理你的业务
    • 你在点餐,相当于在处理耗时操作
    • 服务员等你点菜,相当于当前线程在等待你处理耗时操作
    • 服务员去忙,相当于你处理耗时操作,当前线程去忙其他了
    • 再叫服务员拿好点好的菜单到后厨,相当于重新申请一个线程(不一定是先前的服务员)来帮你处理耗时出来的结果;

异步点餐一定会提升单个客户点餐速度吗?不能;

能减少耗时操作吗?也不能;

异步能服务更多的客户,也就是提高并发数量;

异步的主要特色是:不等,即意思是不在主线程里等, 不会阻塞, 在子线程里还是要等

异步能使得单个请求处理效率变快吗?不会,异步只是提高web服务器能够同时请求数量,仅此而已;

async、await ≠ 多线程

12、异步编程2 async/await使用

异步方法:用async关键字修饰的方法

异步方法的返回值一般是 Task<T>T 是真正的返回值类型,Task<int> 。惯例:异步方法名字以ASync结尾。

即使方法没有返回值,也最好把返回值类型void声明为非泛型的Task;

调用异步方法时,一般在方法前加上await关,这样拿到的返回值就是泛型指定的T类型;

异步方法的“传染性”:一个方法中如果有await调用,则这个方法也必须修饰为async;

static async Task Main(string[] args){
    string fileName = "d:/1.txt";
    File.Delete(fileName);
    File.WriteAllTextAsync(fileName, "hello,world.");
    String s = await File.ReadAllTextAsync(fileName);
    Console.WriteLine(s);
}

不加await的调用异步方法:

案例1:

static async Task Main(string[] args){
    string fileName = "d:/1.txt";
    File.Delete(fileName);
    StringBuilder sb = new ();
    for(int i = 0; i< 10000; i++) sb.AppendLine("hello");
    File.WriteAllTextAsync(fileName, sb.ToString());   // 这里不加await,会直接到下面读取
    String s = await File.ReadAllTextAsync(fileName);   // 上面没写完,直接读取会报错
    Console.WriteLine(s);
}

案例2:

public async Task GetSomeDataAsync()
{
    Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(1000);
    Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}

static async Task Main(string[] args)
{
    Console.WriteLine("Main Start." + Thread.CurrentThread.ManagedThreadId);
    Task<string> task = GetSomeDataAsync(); // 没有使用await关键字
    // 继续执行后续代码
    Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
    Console.ReadKey();
}

13、异步编程3 编写异步方法

static async Task Main(string[] args)
{
    await DownloadHtmlAsync("https://www.baidu.com/", "d:/1.txt");
}
static Task<int> DownloadHtmlAsync(string url, string filename){
    string html = await httpClient.GetStringAsync(url);
    await File.WriteAllTextAsync(filename, html);
    return html.length;
}

理解 .Wait() 和 .Result (不建议使用)

用于等待异步任务完成并获取其结果的方法

用在哪里?对于方法不能是async,且方法内部调用其他方法只有同步的话,就只能使用 .Wait 或者 .Result 了:

static void Mian(string[] args){         // 假设 Main 没有 async
    // 假设没有同步方法 WriteAllText()
    File.WriteAllTextAsync(@"d:/1.txt", "hhhhhhhhhhhhhhhhhh").Wait();
}

.Wait()  无返回值;

.Result 有返回值;

  • .Wait()Task类的实例方法,可以直接在Task对象上调用,例如task.Wait()
  • .Wait()会将异步任务中的异常封装在AggregateException中并抛出,需要进行适当的异常处理。如果异步任务发生多个异常,AggregateException将包含这些异常的集合。
  • .ResultTask类的属性,可以通过访问Task对象的Result属性来获取任务的结果,例如task.Result
  • .Result会将异步任务中的异常直接抛出,不会进行额外的封装。如果异步任务发生多个异常,.Result只会抛出其中一个异常。

都是阻塞操作,与 await 不同的是,await 不等待直接返回,而 .Wait/.Result 会阻塞等待耗时操作完成;

死锁:

使用.Wait()或.Result来等待异步任务的完成可能会导致死锁的风险,特别是在UI线程或主线程中使用时。

  1. 在UI线程或主线程中使用.Wait().Result:如果在UI线程或主线程中使用.Wait().Result来等待异步任务的完成,而该任务需要访问UI或主线程上的资源,就会发生死锁。因为.Wait().Result会阻塞当前线程,而异步任务需要在UI或主线程上执行,两者相互等待对方完成,导致死锁。
  2. 使用同步上下文的异步方法:某些异步方法会使用同步上下文(例如ConfigureAwait(true)),这会导致异步任务在原始的上下文中执行。如果在该上下文中使用.Wait().Result来等待任务的完成,就会发生死锁。因为同步上下文可能需要在当前线程上执行任务,而.Wait().Result会阻塞当前线程,导致死锁。

所以,不建议使用 .Wait() 和 .Result

异步调用的好处?

ThreadPool.QueueUserWorkItem(async (obj) => {     // 模拟并发请求
    while(true){
        await File.WriteAllTextAsync(@"d:/1.txt", "hhhhhhhhhhhhhhhhhh");
        Console.WriteLine("xxxxxxxxxxx");
    }
});
Console.Read();

加深理解 async/await

static async Task Run()
{
    Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(5000);
    // 这里重新申请了线程,来执行后面的代码
    Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task StartRun()
{
    Console.WriteLine("StartRun Start." + Thread.CurrentThread.ManagedThreadId);
    await Run();
    Console.WriteLine("StartRun End." + Thread.CurrentThread.ManagedThreadId);
}

static async Task Main(string[] args)
{
    StartRun();
    Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(5000);
    Console.ReadKey();
}

------------------------

StartRun Start.1
Run Start.1
Main End.1    <---- 输出这里后等待了5秒
Run End.3
StartRun End.3

14、异步编程4 async/await 原理

将下面代码编译,用ILSPY反编译工具查看:

用ILSpy反编译dll(.exe只是windows下的启动器)成c#4.0版本,即:

  • 需要改为 C# 4.0/VS2010
  • 视图 - 显示所有类型和成员
static async Task Main(string[] args)
{
    using(HttpClient httpClient = new HttpClient())
    {
        string html = await httpClient.GetStringAsync("https://www.baidu.com/");
        Console.WriteLine(html);
    }
    string destFilePath = "D:/1.txt";
    string content = "Hello async and await";
    await File.WriteAllTextAsync(destFilePath, content);
    string content2 = await File.ReadAllTextAsync(destFilePath);
    Console.WriteLine(content2);
    Console.Read();
}

【杨中科】11-21 学习笔记7544 作者:妙笔生花 帖子ID:753

await、async是“语法糖”,最终编译成“状态机调用”

总结:async的方法会被C#编译器编译成一个类,主要根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。

用await看似是“等待”,经过编译后,其实没有“wait

为什么要把一个async方法拆分为多个状态然后分为多次调用?

异步的可以避免线程等待耗时操作”但是await还是等待呀?

15、异步编程5 async背后的线程切换

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

案例1:

static async Task Main(string[] args){    
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    StringBuilder sb = new ();
    for(int i = 0; i< 10000; i++) sb.AppendLine("hello");
    await File.WriteAllTextAsync("d:/1.txt", sb.ToString());
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    Console.ReadKey();
}
// --------------  多运行几次查看结果
1
8

案例2:

static async Task Run()
{
    Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(5000);
    // 这里重新申请了线程,来执行后面的代码
    Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task StartRun()
{
    Console.WriteLine("StartRun Start." + Thread.CurrentThread.ManagedThreadId);
    await Run();
    Console.WriteLine("StartRun End." + Thread.CurrentThread.ManagedThreadId);
}

static async Task Main(string[] args)
{
    StartRun();
    Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(5000);
    Console.ReadKey();
}

------------------------

StartRun Start.1
Run Start.1
Main End.1    <---- 输出这里后等待了5秒
Run End.3
StartRun End.3

16、异步编程6 异步方法不等于多线程

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

public static async Task CalcAsync()
{
    Console.WriteLine("CalcAsync Start " + Thread.CurrentThread.ManagedThreadId);
    await  Task.Run(() =>
    {
        Console.WriteLine("Task Start " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);  // 模拟耗时操作
        Console.WriteLine("Task End" + Thread.CurrentThread.ManagedThreadId);
    });
    Console.WriteLine("CalcAsync End " + Thread.CurrentThread.ManagedThreadId);
}

static async Task Main(string[] args)
{
    Console.WriteLine("Main Start " + Thread.CurrentThread.ManagedThreadId);
    await CalcAsync();
    Console.WriteLine("Main End " + Thread.CurrentThread.ManagedThreadId);
    Console.ReadKey();
}
// --------------
Main Start 1
CalcAsync Start 1
Task Start 8    <------这里输出后等待3s
Task End 8
CalcAsync End 8
Main End 8

17、异步编程7 为什么有的异步方法没有async?

比如:File.ReadAllTextAsync();

【杨中科】11-21 学习笔记5852 作者:妙笔生花 帖子ID:753

await 和 async 成对出现的,方法没有await,那么也就不需要async了....

其他案例:

public static Task<string> CalcAsync()
{
    return Task.Run(() =>       // 直接返回 Task<string>
    {
        Thread.Sleep(3000);  // 模拟耗时操作
        return Task.FromResult("ok");    // 将 字符串 包装成 Task<string>
    });
}

static async Task Main(string[] args)
{
    string result = await CalcAsync();
    await Console.Out.WriteLineAsync(result);
    Console.ReadKey();
}

async方法缺点:

1、异步方法会生成一个类运行效率没有普通方法高;

2、可能会占用非常多的线程;

优点:

写起来简单、提高请求数量,响应速度正常;

18、异步编程8 不要使用Shellp()

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

举例:下载一个网址,3秒后下载另一个:

在控制台中没看到区别,但是放到WinForm程序中就能看到区别了。

ASP.NET Core 中也看不到区别,但是Sleep()会降低并发。

用winfrom操作

private async void button1_Click(object sender, EventArgs e){
    using(HttpClient httpClient = new ()){
        string s1 = awiat httpClient.GetStringAsync("https://www.baidu.com/");
        textBox1.Text = s1.SubString(0,100);
        // Thread.Sleep(3000);   // 会阻塞主线程(UI线程)
        await Task.Delay(3000);    // 不会阻塞主线程(UI线程)
        string s2 = awiat httpClient.GetStringAsync("https://www.so.cn/");
        textBox2.Text = s2.SubString(0,100);
        Console.ReadKey();
    }
}

19、异步编程9 CancellationToken

有时需要提前终止任务,比如:请求超时、用户取消请求。

很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。

CancellationToken 结构体

None:空

  • bool lsCancellationRequested 向异步中发送是否取消
  • (*)Register(Action callback) 注册取消监听(不常用)
  • ThrowlfCancellationRequested() 如果任务被取消,执行到这句话就抛异常。

CancellationTokenSource   通过这个类创建 CancellationToken 对象;

  • CancelAfter() 超时后发出取消信号

  • Cancel() 发出取消信号

CancellationToken Token

例子:为“下载一个网址N次”的方法增加取消功能。

分别用 GetStringAsync + IsCancellationRequested、GetStringAsync + ThrowlfCancellationRequested()、带 CancellationToken 的 GetAsync() 分别实现。

取消分别用超时、用户敲按键(不能await)实现。

static HttpClient client = new HttpClient();

public static async Task DownloadAsync(string url, int n, CancellationToken cancellationToken)
{
    for (int i = 0; i < n; i++)
    {
        string html = await client.GetStringAsync(url);
        await Console.Out.WriteLineAsync($"{DateTime.Now}: {html}");

        #region 方法1,判断是否取消
        //if (cancellationToken.IsCancellationRequested)
        //{
        //    Console.WriteLine("请求被取消.");
        //    break;
        //} 
        #endregion

        #region 方法2,取消状态时抛出异常
        cancellationToken.ThrowIfCancellationRequested();
        #endregion
    }
}

static async Task Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(1000);       // 超时触发,超过1s就取消执行            
    CancellationToken cToken = cts.Token;
    try
    {
        await DownloadAsync("https://www.baidu.com", 100, cToken);
    }
    catch (OperationCanceledException e)
    {
        await Console.Out.WriteLineAsync("任务被取消了...");
    }

    // cts.Cancel();     // 手动触发取消执行

    Console.ReadKey();
}

ASP.NET Core 开发中,一般不需要自已处理 CancellationToken、CancellationTokenSource这些,只要做到“能转发CancellationToken就转发“即可。ASP.NET Core 会对于用户请求中断进行处理。

(*)演示一下ASP.NETCore中的使用: 写一个方法,Delay1000次,用Debug.WriteLine(输出,访问中间跳到放到其他网站。

static HttpClient client = new HttpClient();

public async Task<IActionResult> Index(CancellationToken cancellationToken){
    await DownloadAsync("https://www.baidu.com", 1000, cancellationToken);      // 请求后一直刷新输出,只有离开页面才停止
    //await DownloadAsync("https://www.baidu.com", 1000, CancellationToken.None);    // 请求后一直刷新输出,只有停止服务器才停止
    return View();
}

public static async Task DownloadAsync(string url, int n, CancellationToken cancellationToken)
{
    for (int i = 0; i < n; i++)
    {
        string html = await client.GetStringAsync(url);
        Debug.WriteLine(html);

        if (cancellationToken.IsCancellationRequested)
        {
            Debug.WriteLine("请求被取消.");
            break;
        }
    }
}

20、异步编程10 WhenAll

1.Task<Task> WhenAny(lEnumerable<Task> tasks) 等,任何一个Task完成,Task就完成。

2.Task<TResultl> WhenAll<TResult>(params Task<TResult>[]tasks) 等,所有Task完成Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。

3.FromResult() 创建普通数值的Task对象。

Task<string> t1 = File.ReadAllTextAsync("D:/1.txt");
Task<string> t2 = File.ReadAllTextAsync("D:/2.txt");
Task<string> t3 = File.ReadAllTextAsync("D:/3.txt");
string[] results = await Task.WhenAll(t1, t2, t3);
string s1 = results[0];
string s2 = results[1];
string s3 = results[2];

21、异步编程11 异步其他问题

接口中的异步方法:

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

interface ITest
{
    async Task<int> GetChatCount(string file);
}

// 只能在具有正文的方法中使用“async”修饰符

【杨中科】11-21 学习笔记8388 作者:妙笔生花 帖子ID:753

所以,应该这样:

interface ITest
{
    Task<int> GetChatCount(string file);
}

class Test1 : ITest
{
    public async Task<int> GetChatCount(string file)
    {
        return await Task.FromResult(100);
    }
}

异步与yield:

复习:yield return 不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。

foreach(var item in Test()){
    Console.WriteLine(item);
}

static IEnumerable<string> Test(){
    yield return "hello";
    yield return "yzk";
    yield return "youzack";
}

在旧版c#中,async方法中不能用yield。从C#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用 await foreach() 即可。

await foreach(var item in Test()){
    Console.WriteLine(item);
}

static async IAsyncEnumerable<string> Test(){
    yield return "hello";
    yield return "yzk";
    yield return "youzack";
}

ASP.NET Core和控制台项目中没有 SynchronizationContext,因此不用管 ConfigureAwait(false)等这些了。

不要同步、异步混用。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

温馨提示

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

Archiver|手机版|小黑屋|DLSite

GMT+8, 2024-11-22 16:18

Powered by Discuz! X3.5 and PHP8

快速回复 返回顶部 返回列表