欢迎访问yhm138的博客园博客, 你可以通过 [RSS] 的方式持续关注博客更新

MyAvatar

yhm138

HelloWorld!

C#中有哪些异步操作的办法

这里着重关注如何利用异步,多线程,线程池等办法来节省程序运行的时间

在C#中,有几种常见的异步操作方法:

async/await关键字

从C# 5.0开始,C#引入了async和await关键字,用于简化异步编程。通过在方法前加上async关键字并在方法内部使用await,可以让编译器自动管理异步操作和回调。需要注意的是,异步方法通常返回一个Task或Task对象,其中T表示返回的数据类型。

//这个示例展示了如何通过异步操作避免阻塞主线程,从而在等待 I/O 操作(如网络下载)时可以执行其他任务。
//同时展示了使用Task.WhenAll等待两个任务都完成才执行下一步操作
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace AsyncAwaitExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting downloads...");

            Task<string> downloadTask1 = DownloadFileAsync("https://www.example.com/file1.txt");
            Task<string> downloadTask2 = DownloadFileAsync("https://www.example.com/file2.txt");

            // 当文件下载时,执行其他任务
            PerformOtherTasks();

            // 等待所有文件下载完成
            string[] fileContents = await Task.WhenAll(downloadTask1, downloadTask2);

            Console.WriteLine("All downloads completed.");
            Console.WriteLine("File 1 content: ");
            Console.WriteLine(fileContents[0]);
            Console.WriteLine("File 2 content: ");
            Console.WriteLine(fileContents[1]);
        }

        static async Task<string> DownloadFileAsync(string url)
        {
            using HttpClient client = new HttpClient();
            string content = await client.GetStringAsync(url);
            return content;
        }

        static void PerformOtherTasks()
        {
            Console.WriteLine("Performing other tasks while files are being downloaded...");
            // 执行其他任务的代码
        }
    }
}
//这个示例展示了如何通过异步操作避免阻塞主线程,从而在等待 I/O 操作时可以执行其他任务。
//同时展示了使用Task.WhenAll等待两个任务都完成才执行下一步操作
using System;
using System.Threading.Tasks;

namespace AsyncAwaitExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting tasks...");

            Task<int> task1 = CalculateResultAsync(1, 2);
            Task<int> task2 = CalculateResultAsync(3, 4);

            // 当等待长耗时IO任务时,执行其他任务
            PerformOtherTasks();

            // 等待两个任务都完成
            int[] results = await Task.WhenAll(task1, task2);

            Console.WriteLine($"Results: {results[0]} and {results[1]}");
            Console.WriteLine("Both tasks completed. Continuing...");

            Console.ReadLine();
        }

        private static async Task<int> CalculateResultAsync(int a, int b)
        {
            await Task.Delay(TimeSpan.FromSeconds(2)); // 模拟耗时操作
            return a + b;
        }
    }
}

Task Parallel Library (TPL)

TPL是.NET Framework 4引入的,用于处理并发编程。TPL通过Task类和其他相关类来支持并发操作。可以通过调用Task.Run()方法将操作包装成异步任务。

public Task<int> CalculateResultAsync()
{
    return Task.Run(() =>
    {
        int result = LongRunningCalculation();
        return result;
    });
}

Event-based Asynchronous Pattern (EAP)

EAP是在.NET Framework中较早引入的异步模式,通常以BeginXXX和EndXXX方法成对出现,并通过事件通知调用者操作已完成。虽然此模式在较新的.NET版本中已经被async/await取代,但仍有一些旧版本库在使用。

//请注意,尽管这个示例没有直接使用BeginXXX和EndXXX方法,但它仍然是基于事件的异步模式。使用WebClient类的旧版异步API(如DownloadFileAsync方法)实际上是基于BeginXXX和EndXXX方法的,但在这里已经被封装起来了。对于不涉及网络操作的示例,可能会更明显地看到BeginXXX和EndXXX方法。但在实际开发中,我们建议使用async/await关键字和Task Parallel Library,这两种方法提供了更好的编程模型。

using System;
using System.ComponentModel;
using System.IO;
using System.Net;

namespace EAPExample
{
    class Program
    {
        static void Main(string[] args)
        {
            string url = "https://www.example.com/file.txt";
            string localPath = "file.txt";

            FileDownloader downloader = new FileDownloader();
            downloader.DownloadCompleted += Downloader_DownloadCompleted;
            downloader.DownloadProgressChanged += Downloader_DownloadProgressChanged;

            Console.WriteLine("开始下载...");
            downloader.DownloadFileAsync(url, localPath);
            Console.ReadLine();
        }

        private static void Downloader_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            Console.WriteLine($"下载进度: {e.ProgressPercentage}%");
        }

