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)
        {

        }
    }
}

二、知识点一:线程池 ThreadPool

2.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 运行现象&踩坑

  1. 控制台输出顺序随机,不一定按abc1、abc2、abc3顺序打印,由线程池调度决定

  2. 多次运行不会重复创建大量线程,全部复用池内已有线程

  3. 对比原生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 完整执行时序(必考)

  1. 点击按钮,初始化eve→无信号状态

  2. 提交线程池任务,子线程开始休眠1s

  3. 主线程执行 eve.WaitOne(),立刻阻塞,暂停向下执行

  4. 1秒后子线程休眠结束,打印字符串,执行eve.Set()发送信号

  5. 主线程接收信号,解除阻塞,打印AAAAAAAAAAAAAAAAAA

3.5 两组对照实验,吃透信号机制

实验1:构造参数改为true
复制代码
ManualResetEvent 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 线程池高频考点

  1. 线程池核心:线程复用,减少创建销毁开销,优化性能

  2. 缺点:无法获取线程实例,不能修改优先级、线程名称

  3. 提交任务固定方法:ThreadPool.QueueUserWorkItem

6.2 事件等待高频考点

  1. new ManualResetEvent(false):初始阻塞;true:初始放行

  2. WaitOne():阻塞当前线程;Set():放行;Reset():重置阻塞

  3. 作用:替代废弃Suspend、Resume,实现安全线程等待

  4. 执行顺序:阻塞等待→收到信号→继续执行

进度条暂停恢复实战案例

笔记承接 :承接上篇线程池、ManualResetEvent笔记,本次使用轻量化信号类 ManualResetEventSlim;实现进度条【启动-暂停-恢复-终止】完整功能,封装全局线程、线程取消标识,解决UI卡死、线程销毁泄露问题,修复原生代码逻辑bug

窗体控件:ProgressBar进度条 + Button1(启动)、Button2(暂停)、Button3(恢复)、Button4(终止重置)

二、全局变量释义(重中之重)

全部放在窗体类内部全局声明,统一管控线程状态,逐行拆解作用

复制代码
namespace _3例子
{
    public partial class Form1 : Form
    {
        //1.轻量化手动事件信号:控制线程【阻塞/放行】
        //区别:相比于上篇ManualResetEvent,Slim轻量化、占用内存更低,性能更高,本地线程优先使用
        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.isCancel==true:点击终止按钮,退出循环
            //2.t==null:线程被销毁,退出循环
            //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 ManualResetEventSlim

  • ManualResetEvent:重量级、内核级事件,跨进程、跨线程适用

  • ManualResetEventSlim:轻量化事件,占用资源极小,仅限本地线程使用,普通线程控制首选,课堂、项目最常用

  • 两者 Set()、Reset()、Wait() 用法完全一模一样,无缝替换

6.2 进度暂停核心原理

  1. 调用Reset()→信号=false

  2. 子线程执行到 pauseEvent.Wait() 触发阻塞

  3. 调用Set()→信号=true,解除阻塞,进度继续执行

6.3 按钮互斥作用

禁用按钮,防止用户乱点:比如进度暂停时重复点击暂停、启动阶段重复启动线程,引发程序报错、线程错乱

七、代码原生bug修复(原代码隐患)

  • bug1:未做空值判断,直接执行t.Abort(),极端情况报空指针异常,优化为 t?.Abort()

  • bug2:窗体关闭不释放Dispose,长期运行造成内存泄漏

  • bug3:进度跑完不重置线程,无法二次重启进度条

八、执行流程总梳理(背诵版)

  1. 窗体初始化:初始化信号、全局线程、置灰按钮

  2. 点击启动:放行信号、开启线程,进度条自增刷新

  3. 点击暂停:信号置false,线程阻塞,进度停止

  4. 点击恢复:信号置true,解除阻塞,进度继续

  5. 点击终止:销毁线程、复原全部状态,进度归零

  6. 关闭窗体:销毁资源,防止内存泄露

相关推荐
石山代码1 小时前
C++23 新特性在 CLion 中的实战体验
开发语言·c++·c++23
浮江雾1 小时前
Flutter第四节------核心概念学习笔记:从基础语法到异步编程
linux·服务器·开发语言·笔记·flutter·入门·基础
北冥you鱼1 小时前
Go Modules 使用指南:从入门到精通
开发语言·后端·golang
l1t1 小时前
duckdb 1.6dev新增的.manual命令
开发语言·数据库
你怎么知道我是队长2 小时前
JavaScript的函数介绍
开发语言·前端·javascript
YMWM_2 小时前
python-venv虚拟环境
linux·开发语言·python
不负岁月无痕2 小时前
STL -- C++ 红黑树封装 Map 和 Set
java·c语言·开发语言·c++·面试
小园子的小菜2 小时前
Java核心基础:反射与泛型底层原理及实战用法详解
java·开发语言
十五年专注C++开发2 小时前
C++之std::numeric_limits介绍
开发语言·c++·limits