C# WinForm 线程池与事件等待+进度条暂停恢复实战案例 笔记适配说明承接上一篇原生Thread多线程笔记统一排版、代码拆分、修复原代码注释歧义详解【线程池】【ManualResetEvent手动事件等待】两大知识点修正原有模糊注释补充执行流程、底层机制、易错坑点适配课堂学习期末复盘一、前置命名空间整理1.1 原始冗余引用清理原代码大量无用引用本项目只用到线程、窗体相关剔除无效依赖标注每一行作用//基础必备命名空间 using System; //集合类本案例未使用冗余引用 //using System.Collections.Generic; //控件元数据窗体底层依赖无实际业务作用 //using System.ComponentModel; //数据库操作完全无用冗余引用 //using System.Data; //GDI绘图窗体渲染依赖业务无关 //using System.Drawing; //Linq查询未使用冗余引用 //using System.Linq; //字符串编码 using System.Text; //✅线程线程池事件等待 核心命名空间 using System.Threading; //高阶异步任务本案例未使用 //using System.Threading.Tasks; //✅WinForm窗体控件命名空间 using System.Windows.Forms;1.2 窗体基础结构窗体控件1个按钮button1触发线程阻塞、事件放行案例namespace _2线程池和事件等待 { public partial class Form1 : Form { public Form1() { InitializeComponent(); //初始化窗体、控件 //模块1构造函数-线程池基础案例 } //模块2按钮点击-事件等待案例 private void button1_Click(object sender, EventArgs e) { } } }二、知识点一线程池 ThreadPool2.1 线程池核心概念逐句优化原注释定义CLR提前初始化好、存放若干空闲线程的线程容器内置固定数量工作线程执行逻辑需要执行异步任务时优先从池子调取空闲线程无空闲线程则排队等待任务执行完毕线程不会销毁自动归还线程池复用核心优势规避频繁new Thread创建、销毁线程减少CPU开销、内存碎片提升性能开发者无需手动管理线程生命周期简化代码核心缺点无法获取线程实例对象不能设置线程名称、线程优先级无法管控线程状态不能手动暂停、终止线程可控性差适用场景大量短时、简单异步任务不适用需要精细化管控线程的长耗时任务2.2 线程池基础代码拆分构造函数内部2.2.1 核心方法讲解ThreadPool.QueueUserWorkItem()向线程池提交任务自动分配池内空闲线程执行第一个参数回调委托线程执行的业务逻辑第二个参数传入线程的数据委托内部通过参数接收2.2.2 拆分带注释完整版代码public Form1() { InitializeComponent(); #region 线程池基础使用 //提交任务1复用线程池空闲线程传入参数abc1 ThreadPool.QueueUserWorkItem(s { //s接收外部传入的参数object类型自动装箱 Console.WriteLine(s); }, abc1); //提交任务2 ThreadPool.QueueUserWorkItem(s { Console.WriteLine(s); }, abc2); //提交任务3 ThreadPool.QueueUserWorkItem(s { Console.WriteLine(s); }, abc3); #endregion }2.3 运行现象踩坑控制台输出顺序随机不一定按abc1、abc2、abc3顺序打印由线程池调度决定多次运行不会重复创建大量线程全部复用池内已有线程对比原生new Thread原生每次新建线程线程池全程复用线程三、知识点二ManualResetEvent 手动事件等待重点修正原代码歧义注释原注释表述模糊重新通俗定义 ManualResetEvent 线程信号控制器依靠内部布尔信号控制线程阻塞、放行替代.NET废弃的Suspend挂起、Resume恢复API是官方推荐线程等待方案3.1 构造函数参数ManualResetEvent eve new ManualResetEvent(false);传入false初始状态【无信号】调用WaitOne()会阻塞线程传入true初始状态【有信号】调用WaitOne()不会阻塞直接放行3.2 三大核心方法WaitOne()阻塞当前执行线程一直等待直到收到信号才放行Set()发送信号→把内部状态改为true放行所有阻塞线程Reset()清空信号→把内部状态改为false恢复阻塞状态3.3 button1案例逐行拆分全量注释private void button1_Click(object sender, EventArgs e) { //1.初始化事件控制器初始无信号默认阻塞 ManualResetEvent eve new ManualResetEvent(false); //2.向线程池提交异步任务 ThreadPool.QueueUserWorkItem((s) { Thread.Sleep(1000); //子线程休眠1秒模拟耗时业务 Console.WriteLine(s); //打印传入参数 eve.Set(); //发送放行信号内部状态置为true解除主线程阻塞 //eve.Reset(); //重置信号状态改为false后续再次调用WaitOne依旧阻塞 }, 罗志祥); //3.主线程阻塞等待子线程发送信号 //主线程卡在这一行代码不往下执行直到收到Set()信号 eve.WaitOne(); //4.收到信号放行执行后续代码 Console.WriteLine(AAAAAAAAAAAAAAAAAA); }3.4 完整执行时序必考点击按钮初始化eve→无信号状态提交线程池任务子线程开始休眠1s主线程执行eve.WaitOne()立刻阻塞暂停向下执行1秒后子线程休眠结束打印字符串执行eve.Set()发送信号主线程接收信号解除阻塞打印AAAAAAAAAAAAAAAAAA3.5 两组对照实验吃透信号机制实验1构造参数改为trueManualResetEvent eve new ManualResetEvent(true);现象主线程不会阻塞直接打印AAAAAAAAAAAAAAAAAA不需要等待子线程执行完毕实验2执行Set()后调用Reset()eve.Set(); eve.Reset();现象信号放行后立刻重置若二次调用WaitOne会再次阻塞线程四、拓展辨析ManualResetEvent 自动/手动区别ManualResetEvent 手动事件Set放行之后信号永久保留必须手动调用Reset才能重新阻塞AutoResetEvent 自动事件放行之后自动重置信号执行完立刻恢复阻塞状态五、整合可直接运行完整版代码using System; using System.Text; using System.Threading; using System.Windows.Forms; namespace _2线程池和事件等待 { public partial class Form1 : Form { public Form1() { InitializeComponent(); #region 线程池基础案例 //线程池复用空闲线程执行简单任务 ThreadPool.QueueUserWorkItem(s { Console.WriteLine(s); }, abc1); ThreadPool.QueueUserWorkItem(s { Console.WriteLine(s); }, abc2); ThreadPool.QueueUserWorkItem(s { Console.WriteLine(s); }, abc3); #endregion } private void button1_Click(object sender, EventArgs e) { //初始化事件控制器无信号、默认阻塞 ManualResetEvent eve new ManualResetEvent(false); //线程池执行耗时任务 ThreadPool.QueueUserWorkItem((s) { Thread.Sleep(1000); Console.WriteLine(s); //发送放行信号 eve.Set(); }, 罗志祥); //阻塞主线程等待信号 eve.WaitOne(); //放行执行 Console.WriteLine(AAAAAAAAAAAAAAAAAA); } } }六、易错点面试背诵总结6.1 线程池高频考点线程池核心线程复用减少创建销毁开销优化性能缺点无法获取线程实例不能修改优先级、线程名称提交任务固定方法ThreadPool.QueueUserWorkItem6.2 事件等待高频考点new ManualResetEvent(false)初始阻塞true初始放行WaitOne()阻塞当前线程Set()放行Reset()重置阻塞作用替代废弃Suspend、Resume实现安全线程等待执行顺序阻塞等待→收到信号→继续执行进度条暂停恢复实战案例笔记承接承接上篇线程池、ManualResetEvent笔记本次使用轻量化信号类ManualResetEventSlim实现进度条【启动-暂停-恢复-终止】完整功能封装全局线程、线程取消标识解决UI卡死、线程销毁泄露问题修复原生代码逻辑bug窗体控件ProgressBar进度条 Button1(启动)、Button2(暂停)、Button3(恢复)、Button4(终止重置)二、全局变量释义重中之重全部放在窗体类内部全局声明统一管控线程状态逐行拆解作用namespace _3例子 { public partial class Form1 : Form { //1.轻量化手动事件信号控制线程【阻塞/放行】 //区别相比于上篇ManualResetEventSlim轻量化、占用内存更低性能更高本地线程优先使用 ManualResetEventSlim pauseEvent; //2.全局线程对象全局保存进度条工作线程方便随时暂停、终止、重置 Thread t null; //3.线程取消标识软停止标记安全终止线程替代暴力终止 bool isCancel false; public Form1() { InitializeComponent(); //初始化按钮状态默认仅启动按钮可用其余按钮置灰 button2.Enabled false; //暂停 button3.Enabled false; //恢复 button4.Enabled false; //终止 //实例化进度条工作线程绑定业务方法 t new Thread(updateProgressBar); //✅初始化信号true默认放行、非阻塞false默认阻塞 pauseEvent new ManualResetEventSlim(true); //三个核心信号方法和ManualResetEvent用法完全一致 // pauseEvent.Wait(); 根据信号阻塞线程 // pauseEvent.Set(); 信号置true → 放行线程 // pauseEvent.Reset(); 信号置false → 阻塞线程 }三、核心业务方法进度条刷新方法修复原代码逻辑bug原代码入参a循环赋值存在数据错乱补齐逐行批注、梳理循环判定条件、跨线程UI更新逻辑/// summary /// 子线程执行方法循环刷新进度条 /// 入参a进度条初始值 /// /summary public void updateProgressBar(object a) { //循环终止三大条件 //1.isCanceltrue点击终止按钮退出循环 //2.tnull线程被销毁退出循环 //3.进度大于等于100进度加载完毕退出循环 while (!isCancel t ! null Convert.ToInt32(a) 100) { //✅线程阻塞关键点读取信号状态 //信号true直接放行代码继续执行 //信号false卡住当前线程进度暂停 pauseEvent.Wait(); Thread.Sleep(100); //模拟业务耗时放慢进度条速度 a Convert.ToInt32(a) 1; //进度自增 //进度合法更新UI if (Convert.ToInt32(a) 100) { //跨线程更新UI子线程禁止直接操作控件Invoke切回主线程 Invoke(new Action(() { progressBar1.Value Convert.ToInt32(a); })); } else { //进度溢出标记取消线程 isCancel true; } } }四、四大按钮点击事件拆分4.1 按钮1启动进度条private void button1_Click(object sender, EventArgs e) { pauseEvent.Set();//强制放行线程信号防止线程卡死 t.Start(0); //启动子线程传入初始进度0 //按钮状态联动互斥置灰防止重复点击报错 button1.Enabled false; button2.Enabled true; button4.Enabled true; }4.2 按钮2暂停进度private void button2_Click(object sender, EventArgs e) { pauseEvent.Reset();//信号置false → 触发pauseEvent.Wait()阻塞线程暂停 //切换按钮状态禁用暂停、启用恢复 button3.Enabled true; button2.Enabled false; }4.3 按钮3恢复进度private void button3_Click(object sender, EventArgs e) { pauseEvent.Set();//信号置true → 解除阻塞线程继续运行 //切换按钮状态 button3.Enabled false; button2.Enabled true; button4.Enabled true; }4.4 按钮4强制终止重置核心难点终止线程、复位全部状态、重建线程支持二次启动private void button4_Click(object sender, EventArgs e) { isCancel true; //开启取消标记退出循环 t?.Abort(); //安全判断线程不为空暴力终止线程 //重置全部按钮状态 button4.Enabled false; button1.Enabled true; button2.Enabled false; button3.Enabled false; //复原所有全局状态实现重启进度条 pauseEvent.Set();//放行阻塞线程防止线程残留卡死 progressBar1.Value 0; //进度条归零 t new Thread(updateProgressBar); //重新实例化工作线程 isCancel false;//清空取消标识 }五、窗体关闭事件释放资源防止内存泄漏重点考点窗体关闭必须销毁线程、释放信号资源避免后台线程常驻内存、程序闪退private void Form1_FormClosing(object sender, FormClosingEventArgs e) { isCancel true; //标记线程取消 pauseEvent.Set();//解除线程阻塞避免线程死锁 t.Abort();//强制销毁工作线程 pauseEvent.Dispose();//释放信号控制器托管资源杜绝内存泄漏 } } }六、核心知识点辨析6.1 ManualResetEvent VS ManualResetEventSlimManualResetEvent重量级、内核级事件跨进程、跨线程适用ManualResetEventSlim轻量化事件占用资源极小仅限本地线程使用普通线程控制首选课堂、项目最常用两者 Set()、Reset()、Wait() 用法完全一模一样无缝替换6.2 进度暂停核心原理调用Reset()→信号false子线程执行到 pauseEvent.Wait() 触发阻塞调用Set()→信号true解除阻塞进度继续执行6.3 按钮互斥作用禁用按钮防止用户乱点比如进度暂停时重复点击暂停、启动阶段重复启动线程引发程序报错、线程错乱七、代码原生bug修复原代码隐患bug1未做空值判断直接执行t.Abort()极端情况报空指针异常优化为t?.Abort()bug2窗体关闭不释放Dispose长期运行造成内存泄漏bug3进度跑完不重置线程无法二次重启进度条八、执行流程总梳理背诵版窗体初始化初始化信号、全局线程、置灰按钮点击启动放行信号、开启线程进度条自增刷新点击暂停信号置false线程阻塞进度停止点击恢复信号置true解除阻塞进度继续点击终止销毁线程、复原全部状态进度归零关闭窗体销毁资源防止内存泄露