门户网站 技术方案,wordpress反爬虫,上海网站建设公司哪家好?,网站推广优化建设方案一、ThreadPool线程池ThreadPool线程池是.NET Framework 2.0时代的产物#xff0c;.NET CORE时代下用的很少#xff0c;但是我们还是要理解ThreadPool线程池#xff0c;因为有助于理解后续的Task任务。1.1 为什么要用到ThreadPool线程池#xff1f;如果我们在高并发场景下使…一、ThreadPool线程池ThreadPool线程池是.NET Framework 2.0时代的产物.NET CORE时代下用的很少但是我们还是要理解ThreadPool线程池因为有助于理解后续的Task任务。1.1 为什么要用到ThreadPool线程池如果我们在高并发场景下使用Thread创建线程就会出现一个致命的问题可以看下面的代码// 使用Thread手动创建线程 { Thread thread1 new Thread(() { Console.WriteLine($创建了一个线程线程ID为{Thread.CurrentThread.ManagedThreadId}); }); thread1.Start(); Thread thread2 new Thread(() { Console.WriteLine($创建了一个线程线程ID为{Thread.CurrentThread.ManagedThreadId}); }); thread2.Start(); // 高并发场景下 for (int i 0; i 100; i) { new Thread(() { Console.WriteLine($创建了一个线程线程ID为{Thread.CurrentThread.ManagedThreadId}); }).Start(); } }运行结果结果非常牛逼克拉斯啊直接创建了一百多个线程几乎是没有重复的。并且这里只是模拟100个并发假如有一万个呢要知道我们普通的电脑总共的线程才只有几千个。所以用Thread手动创建线程是非常不可取的。那咋办呢为了不让我们自己手动创建线程.NET非常贴心的帮我们创建好了线程。它把这些创建好的线程放在一个池子里如果我们需要用到其他线程那就从池子里拿就行不需要我们自己创建。.NET将这样装有很多线程的池子命名为线程池ThreadPool,我们来看看线程池的强大之处现在不需要能理解下面的代码只需要关注执行结果// 使用线程池的线程 { for (int i 0; i 100; i) { ThreadPool.QueueUserWorkItem(_ { Console.WriteLine($创建了一个线程线程ID为{Thread.CurrentThread.ManagedThreadId}); }); } Thread.Sleep(2000); // 等任务完成 }运行结果可以看到效果非常明显原本要创建出100多个线程现在只需要线程池给我们创建好的那几个线程就行了线程复用率大大提高1.2ThreadPool线程池的工作原理看完 1.1 的对比是不是好奇为啥线程池能这么牛100 个任务只复用几个线程就搞定首先用了线程池之后我们就不需要自己去管理线程的生命周期了用Thread时你得手动new Thread()、Start()启动、甚至手动等线程完成但线程池帮你包办了所有琐事你只需要调用ThreadPool.QueueUserWorkItem把任务 “丢进池子”剩下的启动、执行、复用、回收全由线程池自动处理就像你点外卖只需要下单不用管骑手是谁、怎么取餐、送完这单去哪 —— 骑手线程的调度全由平台线程池管。其次线程池就是一个 “预创建 可复用” 的线程仓库妈妈再也不用担心我滥用线程了线程池在程序启动后会提前创建少量 “待命线程”比如默认先创建 2-4 个就像公司提前招好的 “待命员工”——当你用ThreadPool.QueueUserWorkItem丢任务时线程池不会新建线程而是直接把任务分给 “待命员工”已有线程这个线程执行完当前任务后不会销毁而是回到 “待命状态”等着接下一个任务只有当待命线程全忙、任务还在排队时线程池才会 “按需新建少量线程”但不会无限制建任务少了还会慢慢销毁多余线程。Thread和ThreadPool的对比操作场景手动用 Thread用 ThreadPool100 个任务来了建 100 个新线程100 个新骑手复用几个线程几个骑手跑 100 单任务执行完100 个线程全销毁骑手全离职线程回到待命状态骑手等下一单资源开销内存 / CPU 直接飙高资源消耗只有 Thread 的几十分之一1.3ThreadPool的核心用法如何把任务交给线程池基于前面的原理我们知道线程池的核心价值是 “复用线程、自动调度”——我们不用关心线程的创建、销毁并且我们无法创建线程池线程池是由系统创建好的我们只需要把要执行的 “任务” 交给线程池就行。而把任务传给线程池的核心方法就是ThreadPool.QueueUserWorkItem(...)。在讲具体用法前先搞懂一个基础约定线程池要求 “交给它的任务必须符合固定格式”这个格式由WaitCallback委托定义是线程池和我们的 “沟通规则”。前置线程池的任务格式 ——WaitCallback线程池规定要扔给它执行的任务必须是 “无返回值、接收一个object类型参数” 的方法。这个规则被封装成了WaitCallback委托源码核心定义如下不用记理解规则就行// 线程池的“任务格式约定”无返回值参数为object可传null public delegate void WaitCallback(object? state);我们后续调用QueueUserWorkItem时传的任务本质都是符合这个格式的方法包括匿名方法。1.3.1 基础用法无参数的任务这个静态方法的签名public static bool QueueUserWorkItem(WaitCallback callBack)接收WaitCallback参数返回bool类型返回的Bool类型如果用不到可以不接收。这里的WaitCallback委托的签名我们再前面介绍过了这里就直接创建这样的一个委托然后把它传递到QueueUserWorkItem里就行// 只有一个WaitCallback的用法 { WaitCallback waitCallback obj { Console.WriteLine($我被分配给了线程池里的线程这个线程的id是{Thread.CurrentThread.ManagedThreadId}); }; ThreadPool.QueueUserWorkItem(waitCallback); //Thread.Sleep(2000); }重点注意可以看到我这里特地写了这样一行代码//Thread.Sleep(2000);我注释了这个等待代码于是执行结果是这样的什么都没有执行这可不是因为代码写错了而是因为线程池里创建好的线程默认都是后台线程。而进程不会等待后台线程只会等待前台线程所以这里主线程执行完了后就直接杀死进程了程序退出线程池里的线程任务不会被执行于是就什么也没有打印当我们把//Thread.Sleep(2000);放开注释后主线程会在这里卡2秒2秒后线程池里的线程大概率是做完了这个任务的所以就可以正常打印了用Lambda作为WaitCallback任务传递给ThreadPool.QueueUserWorkItem方法{ for (int i 0; i 100; i) { ThreadPool.QueueUserWorkItem(obj { Console.WriteLine($我被分配给了线程池里的线程这个线程的id是{Thread.CurrentThread.ManagedThreadId}); }); } Thread.Sleep(5000); }执行结果1.3.2 进阶用法带参数的任务这个静态方法的签名public static bool QueueUserWorkItem(WaitCallback callBack, object? state)接收一个WaitCallback参数和一个object类型的可空参数返回bool类型返回的Bool类型如果用不到可以不接收。{ WaitCallback waitCallback obj { Console.WriteLine($我是{obj}我被线程{Thread.CurrentThread.ManagedThreadId}执行); }; ThreadPool.QueueUserWorkItem(waitCallback, 小王); Thread.Sleep(2000); }运行结果一个更贴近实战的例子public class OrderInfo { public int OrderId { get; set; } // 订单编号 public string UserName { get; set; } // 用户名 public decimal Amount { get; set; } // 订单金额 } { ListOrderInfo orders new ListOrderInfo() { new OrderInfo() { OrderId 1, UserName 小王, Amount 11.5m }, new OrderInfo() { OrderId 2, UserName 小明, Amount 12.5m }, new OrderInfo() { OrderId 3, UserName 小张, Amount 13.5m }, new OrderInfo() { OrderId 4, UserName 小李, Amount 14.5m } }; ThreadPool.QueueUserWorkItem(obj { var orders obj as ListOrderInfo; foreach (var orderInfo in orders) { Console.WriteLine($id{orderInfo.OrderId},username {orderInfo.UserName}Amount {orderInfo.Amount}); } },orders); Thread.Sleep(2000); Console.WriteLine(所有订单处理完毕); }运行结果1.4ThreadPool的致命缺点前面学了 ThreadPool 的用法但它有个实际开发完全绕不开的致命缺点 ——我们在主线程并不知道子线程什么时候执行完成只能无脑使用Thread.Sleep来猜子线程执行完了没。举个例子需求计算 1020 的和在主线程中拿到结果并打印。用 ThreadPool 实现如果主线程等待的事件太短了结果就拿不到{ int a 10, b 20; int sum 0; // 想存计算结果 // 用ThreadPool执行计算任务 ThreadPool.QueueUserWorkItem(obj { Thread.Sleep(1000); sum a b; // 线程内计算完成赋值给sum Console.WriteLine($线程池内计算{a}{b}{sum}); }); Thread.Sleep(500); Console.WriteLine($主线程想拿结果{a}{b}{sum}); }运行结果主线程想拿结果10200问题 1等待时间不匹配主线程拿不到正确结果线程池任务里加了Thread.Sleep(1000)意味着任务要等 1 秒后才会给sum赋值为 30主线程只加了Thread.Sleep(500)只等 0.5 秒就去打印sum—— 此时线程池任务还在 “睡觉”sum还是初始值 0所以主线程打印10200。问题 2主线程退出导致线程池任务直接中断主线程打印完sum0后整个代码块执行完毕进程直接退出控制台显示 “已退出代码为 0”—— 而线程池任务还没等到 1 秒的睡眠结束连Console.WriteLine($线程池内计算{a}{b}{sum})这行都没机会执行直接被杀死了。这就是ThreadPool最坑的地方你永远没法精准控制 “任务是否执行完”也没法让主线程 “等任务真的完成再退出”。对比 Task一行代码解决所有问题可靠又优雅同样的需求用Task改写后不管是等待还是拿结果都 100% 可靠还不会让进程提前退出{ int a 10, b 20; // Task声明执行一个返回int的任务哪怕任务要sleep1000ms Taskint sumTask Task.Run(() { Thread.Sleep(1000); // 模拟任务耗时 int sum a b; Console.WriteLine($Task内计算{a}{b}{sum}); return sum; // 直接返回结果不用共享变量 }); // 核心精准等待Task完成不用猜Sleep时间到底是睡1秒还是睡半秒拿到结果 int sum sumTask.Result; Console.WriteLine($主线程拿到结果{a}{b}{sum}); // 永远是30 }运行结果100% 可靠Task内计算102030 主线程拿到结果102030二、Task 任务 —— ThreadPool 的全能升级版既然ThreadPool有 “拿不到可靠结果、等待靠瞎猜、进程提前退出” 这些致命缺点那.NET Core 时代我们该用什么答案就是Task—— 它完全继承了ThreadPool“线程复用、控数量” 的优点又把所有痛点全解决了是实际开发中多线程编程的首选方案。2.1 Task 的核心定位为啥能替代 ThreadPool先一句话说清关系Task 是 ThreadPool 的 “高级封装”——Task 底层还是用 ThreadPool 的线程执行任务但给我们提供了更友好、更可靠的 API完美解决 ThreadPool 的 4 大痛点痛点ThreadPool解决方案Task新手直观感受任务无返回值Task 直接返回强类型结果不用共享变量不用再靠全局变量传值拿结果更靠谱等待靠 Thread.SleepResult/Wait/await 精准等待任务完成不用瞎猜 Sleep 多久任务完了才继续进程提前终止任务主线程会等 Task 执行完不会中途杀死任务任务不会被莫名中断逻辑能完整执行异常 “悄悄吞掉”可捕获 Task 的异常主线程能感知任务失败哪里错了能直接看到不用猜原因2.2 Task 核心语法Task 的用法分两类「手动创建new Task ()Start ()」和「快捷创建Task.Run ()」先学手动创建理解更透再学快捷方式实际开发常用。2.2.1 基础款new Task() Start()(手动创建延时启动)① 构造函数 1无参、无返回值(最简单的任务)适用场景只干活不拿结果比如打印、清理文件、写日志。// 用Task定义个任务 // Task构造函数的参数是一个Action委托我们直接用Lambda表达式传递 Task task new Task(() { Console.WriteLine(子线程开始执行任务); Console.WriteLine($线程Id{Thread.CurrentThread.ManagedThreadId}); Console.WriteLine(子线程结束执行任务); }); Console.WriteLine(主线程不等待直接执行Start()); // 启动任务 task.Start(); Console.WriteLine(遇到了Wait(),主线程开始等待子线程执行结束并且不需要猜子线程什么时候结束完毕); task.Wait(); Console.WriteLine(Wait()结束主线程结束等待子线程执行结束);执行结果主线程不等待直接执行Start() 遇到了Wait(),主线程开始等待子线程执行结束并且不需要猜子线程什么时候结束完毕 子线程开始执行任务 线程Id6 子线程结束执行任务 Wait()结束主线程结束等待子线程执行结束new Task(...)只是 “定义任务”不是 “执行任务”Start()是 “启动任务” 的唯一方式漏写就白定义Wait()是 “精准等待”替代 ThreadPool 的Thread.Sleep瞎猜。② 构造函数 2带参数给任务传递数据适用场景任务需要外部数据比如给指定用户发消息、处理指定订单。// 此时的构造函数是Task.Task(Actionobject? action, object? state) // 所以我们要传递一个带object类型的委托还要传递一个object类型的参数 Task task new Task(obj { string nameof obj as string; Console.WriteLine($欢迎{obj}); },张三); // 从线程池中找个线程执行这个task任务 task.Start(); // 主线程阻塞等待子线程 task.Wait();运行结果欢迎张三要点参数是object类型任务内要手动转换为目标类型用is/as最安全可传任意复杂参数比如new OrderInfo { OrderId 1001 }只需在任务内转换为OrderInfo。③ 构造函数 3可以取消适用场景任务可能需要中途终止比如用户取消操作、超时、条件不满足。核心工具CancellationTokenSource简称 CTS“叫停哨子”CancellationToken“令牌”传给任务让它能听到叫停。using CancellationTokenSource cts new CancellationTokenSource(); // 此时构造函数是Task(Action, CancellationToken) // 意味着我们要传递一个Action 和一个CancellationToken Task task new Task(() { for (int i 0; i 10; i) { if (cts.Token.IsCancellationRequested) { Console.WriteLine($子线程{Thread.CurrentThread.ManagedThreadId} 任务被取消不执行任务了......); return; } Console.WriteLine($子线程{Thread.CurrentThread.ManagedThreadId} 执行任务中......); Thread.Sleep(500); } }, cts.Token); // 找了个空闲的子线程执行任务 Console.WriteLine(遇到了Start()找了个空闲的子线程执行task任务); task.Start(); Thread.Sleep(2000); Console.WriteLine(遇到了Cancel子线程的任务被取消了); cts.Cancel(); Console.WriteLine(遇到了Wait()如果子线程没执行完主线程阻塞等待如果执行完了直接走); task.Wait();运行结果遇到了Start()找了个空闲的子线程执行task任务 子线程6 执行任务中...... 子线程6 执行任务中...... 子线程6 执行任务中...... 子线程6 执行任务中...... 遇到了Cancel子线程的任务被取消了 遇到了Wait()如果子线程没执行完主线程阻塞等待如果执行完了直接走 子线程6 任务被取消不执行任务了......要点取消任务的核心是「任务内主动用if检测令牌」只喊Cancel()不检测任务会继续干CTS 用完要用using 自动释放不然会占内存。3.1.4 构造函数 4有返回值Task拿任务结果适用场景任务干完要拿结果比如计算求和、查询数据、调用接口返回值—— 这是替代 ThreadPool 的核心优势// 使用有返回值的构造函数TaskT(Funcint function) Taskint task new Taskint(() { Console.WriteLine(任务开始我负责计算2010); return 2010; }); Console.WriteLine(遇到Start:找了个线程开始执行task任务); task.Start(); Console.WriteLine(遇到Result:主线程阻塞了等待子线程执行完毕); int sum task.Result; Console.WriteLine($子任务执行完毕了结果是{sum});运行结果遇到Start:找了个线程开始执行task任务 遇到Result:主线程阻塞了等待子线程执行完毕 任务开始我负责计算2010 子任务执行完毕了结果是30要点有返回值的任务要用TaskTT 是结果类型比如Taskstring返回字符串TaskOrderInfo返回自定义对象Result是 “阻塞式获取结果”主线程会等任务干完再拿到结果不用像 ThreadPool 那样靠共享变量传值。3.2 快捷款Task.Run ()创建 立即启动实际开发首选Task.Run()是new Task()Start()的快捷写法 —— 少写一行Start()直接创建并启动任务90% 的业务场景用这个就够。3.2.1 无返回值替代 new Task ((){...}).Start ()// 一行搞定创建启动无返回值任务 Task task Task.Run(() { Console.WriteLine(开始执行任务啦~~); }); Console.WriteLine(主线程开始等待); task.Wait(); Console.WriteLine(主线程等待结束);执行结果主线程开始等待 开始执行任务啦~~ 主线程等待结束3.2.2 有返回值替代 new Task((){...}).Start()Taskint task Task.Run(() { return DateTime.Now.Year; }); int sum task.Result; Console.WriteLine($今年是{sum}年);3.2.3 带参数小技巧用闭包传参比构造函数更简洁string userName 李四; // 要传的参数 Task paramTask Task.Run(() { // 直接用外部变量闭包不用转obj更简洁 Console.WriteLine($✅ 欢迎 {userName}); }); paramTask.Wait();四、Task 的等待方式精准等待不用瞎猜除了单个任务的Wait()Task 还支持「批量等待」满足复杂场景4.1 等待单个任务Task.Wait ()Task task Task.Run(() Thread.Sleep(1000)); task.Wait(); // 等这个任务干完4.2 等待所有任务Task.WaitAll ()适用场景批量任务都干完才继续比如批量查询数据汇总所有结果。// 创建3个任务 Task t1 Task.Run(() { Thread.Sleep(500); Console.WriteLine( t1干完); }); Task t2 Task.Run(() { Thread.Sleep(1000); Console.WriteLine( t2干完); }); Task t3 Task.Run(() { Thread.Sleep(1500); Console.WriteLine( t3干完); }); // 等所有任务干完等最慢的t3耗时1.5秒 Task.WaitAll(t1, t2, t3); Console.WriteLine( 所有任务都干完了汇总结果);4.3 等待任意一个任务Task.WaitAny ()适用场景多个任务抢跑拿到第一个结果就继续比如多源查询缓存比数据库快拿缓存结果就返回。// 创建2个任务缓存查询快数据库查询慢 Task cacheTask Task.Run(() { Thread.Sleep(500); Console.WriteLine( 缓存查询完成); }); Task dbTask Task.Run(() { Thread.Sleep(2000); Console.WriteLine( 数据库查询完成); }); // 等第一个任务干完cacheTask先完成耗时0.5秒 Task.WaitAny(cacheTask, dbTask); Console.WriteLine( 拿到第一个结果直接返回);五、Task 的异常处理不吞异常新手能定位问题ThreadPool 的异常会 “悄悄消失”Task 会把异常包装在AggregateException里主线程能精准捕获。5.1 单个任务的异常捕获Task errorTask Task.Run(() { // 模拟任务出错除以0 int num 0; int result 10 / num; }); try { errorTask.Wait(); // 调用Wait/Result时异常会抛到主线程 } catch (AggregateException ex) { // 遍历InnerExceptions拿到真实异常 foreach (var innerEx in ex.InnerExceptions) { Console.WriteLine($捕获异常{innerEx.GetType().Name} - {innerEx.Message}); } }运行结果捕获异常DivideByZeroException - 尝试除以零。5.2 批量任务的异常捕获Task t1 Task.Run(() { throw new ArgumentNullException(参数为空); }); Task t2 Task.Run(() { throw new IndexOutOfRangeException(索引越界); }); try { Task.WaitAll(t1, t2); } catch (AggregateException ex) { foreach (var innerEx in ex.InnerExceptions) { Console.WriteLine($ 异常{innerEx.Message}); } }六、Task 的进阶async/await语法糖简化异步代码async/await是 Task 的 “语法糖”—— 不用写Wait()/Result让异步代码看起来像同步代码新手学完基础后必学。6.1 基础用法异步计算求和// 步骤1定义异步方法标记async返回Task/TaskT async Taskint CalculateSumAsync() { // 步骤2await 替代 Wait()/Result非阻塞等待 int sum await Task.Run(() { Thread.Sleep(1000); return 10 20; }); return sum; } // 步骤3调用异步方法控制台程序需WaitWinForm/Web不用 async Task Main() { int sum await CalculateSumAsync(); Console.WriteLine($ 结果{sum}); // 输出30 } // 执行入口 Main().Wait();核心规则方法必须标记async返回值是Task无返回值或TaskT有返回值用await等待 Task 完成代码更简洁不阻塞线程。