C# 中的并发背后的秘密

介绍

一般应用程序不直接使用 System.Threading 中的 Thread 类。如果您在某个项目中看到它,则表明您正在维护遗留的代码。众所周知,.NET 1.0 引入了 Thread API,但使用它并不那么容易。最好不要使用它,为什么呢?

1.Thread API 是底层 API,可让您直接管理一切。这个选项听起来不错,但对不熟悉的东西进行更多控制可能会最终令人沮丧。

2.掌握 Thread API 并非易事。它有很多背景历史,需要你深入研究操作系统的细节才能完全理解。嗯,听起来有点吓人 )

3.Thread API 迫使您从线程而不是任务的角度来思考。作为开发人员,您应该考虑如何让代码并发,而不是考虑如何创建线程。线程是一个物理概念,是一种实现细节。

4.线程创建是一项昂贵的操作。

5.从可理解性和可维护性的角度来看,使用线程 API 会增加额外的复杂性。

在 .NET 2.0 中,微软引入了比 Thread API 更好的 ThreadPool 类。主要改进是将我们从 Thread 概念中分离出来。您不需要从线程的角度来思考。作为开发人员,您应该从要并行化的任务的角度来思考。

ThreadPool 是线程的包装器。

为什么它是一个更好的选择?

1.通过 ThreadPool,你不再是线程的“所有者”。无需手动创建,无需复杂操作,也无需担心线程管理、死锁等问题。猜猜怎么着?最大的问题不是线程,而是你 😃

2.ThreadPool 管理何时以及为何创建线程。简单来说,假设我们有 5 个方法。您想以多线程模式运行它们。您需要 5 个方法来运行它们,但 ThreadPool 可能会使用 2 或 3 个线程来完成您的任务。限制取决于您在传统线程 API 中使用的线程数。

internal class Program
{
    static void Main(string[] args)
    {
        new Thread(ComplexTask).Start();
        Thread.Sleep(1000);
        new Thread(ComplexTask).Start();
        Thread.Sleep(1000);
        new Thread(ComplexTask).Start();
        Thread.Sleep(1000);
        new Thread(ComplexTask).Start();
        Thread.Sleep(1000);
        Console.ReadLine();
    }

    static void ComplexTask()
    {
        Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
        //Thread.Sleep(40);
        Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
    }
}

结果如下

每个操作都是一个线程,这不是一个优化的使用方式。

ThreadPool 将我们从 Thread 中分离出来,并提供了一个简单的 API 来进入多线程模式。让我们修改代码,并尝试理解直接使用 ThreadPool 而不是 Thread 的价值。

internal class Program
{
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);

        Console.ReadLine();
    }
    static void ComplexTask()
    {
        Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
        //Thread.Sleep(40);
        Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
    }
}

正如您所意识到的,ThreadPool 使用 Thread,但方式经过了优化。如果任何现有线程空闲,ThreadPool 将使用已存在的线程,而不是创建新线程。等一下…我们是否可以执行相同的操作?我的意思是,我们可以运行同一个线程两次吗?让我们试试。

您无法两次运行同一个线程,但 ThreadPool 却可以。ThreadPool 可以重用线程,当您要在多线程模式下运行代码块时,它有助于避免每次都创建线程。在经典 .NET 中,根据其版本和您的 CPU 架构,工作线程的容量会有所不同。

就我而言,我使用的是带有 x64 英特尔处理器的最新 .NET(.NET 8),并且拥有 32767 个工作线程和 1000 个完成后线程。

ThreadPool 通常以池中的 1 个线程开始。根据上下文,它可能会自动增加线程。您可以检查 ThreadPool 中的可用线程。

internal class Program
{
    static void Main(string[] args)
    {
        ThreadCalculator();
        for (int i = 0; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        }
        Thread.Sleep(15);
        ThreadCalculator();
        Console.ReadLine();
    }
    static void ThreadCalculator()
    {
        ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
        Console.WriteLine($"worker threads = {workerThreads}, " +
            $"and completion Port Threads = {completionPortThreads}");
    }
    static void ComplexTask()
    {
        Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
        //Thread.Sleep(40);
        Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
    }
}


ThreadPool 中最常用的 API 当然是 QueueUserWorkItem。
我们需要介绍一下。ThreadPool.QueueUserWorkItem 是 C# 中的一种方法,它允许您安排任务在线程池中执行。以下是其功能的细分:

