08-C#.Net-Thread-学习笔记

一、多线程基础概念

多线程的三大特点:

  • 异步执行:不阻塞主线程,多件事可以同时进行
  • 效率高:充分利用 CPU 等计算机资源
  • 无序性:多个线程的执行顺序不可预测,无法控制

⚠️ 正因为无序性,多线程调试困难,通常只能通过写日志、输出结果、结合线程 ID 来分析问题。

// 获取当前线程 ID,用于区分不同线程
int id = Thread.CurrentThread.ManagedThreadId;

二、Thread 类

基本概念

  • 来源System.Threading 命名空间
  • 出现时间:.NET Framework 1.0
  • 类型:密封类(sealed class),不可被继承
  • 作用:操作计算机线程资源的帮助类库

开启线程

方式一:ThreadStart(无参数)

ThreadStart threadStart = new ThreadStart(() =>
{
    DoSomething("Richard");
});
Thread thread = new Thread(threadStart);
thread.Start();

方式二:ParameterizedThreadStart(带参数)

ParameterizedThreadStart pts = new ParameterizedThreadStart(obj =>
{
    Console.WriteLine($"参数: {obj}");
});
Thread thread = new Thread(pts);
thread.Start("传递的参数");

常用 API

线程控制

thread.Suspend();       // 暂停线程(不会立即停止)
thread.Resume();        // 恢复执行
thread.Abort();         // 停止线程(向线程抛出异常)
Thread.ResetAbort();    // 取消停止,让线程继续执行

SuspendResumeAbort 均已标记为过时(Obsolete),实际开发中不应使用。线程无法真正从外部被强制终止,这些方法只是通过异常机制间接干预,存在安全隐患。

线程等待

方式一:观望式轮询(不推荐)

while (thread.ThreadState != ThreadState.Stopped)
{
    Thread.Sleep(500); // 每 500ms 检查一次
}

❌ 持续轮询会浪费 CPU 时间片,不推荐。

方式二:Join 等待(推荐)

thread.Join();                              // 无限等待,直到线程结束
thread.Join(500);                           // 最多等待 500ms,过时不候
thread.Join(TimeSpan.FromMilliseconds(500)); // 同上,TimeSpan 写法

Join() 是最简洁直接的线程等待方式。

前台线程 vs 后台线程

thread.IsBackground = true;  // 后台线程:程序关闭时,线程立即终止
thread.IsBackground = false; // 前台线程:程序关闭时,等待线程执行完毕再退出
  • 默认:通过 new Thread() 创建的线程是前台线程
  • ThreadPool 中的线程全部是后台线程
  • 主线程(Main 方法所在线程)是前台线程

⚠️ 如果忘记将长时间运行的线程设为后台线程,可能导致程序关闭后进程仍然存活。

线程优先级

thread.Priority = ThreadPriority.Highest; // 最高优先级
thread.Priority = ThreadPriority.Lowest;  // 最低优先级

⚠️ 设置优先级只是提高被优先调度的概率,操作系统不保证严格按优先级执行,不能依赖它来控制执行顺序。


三、Thread 扩展封装

问题背景

多线程天然无序,但有时需要:

  1. 两个操作都在新线程中执行
  2. 同时保证它们的执行顺序

方案一:顺序执行两个委托

把两个委托放进同一个线程,天然保证顺序:

private void CallBackThread(ThreadStart threadStart, Action action)
{
    Thread thread = new Thread(() =>
    {
        threadStart.Invoke(); // 先执行
        action.Invoke();      // 后执行
    });
    thread.Start();
}

方案二:带返回值的异步执行

核心思路:立即开启新线程执行,返回一个"取结果的委托",调用方在需要结果时才触发等待。

private Func<T> CallBackFunc<T>(Func<T> func)
{
    T result = default;

    Thread thread = new Thread(() =>
    {
        result = func.Invoke(); // 新线程中执行
    });
    thread.Start(); // 立即开启,不阻塞

    // 返回一个延迟取值的委托
    return new Func<T>(() =>
    {
        thread.Join(); // 调用时才等待线程完成
        return result;
    });
}