        private static void Downloader_DownloadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            Console.WriteLine("下载完成!");
        }
    }

    class FileDownloader
    {
        private WebClient _client;

        public event EventHandler<AsyncCompletedEventArgs> DownloadCompleted;
        public event EventHandler<DownloadProgressChangedEventArgs> DownloadProgressChanged;

        public void DownloadFileAsync(string url, string localPath)
        {
            _client = new WebClient();
            _client.DownloadFileCompleted += Client_DownloadFileCompleted;
            _client.DownloadProgressChanged += Client_DownloadProgressChanged;
            _client.DownloadFileAsync(new Uri(url), localPath);
        }

        private void Client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            DownloadProgressChanged?.Invoke(this, e);
        }

        private void Client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
        {
            DownloadCompleted?.Invoke(this, e);
        }
    }
}

使用线程

虽然不推荐直接使用线程进行异步操作,但在某些特殊情况下,可能需要手动创建和管理线程。可以使用Thread类或ThreadPool类创建线程。

public void StartNewThread()
{
    Thread thread = new Thread(LongRunningMethod);
    thread.Start();
}
//线程池可以帮助我们降低线程创建及销毁的开销,和上下文切换的开销。

//需要注意的是,在高并发情况下,ThreadPool可能会遇到线程饥饿问题。
//因此,在执行大量长时间运行的任务时,请确保线程池配置正确,以便为其他并发任务留出足够的线程。
public void StartNewThreadWithThreadPool()
{
    ThreadPool.QueueUserWorkItem(state => LongRunningMethod());
}

ThreadPool还有哪些好用的方法

int workerThreads;
int completionPortThreads;
ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);  //获取线程池中工作线程的最小数量
ThreadPool.SetMinThreads(workerThreads + 2, completionPortThreads);  //设置线程池中工作线程的最小数量。
ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);  //获取线程池中工作线程的最大数量
ThreadPool.SetMaxThreads(workerThreads - 2, completionPortThreads);  //设置线程池中工作线程的最大数量。
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);   //获取线程池中可用的工作线程数量。


//UnsafeQueueUserWorkItem将工作项排入队列,但不考虑调用线程的安全上下文。在某些特殊情况下,这可能会导致性能提升,但需要确保工作项不依赖于调用线程的安全上下文。
ThreadPool.UnsafeQueueUserWorkItem(state =>
{
    Console.WriteLine("Unsafe work item executing");
}, null);

//RegisterWaitForSingleObject:将等待操作排入线程池。当指定的等待句柄收到信号或超时时,线程池将执行回调方法。这允许你在等待系统资源时不阻塞线程。
AutoResetEvent autoResetEvent = new AutoResetEvent(false);
ThreadPool.RegisterWaitForSingleObject(autoResetEvent, (state, timedOut) =>
{
    Console.WriteLine("Wait handle signaled or timeout occurred");
}, null, TimeSpan.FromSeconds(10), true);




TIO

/*
ThreadPool
开10个线程,1号到10号,
结果为res,类型是 int []
第1号线程计算【从1加到5】,结果存放到res的0号位置
第2号线程计算【从6加到10】,结果存放到res的1号位置
...
第n号线程计算【从5*n-4加到5*n】,结果存放到res的(n-1)号位置
如果全部线程都计算完了,打印res。
*/
using System;
using System.Threading;

namespace ThreadPoolExample
{
    class Program
    {
        private static int[] res = new int[10];
        private static int finishedThreads = 0;
        private static object locker = new object();

        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                int threadNumber = i + 1;
                ThreadPool.QueueUserWorkItem(CalculateSum, threadNumber);
            }

            while (true)
            {
                lock (locker)
                {
                    if (finishedThreads == 10)
                    {
                        Console.WriteLine("结果数组:");
                        for (int i = 0; i < res.Length; i++)
                        {
                            Console.WriteLine($"res[{i}] = {res[i]}");
                        }
                        break;
                    }
                }
                Thread.Sleep(100);
            }
        }

        private static void CalculateSum(object state)
        {
            int threadNumber = (int)state;
            int start = 5 * threadNumber - 4;
            int end = 5 * threadNumber;
            int sum = 0;

            for (int i = start; i <= end; i++)
            {
                sum += i;
            }

            lock (locker)
            {
                res[threadNumber - 1] = sum;
                finishedThreads++;
            }
        }
    }
}

结论

总之,推荐使用async/await关键字和Task Parallel Library进行异步操作,这两种方法提供了更好的编程模型,易于理解和维护。不过,在使用旧版本库或特殊场景下,可能需要使用Event-based Asynchronous Pattern或手动管理线程。

posted @ 2023-05-09 08:59  yhm138  阅读(129)  评论(0编辑  收藏  举报