目的

  • 安排任务异步运行,无需创建和管理单独的线程。
  • 利用预先创建的线程池,提高性能和资源管理。

怎么运行的

提供一个代表您想要完成的工作的委托(例如 Action 或 WaitCallback)。或者,您可以传递一个包含委托要使用的数据的对象。ThreadPool.QueueUserWorkItem 将委托和数据(如果提供)添加到线程池的队列。
当池中的线程可用时,它会从队列中获取第一个项目并使用提供的数据执行委托。

好处或使用原因

  • **性能提升:**重用线程避免了为每个任务创建和销毁线程的开销。
  • **资源优化:**维持受控的线程数,防止系统过载。
  • **简化的并发管理:**线程池处理调度并确保任务高效执行。

补充笔记

  • 线程池可以根据工作负载动态调整线程数量。

  • 如果所有线程都处于繁忙状态,则将任务排队,以确保不会丢弃任何任务。

  • ThreadPool.QueueUserWorkItem 具有重载,包括类型安全的通用版本和影响线程选择的选项。

  • 好的,但是完成端口线程怎么样?

C# 中的 ThreadPool 类中的 finishPortThreads 概念可能不会像您期望的那样直接公开。
ThreadPool 类内部管理两种类型的线程。

  • 工作线程:这些线程处理通过 QueueUserWorkItem 提交的通用任务。
  • **I/O 完成端口线程(Completion Port Threads):**这些是针对处理异步 I/O 操作而优化的专门线程。

完成端口线程的用途

异步 I/O 操作通常涉及等待网络请求、文件访问或其他外部事件。完成端口线程使用称为 I/O 完成端口 (IOCP) 的系统机制高效地等待这些事件。

当 I/O 操作完成时,相应的完成端口线程会收到通知,然后它可以出队并处理已完成的任务。

有什么价值?

  • 提高了异步 I/O 绑定任务的性能。
  • 完成端口线程避免忙等待,减少等待I/O事件时的CPU使用率。
  • 用于 I/O 操作的专用线程可防止工作线程被阻塞,从而提高整体响应能力。

重要笔记

您不能直接控制completionPortThreads。ThreadPool 类根据系统工作负载动态管理其数量。

ThreadPool.SetMinThreads 和 ThreadPool.GetAvailableThreads 等方法允许您间接影响工作线程和完成端口线程的最小数量,但是对每种类型没有单独的控制。

谁使用 C# 中的 ThreadPool?

ThreadPool 是 C# 中用于高效管理线程的基本机制。它提供了一个工作线程池,可供应用程序的各个部分重复使用,包括:

  • **任务并行库 (TPL):**当您创建 Task 或 Task 对象时,TPL 通常默认安排它们在 ThreadPool 线程上运行。这样便可以并行执行任务,而无需明确管理线程。
  • **异步编程:**使用 async 和 await 关键字等异步操作通常依赖于 ThreadPool 在后台执行实际工作,同时主线程保持响应。
  • **并行编程:**并行 For 循环 (Parallel.For) 和 PLINQ (Parallel LINQ) 等库通常利用 ThreadPool 在多个线程之间分配工作项。

使用线程池的好处

  • 减少线程创建开销:创建线程可能很昂贵。ThreadPool 通过预先创建线程池并根据需要重复使用它们来消除这种开销。
  • 提高性能:通过有效地管理线程,ThreadPool 可以增强应用程序的响应能力和吞吐量,尤其是在处理并发任务时。
  • 简化的线程管理: ThreadPool 处理线程的创建、销毁和空闲线程管理,使您摆脱这些复杂性。

何时考虑替代方案

  • **长时间运行的操作:**如果您的工作项需要长时间运行(例如,几秒或更长时间),则使用专用线程或带有自定义线程创建选项的 TaskFactory.StartNew 方法可能更合适,以避免线程池饱和并影响整体应用程序性能。
  • **专门的线程要求:**如果您的任务需要特定的线程优先,您可能需要创建具有所需设置的专用线程。

结论

ThreadPool 是 C# 中并发编程的宝贵工具。它提供了一种方便而有效的线程管理方法,尤其是对于短暂的、受 CPU 限制的任务。通过了解 ThreadPool 的工作原理及其适当的用例,您可以创建结构良好且性能良好的 C# 应用程序。

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锋.谢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值