NETCORE - TASK多线程的使用
NETCORE - TASK多线程的使用
Task是从 .NET Framework 4 开始引入的一项基于队列的异步任务(TAP)模式,从 .NET Framework 4.5 开始,任何使用 async/await 进行修饰的方法,都会被认为是一个异步方法;实际上,这些异步方法都是基于队列的线程任务,从你开始使用 Task 去运行一段代码的时候,实际上就相当于开启了一个线程,默认情况下,这个线程数由线程池 ThreadPool 进行管理的。
1. Task 的使用方法
/// <summary> /// 最简单的使用方式 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask")] public IActionResult GetTask() { Console.ForegroundColor = ConsoleColor.Red; // 执行一个无返回值的任务 Task.Run(() => { Console.WriteLine("runing ..."); }); // 执行一个返回 int 类型结果的任务 var res1 = Task.Run<int>(() => { return 483; }); // 声明一个任务,仅声明,不执行 Task t = new Task(() => { Console.WriteLine("声明"); }); Console.ResetColor(); return Ok("test"); }
2. 使用 TaskFactory 工厂开始异步任务
/// <summary> /// 使用 TaskFactory 工厂开始异步任务 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask2")] public IActionResult GetTask2() { Console.ForegroundColor = ConsoleColor.Red; List<Task<int>> tasks = new List<Task<int>>(); TaskFactory factory = new TaskFactory(); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t1"); return 1; })); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t2"); return 2; })); tasks.Add(factory.StartNew<int>(() => { Console.WriteLine("t3"); return 3; })); tasks.ForEach(t => Console.WriteLine("Task:{0}", t.Result)); Console.ResetColor(); return Ok("test2"); }
上面的代码使用 TaskFactory 创建并运行了两个异步任务,同时把这两个任务加入了任务列表 tasks 中,然后立即迭代此 tasks 获取异步任务的执行结果,使用 TaskFactory 工厂类,可以创建一组人物,然后依次执行它们
3. 处理 Task 中的异常
/// <summary> /// 处理 Task 中的异常 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask3")] public IActionResult GetTask3() { Console.ForegroundColor = ConsoleColor.Red; var task = Task.Run(() => { Console.WriteLine("SimpleTask"); Task.Delay(1000).Wait(); throw new Exception("SimpleTask Error"); }); try { task.Wait(); } catch (Exception ex) { Console.WriteLine(ex.Message); } if (task.IsCompletedSuccessfully)//任务成功 { Console.WriteLine("IsCompletedSuccessfully"); } if (task.IsCompleted)//任务完成 { Console.WriteLine("IsCompleted"); } Console.ResetColor(); return Ok("test2"); }
异步任务中发生异常会导致任务抛出 TaskCancelException 的异常,仅表示任务退出,程序应当捕获该异常;然后,立即调用 Task 进行状态判断,获取内部异常
上面的代码模拟了 Task 内部发生的异常,并捕获了异常,通常情况下,推荐使用 Task 的任务状态判断以进行下一步的任务处理(如果需要),如果仅仅是简单的执行一个异步任务,直接捕获异常即可,这里使用了状态判断,如果任务已完成,则打印一则消息:IsCompleted;很明显,在上面的代码中,此 “IsCompleted” 消息并不会被打印到控制台
注意,这里使用了 task.IsCompletedSuccessfully 而不是 task.IsCompleted,这两者的区别在于,前者只有在任务正常执行完成,无异常,无中途退出指令的情况下才会表示已完成,而 task.IsCompleted 则仅仅表示“任务完成”
4. 同步上下文
在 WinForm/WPF 应用程序中,也常常需要在 UI 上开辟异步任务,通常情况下,窗体控件仅允许创建其的线程访问,在没有 Task 的时代,处理异步上下文到同步上下文是一件非常复杂的事情,在 Task 出现以后,提供了 TaskScheduler 任务调度器,让我们可以非常方便的在异步线程中访问 UI 线程的资源
/// <summary> /// 获取当前线程上下文对象 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask4")] public IActionResult GetTask4() { Console.ForegroundColor = ConsoleColor.Red; SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext(); var t1 = Task.Factory.StartNew(() => { return 1; }); t1.ContinueWith((atnt) => { Console.WriteLine("从这里访问 UI 线程的资源"); }, UISyncContext); Console.ResetColor(); return Ok("test2"); }
从上面的代码可以发现,仅仅需要调用 TaskScheduler.FromCurrentSynchronizationContext() 获得当前线程的同步上下文,然后在执行异步任务的时候传入,即可访问当前线程创建的 UI 资源
5. Task 的运行方式
基于 ThreadPool 线程池的方式
/// <summary> ///基于 ThreadPool 线程池的方式 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask5")] public IActionResult GetTask5() { Console.ForegroundColor = ConsoleColor.Red; var available = ThreadPool.SetMaxThreads(8, 16); Console.WriteLine("result:{0}", available); Console.ResetColor(); return Ok("test2"); }
一个异步任务总是处于队列中,任务队列基于先进先出的原则,最先进入队列的任务总是最先被执行;但是,在多线程的环境下,最先执行并不意味着最先结束,意识到这一点很重要,每个任务可调度的资源和处理的进度决定了任务的完成时间。
默认情况下,所有的任务都使用 ThreadPool 的资源,当你开启一个 Task 的时候,实际上,是由 ThreadPool 分配了一个线程,ThreadPool 的上限取决于很多方面的因素,例如虚拟内存的大小,当 Task 开启的数量超过ThreadPool 的上限的时候,Task 将进入排队状态,可以手动设置 ThreadPool 的大小
上面的代码表示设置当前程序可使用的线程池大小,但是,SetMaxThreads 的值不应该小于托管服务器的 CPU 核心数量,否则,变量 available 的值将显示为 false,表示未成功设置线程池上限
注意:ThreadPool 上的所有线程都是后台线程,也就是说,其IsBackground属性是true,在托管程序退出后,ThreadPool 也将会退出。
6. 长时间运行于后台的任务
/// <summary> /// 长时间运行于后台的任务 /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask6")] public IActionResult GetTask6() { Console.ForegroundColor = ConsoleColor.Red; Task.Factory.StartNew(() => { Console.WriteLine("LongRunging Task"); }, TaskCreationOptions.LongRunning); Console.ResetColor(); return Ok("test2"); }
在创建 Task 的时候,我们可能需要做一些长时间运行的业务,这个时候如果使用默认的 ThreadPool 资源,在并发状态下,这是不合适的,因为该任务总是长时间的占用线程池中的资源,导致线程池数量受限,这种情况下,可以在创建任务的时候使用指定 TaskCreationOptions.LongRunning 方式创建 Task
7.有条件的 Task
/// <summary> /// 有条件的 Task /// </summary> /// <returns></returns> [HttpGet] [Route("GetTask7")] public IActionResult GetTask7() { Console.ForegroundColor = ConsoleColor.Red; //var order1 = Task.Run<string>(() => { Console.WriteLine("Order 1 "); return "order1 result"; }); //// 匿名委托将等待 order1 执行完成后执行,并将 order1 对象作为参数传入 //order1.ContinueWith((task) => { Console.WriteLine("order1 is completed! {0}", task.Result); }); //var t1 = Task.Run(() => { Task.Delay(1500).Wait(); Console.WriteLine("t1"); }); //var t2 = Task.Run(() => { Task.Delay(2000).Wait(); Console.WriteLine("t2"); }); //var t3 = Task.Run(() => { Task.Delay(3000).Wait(); Console.WriteLine("t3"); }); //Task.WaitAll(t1, t2, t3); //// t1,t2,t3 完成后输出下面的消息 //Console.WriteLine("t1,t2,t3 Is Complete"); //var t4 = Task.Run(() => { Task.Delay(1500).Wait(); Console.WriteLine("t4"); }); //var t5 = Task.Run(() => { Task.Delay(2000).Wait(); Console.WriteLine("t5"); }); //var t6 = Task.Run(() => { Task.Delay(3000).Wait(); Console.WriteLine("t6"); }); //Task.WaitAny(t4, t5, t6); //// 当任意任务完成时,输出下面的消息,目前按延迟时间计算,在 t4 完成后立即输出下面的信息 //Console.WriteLine("t4,t5,t6 Is Complete"); //var t7 = Task.Run(() => { Task.Delay(1500).Wait(); Console.WriteLine("t7"); }); //var t8 = Task.Run(() => { Task.Delay(2000).Wait(); Console.WriteLine("t8"); }); //var t9 = Task.Run(() => { Task.Delay(3000).Wait(); Console.WriteLine("t9"); }); //var whenAll = Task.WhenAll(t7, t8, t9); //// WhenAll 不会等待,所以这里必须显示指定等待 //whenAll.Wait(); //// 当所有任务完成时,输出下面的消息 //Console.WriteLine("t7,t8,t9 Is Complete"); var t10 = Task.Run(() => { Task.Delay(1500).Wait(); Console.WriteLine("t10"); }); var t11 = Task.Run(() => { Task.Delay(2000).Wait(); Console.WriteLine("t11"); }); var t12 = Task.Run(() => { Task.Delay(3000).Wait(); Console.WriteLine("t12"); }); var whenAny = Task.WhenAll(t10, t11, t12); // whenAny 不会等待,所以这里必须显示指定等待 whenAny.Wait(); // 当任意任务完成时,输出下面的消息,目前按延迟时间计算,在 t10 完成后立即输出下面的信息 Console.WriteLine("t10,t11,t12 Is Complete"); Console.ResetColor(); return Ok("test2"); }
Task 内部提供多种多样的基于队列的链式任务管理方法,通过使用这些快捷方式,可以让异步队列有序的执行,比如ContinueWith(),ContinueWhenAll(),ContinueWhenAny(),WaitAll(),WaitAny(),WhenAll(),WhenAny()
值得注意的是,当调用 WhenAll 方法时,会返回执行任务的状态,此状态是所有任务的统一状态,如果执行了 3 个任务,而其中一个出错,则返回任务状态表示为:Faulted,如果任意任务被取消,则状态为:Canceled;
当调用 WhenAny() 方法时,表示任意任务完成即可表示完成,此时,会返回最先完成的任务信息
注意:WhenAll 和 WhenAny 方法正常执行,无异常,无取消,则所返回的完成状态表示为:RanToCompletion
控制Task的并发数量,该怎么做?(下文会介绍更好的解决方案)
{ //假如说我想控制下Task的并发数量,该怎么做? 20个 List<Task> taskList = new List<Task>(); for (int i = 0; i < 10000; i++) { int k = i; if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 20) { Task.WaitAny(taskList.ToArray()); taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList(); } taskList.Add(Task.Run(() => { Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(2000); })); } }
Parallel类
Parallel可以启动多个线程去并发执行多个Action,它是多线程的。Parallel最直观的特点是主线程(当前线程)也会参与计算---阻塞界面(主线程忙于计算,无暇他顾)。
Parallel是在Task的基础上做了一个封装,它的效果等于TaskWaitAll+主线程(当前线程)参与计算。
/// <summary> /// Parallel /// </summary> private void btnParallel_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnParallel_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); { //Parallel并发执行多个Action 多线程的 //主线程(当前线程)会参与计算---阻塞界面 //等于TaskWaitAll+主线程计算 Parallel.Invoke( () => this.DoSomethingLong("btnParallel_Click_1"), () => this.DoSomethingLong("btnParallel_Click_2"), () => this.DoSomethingLong("btnParallel_Click_3"), () => this.DoSomethingLong("btnParallel_Click_4"), () => this.DoSomethingLong("btnParallel_Click_5")); } Console.WriteLine($"****************btnParallel_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
Parallel其他的API如下所示:
//主线程(当前线程)会参与计算 { Parallel.For(0, 5, i => this.DoSomethingLong($"btnParallel_Click_{i}")); } //主线程(当前线程)会参与计算 { Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, i => this.DoSomethingLong($"btnParallel_Click_{i}")); }
如何控制并发(线程)数量?上面我们在介绍Task的时候已经写了一种解决方案,其实还有一种更好的解决方案,就是使用Parallel来实现。
//控制线程数量(并发数量) //会阻塞当前线程(主线程)-- 卡界面 { ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}")); } //控制线程数量(并发数量) //不会阻塞当前线程(主线程) { Task.Run(() => { ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 3; Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}")); }); }
测试
[Route("TaskTest")] [HttpGet] public async Task TaskTest() { getThreadCount(); Task.Run(() => { ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = 5; Parallel.For(0, 100, options, i => funText(i)); }); } private void funText(int i) { getThreadCount(); Thread.Sleep(3000); Console.WriteLine($"工作线程{i}:" + Thread.CurrentThread.ManagedThreadId); } private int getThreadCount() { int threadCount = 0; using (Process process = Process.GetCurrentProcess()) { // 获取当前进程的所有线程 ProcessThreadCollection threads = process.Threads; // 计算线程数 threadCount = threads.Count; Console.WriteLine($"当前进程打开的线程数: {threadCount}"); } return threadCount; }
项目:NETCORE.TASKRORD
附代码:https://gitee.com/wuxincaicai/NETCORE.git
引用:https://www.cnblogs.com/viter/p/10201228.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异