08-C#

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 对比

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

六、注意事项与最佳实践

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值