使用示例:

Func<int> asyncFunc = CallBackFunc(() =>
{
    Thread.Sleep(5000);
    return DateTime.Now.Year;
});

// 这里不阻塞,可以继续做其他事
Console.WriteLine("继续执行其他任务...");

// 需要结果时才等待
int result = asyncFunc.Invoke();

⚠️ .NET CoreDelegate.BeginInvoke / EndInvoke 已不再支持,不能用于异步执行委托,需要用上述方式或 Task 替代。


四、ThreadPool 线程池

为什么需要线程池

Thread 的问题:

  • API 繁多,控制复杂
  • 线程数量需要程序员手动管理,容易滥用
  • 频繁创建/销毁线程,系统开销大

ThreadPool 的优势:

  • .NET Framework 2.0 引入,采用池化思想
  • 预先创建线程放入池中,需要时直接取用,用完自动归还
  • 自动管理线程数量,防止资源耗尽
  • API 更简洁,去掉了 Thread 中不必要的方法

申请线程执行任务

ThreadPool.QueueUserWorkItem(obj =>
{
    DoSomething(obj.ToString());
}, "参数");

线程等待:ManualResetEvent

ThreadPool 没有 Join(),需要用 ManualResetEvent 实现等待。

ManualResetEvent resetEvent = new ManualResetEvent(false); // 初始为未触发

ThreadPool.QueueUserWorkItem(obj =>
{
    DoSomething(obj.ToString());
    resetEvent.Set(); // 任务完成,发出信号
}, "Richard");

// 主线程可以继续做其他事
Console.WriteLine("主线程继续执行...");

resetEvent.WaitOne(); // 阻塞,直到 Set() 被调用
Console.WriteLine("子线程已完成");

工作原理:

  1. new ManualResetEvent(false) — 初始状态为"未触发"
  2. 子线程任务完成后调用 Set() — 状态变为"已触发"
  3. 主线程调用 WaitOne() — 阻塞等待,直到状态变为"已触发"才继续

控制线程数量

ThreadPool.SetMinThreads(4, 4); // 设置最小工作线程数和 IO 线程数
ThreadPool.SetMaxThreads(8, 8); // 设置最大工作线程数和 IO 线程数

ThreadPool.GetMinThreads(out int minWorker, out int minIO);
ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
  • 第一个参数:工作线程数(Worker Threads),用于 CPU 密集型任务
  • 第二个参数:IO 线程数(Completion Port Threads),用于 IO 操作

强烈不建议手动设置线程池大小。原因:

  • 这是进程级别的全局设置
  • TaskParallelasync/await 底层都使用线程池,修改后会影响所有这些功能
  • 设置不当容易引发性能问题甚至死锁

五、Thread vs ThreadPool 对比

特性 Thread ThreadPool
出现时间 .NET 1.0 .NET 2.0
线程管理 手动创建和销毁 自动管理,复用线程
API 复杂度 复杂,功能多 简单,易用
性能 频繁创建销毁,开销大 复用线程,性能好
线程等待 Join() ManualResetEvent
默认线程类型 前台线程 后台线程
适用场景 需要精细控制的长时间任务 大量短时间并发任务

六、注意事项与最佳实践

✅ 短时间任务优先使用 ThreadPool
✅ 使用 Join() 进行 Thread 的线程等待
✅ 使用 ManualResetEvent 进行 ThreadPool 的线程同步
✅ 长时间运行的线程记得设置 IsBackground = true
❌ 避免使用 Abort()Suspend()Resume()
❌ 不要手动设置线程池大小
❌ 不要在生产环境依赖线程优先级来控制执行顺序

⚠️ 多个线程访问共享资源时需要加锁(lockMonitor 等),否则会出现数据竞争问题。
⚠️ 线程内部的异常不会自动传播到主线程,必须在线程内部自行处理。
⚠️ 在实际项目中,更推荐使用 Taskasync/await,它们是对线程池的更高层封装,代码更简洁,异常处理和取消机制也更完善。

posted @ 2026-03-19 17:19  龙猫•ᴥ•  阅读(1)  评论(0)    收藏  举报