如何在 .NET Core 中轻松实现异步编程并提升性能

发布于:2024-12-22 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

初识异步编程

与多线程关系

异步编程操作


初识异步编程

        异步编程:是指在执行某些任务时程序可以在等待某个操作完成的过程中继续执行其他任务,而不是阻塞当前线程,这在处理I/O密集型操作(如文件读取、数据库查询、网络请求等)时尤为重要,能够显著提高应用程序的性能和响应能力。

在.net core中异步编程主要通过asyncawait关键字来实现,结合Task类进行异步操作的管理:

async:标记方法为异步方法使其能够包含await表达式

await:在异步方法中用于等待一个异步操作完成,允许方法在等待期间继续执行其他代码而不会阻塞线程

异步方法通常返回TaskTask<T> 类型:

Task:表示一个没有返回值的异步操作

Task<T>:表示一个带返回值的异步操作,其中T是返回值的类型

        作用意义:在进行项目开发的时候通常都会遇到异步编程的使用方式,具备的意义如下:

1)提高应用程序响应性:异步编程可以使得应用程序在等待长时间操作(例如网络请求或文件读写)时继续处理其他任务而不会卡住主线程,比如在桌面应用或Web应用中用户界面不会因等待数据加载而变得无响应

2)提升性能:传统的同步编程会阻塞线程直到操作完成,这可能导致线程资源的浪费,特别是在高并发场景下异步编程允许线程释放去处理其他任务,避免了线程饥饿问题,最大化利用CPU资源特别是在I/O密集型操作时

3)节省资源:异步操作不需要为每个操作分配新的线程,因此能够节省系统资源,在高并发情况下异步编程可以显著减少上下文切换开销

4)适用于I/O密集型应用:异步编程特别适用于那些需要大量I/O操作的应用,例如网络请求、磁盘读取、数据库查询等。通过异步操作可以避免长时间等待I/O操作而导致的性能瓶颈

接下来我们通过异步方法从指定的URL路径上下载网页内容,将网页内容保存到本地文件并输出下载的内容的字节数,代码结果如下所示:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace Program
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int len = await DownloadHtmlAsync("https://www.baidu.com", @"d:\test\1.txt");
            Console.WriteLine($"Downloaded {len} bytes.");
        }
        static async Task<int> DownloadHtmlAsync(string url, string filename)
        {
            using (HttpClient client = new HttpClient())
            {
                string html = await client.GetStringAsync(url);
                await File.WriteAllTextAsync(filename, html);
                return html.Length;
            }
        }
    }
}

与多线程关系

异步编程并不等于多线程,这是两个概念,异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行,以下做一个简单的演示:

使用异步方法来执行长时间运行的计算,通过Thread.CurrentThread.ManagedThreadId输出当前执行线程的ID,可以看到异步方法和线程调度的关系如下,这里我们通过计算5000*5000的情况避免执行太快:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace Program
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId);
            double r = await CalcAsync(5000);
            Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId);
        }
        static async Task<double> CalcAsync(int n)
        {
            Console.WriteLine("CalcAsync" + Thread.CurrentThread.ManagedThreadId);
            double result = 0;
            Random rand = new Random();
            for (int i = 1; i <= n*n; i++)
            {
                result += rand.NextDouble();
            }
            return result;
        }
    }
}

那么如何在新的线程中执行异步方法呢?这里我们必须手动将异步方法放到新的线程中,这里我们只需要将要执行的代码以委托的形式传递给Task.Run(),这样就会从线程池中取出一个线程来执行我们的委托,如下可以看到我们的线程已经发送了变化:

如果一个异步方法只是对别的异步方法调用转发,并没有太多复杂的逻辑,那么就可以去掉异步关键字直接返回Task,如下所示:

对于多线程来讲,在await调用的等待期间.net会把当前的线程返回给线程池,等异步方法调用执行完毕之后,框架会从线程池再取出一个线程执行后续代码,怎么理解?比如说你进入一家餐馆服务器给你递上一个菜单,在等待你选菜的时候服务你的服务员可能会继续服务别人,当你点餐完毕提交给服务员之后,服务你的服务员可能就不是刚刚服务你的人了,就是这么个意思,当然如果异步等待时间极短,线程可能就不会发送变化:

尽管异步编程和多线程都涉及到并发执行任务,但它们的工作原理和适用场景有所不同,可以把它们看作是两个不同的工具用于解决不同类型的并发问题:

异步编程:通过非阻塞的方式执行操作,适用于 I/O 密集型任务,能够节省线程资源提高应用的响应性。

多线程:通过并行执行任务来提高CPU密集型任务的性能,但线程管理复杂可能带来上下文切换和同步问题。

异步编程与多线程的结合:异步编程通过减少阻塞来提高效率而多线程通过并行执行来加速计算密集型任务,在某些情况下它们可以结合使用:例如异步任务通过线程池线程执行,充分利用多核处理器的计算能力。

异步编程操作

异步等待:如果想在异步方法中暂停一段时间的话,这里可以使用 await Task.Delay()的方式,例如下载一个网址然后等待3秒再下载另一个,注意这里是不能使用Thread.Sleep()方法的,该方法是阻塞线程用的,异步编程并不适用,这里我们演示一下异步等待的效果,如下所示:

class Program
{
    static async Task Main(string[] args)
    {
        var input = Console.ReadLine();
        Console.WriteLine(input);
        await Task.Delay(3000);
        Console.WriteLine(input+".net core");
    }
}

异步取消:在异步编程中CancellationToken是用于取消异步操作的一种机制,它提供了一种优雅的方式来中止正在进行的操作,当用户操作可能会长时间运行时,例如下载文件、访问数据库、调用远程API等,CancellationToken用于支持任务的取消操作,示例如下:

class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        cts.CancelAfter(3000);
        CancellationToken cToken = cts.Token;
        await Download1Async("https://www.baidu.com", 100, cToken);
    }
    static async Task Download1Async(string url, int n, CancellationToken token)
    {
        using(HttpClient client = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                string html = await new HttpClient().GetStringAsync(url);
                Console.WriteLine($"{DateTime.Now}:{html}");
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("超时任务取消");
                    break;
                }
            }
        }
    }
} 

当我们打印数据的时候,请求超过5秒还没有结束的话我们就手动取消异步操作:

Task类方法:在Task类中有许多方法可以使用,以下是其常用的方法讲解:

1)Task.WhenAny():任何一个Task完成,Task就完成

2)Task.WhenAll():所有Task完成Task才完成,用于等待多个任务执行结束不在乎顺序

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

如下我们可同WhenAll拿到所有文件的数据:

class Program
{
    static async Task Main(string[] args)
    {
        Task<string> t1 = File.ReadAllTextAsync(@"d:\test\1.txt");
        Task<string> t2 = File.ReadAllTextAsync(@"d:\test\2.txt");
        Task<string> t3 = File.ReadAllTextAsync(@"d:\test\3.txt");
        string[] result = await Task.WhenAll(t1, t2, t3);
    } 
}