并发编程 ---为何要线程池化 不念不忘少年蓝@ 2024-03-23 19:17 50阅读 0赞 ### 引言 ### 众所周知,使用线程可以极大的提高应用程序的效率和响应性,提高用户体验,但是不可以无节制的使用线程,为什么呢? ### 线程的开销 ### 线程的开销实际上是非常大的,我们从空间开销和时间开销上分别讨论。 #### 线程的空间开销 #### 线程的空间开销来自这四个部分: 1. 线程内核对象(Thread Kernel Object)。每个线程都会创建一个这样的对象,它主要包含线程上下文信息,在32位系统中,它所占用的内存在700字节左右。 2. 线程环境块(Thread Environment Block)。TEB包括线程的异常处理链,32位系统中占用4KB内存。 3. 用户模式栈(User Mode Stack),即线程栈。线程栈用于保存方法的参数、局部变量和返回值。每个线程栈占用1024KB的内存。要用完这些内存很简单,写一个不能结束的递归方法,让方法参数和返回值不停地消耗内存,很快就会发生 `OutOfMemoryException` 。 4. 内核模式栈(Kernel Mode Stack)。当调用操作系统的内核模式函数时,系统会将函数参数从用户模式栈复制到内核模式栈。在32位系统中,内核模式栈会占用12KB内存。 #### 线程的时间开销 #### 线程的时间开销来自这三个过程: 1. 线程创建的时候,系统相继初始化以上这些内存空间。 2. 接着CLR会调用所有加载DLL的DLLMain方法,并传递连接标志(线程终止的时候,也会调用DLL的DLLMain方法,并传递分离标志)。 3. 线程上下文切换。一个系统中会加载很多的进程,而一个进程又包含若干个线程。但是一个CPU内核在任何时候都只能有一个线程在执行。为了让每个线程看上去都在运行,系统会不断地切换“线程上下文”:每个线程及其短暂的执行时间片,然后就会切换到下一个线程了。 这个线程上下文切换过程大概又分为以下5个步骤: * 步骤1进入内核模式。 * 步骤2将上下文信息(主要是一些CPU寄存器信息)保存到正在执行的线程内核对象上。 * 步骤3系统获取一个 `Spinlock` ,并确定下一个要执行的线程,然后释放 `Spinlock` 。如果下一个线程不在同一个进程内,则需要进行虚拟地址交换。 * 步骤4从将被执行的线程内核对象上载入上下文信息。 * 步骤5离开内核模式。 所以,由于要进行如此多的工作,所以创建和销毁一个线程就意味着代价“昂贵”,即使现在的CPU多核多线程,如无节制的使用线程,依旧会严重影响性能。 ### 引入线程池 ### 为了免程序员无节制地使用线程,微软开发了“线程池”技术。简单来说,线程池就是替开发人员管理工作线程。当一项工作完毕时,CLR不会销毁这个线程,而是会保留这个线程一段时间,看是否有别的工作需要这个线程。至于何时销毁或新起线程,由CLR根据自身的算法来做这个决定。 线程池技术能让我们重点关注业务的实现,而不是线程的性能测试。 微软除实现了线程池外,还需要关注一个类型:`BackgroundWorker`。 `BackgroundWorker` 是在内部使用了线程池的技术:同时,在WinForm或WPF编码中,它还给工作线程和UI线程提供了交互的能力。 实际上, `Thread` 和 `ThreadPool` 默认都没有提供这种交互能力,而 `BackgroundWorker` 却通过事件提供了这种能力。这种能力包括:报告进度、支持完成回调、取消任务、暂停任务等。 `BackgroundWorker` 的简单示例如下: private BackgroundWorker backgroundWorker = new BackgroundWorker(); private void AsyncButton_Click(object sender, RoutedEventArgs e) { //注册要执行的任务 backgroundWorker.DoWork += BackgroundWorker_DoWork; //注册报告进度 backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged; //注册完成时的回调 backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted; //设置允许任务取消 backgroundWorker.WorkerSupportsCancellation = true; //设置允许报告进度 backgroundWorker.WorkerReportsProgress = true; backgroundWorker.RunWorkerAsync(); } private void Cancel_Click(object sender, RoutedEventArgs e) { //取消任务 if (backgroundWorker.IsBusy) backgroundWorker.CancelAsync(); } private void BackgroundWorker_RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs e) { //完成时回调 MessageBox.Show("BackgroundWorker RunWorkerCompleted"); } private void BackgroundWorker_ProgressChanged(object? sender, ProgressChangedEventArgs e) { //报告进度 this.textbox.Text = e.ProgressPercentage.ToString(); } private void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e) { BackgroundWorker? worker = sender as BackgroundWorker; if (worker != null) { for (int i = 0; i < 20; i++) { if (worker.CancellationPending) { e.Cancel = true; break; } worker.ReportProgress(i); Thread.Sleep(100); } } } 建议使用WinForm和WPF的开发人员使用 `BackgroundWorker`。 ### Task替代ThreadPool ### `ThreadPool` 相对于 `Thread` 来说具有很多优势,但是 `ThreadPool` 在使用上却存在一定的不方便。比如: * `ThreadPool` 不支持线程的取消、完成、失败通知等交互性操作。 * `ThreadPool` 不支持线程执行的先后次序。 所以随着 `Task` 类及其所提供的异步编程模型的引入,`Task`相较`ThreadPool`具有更多的优势。大概有一下几点: 1. Task是.NET Framework的一部分,它提供了更高级别的抽象来表示异步操作或并发任务。相比之下,ThreadPool较为底层,需要手动管理线程池和任务队列。通过使用Task,我们可以以更简洁、更可读的方式表达并发逻辑,而无需关注底层线程管理的细节。 2. Task是基于Task Parallel Library(TPL)构建的核心组件,它提供了强大的异步编程支持。利用Task,我们能够轻松定义异步方法、等待异步操作完成以及处理任务结果。与此相反,ThreadPool主要用于执行委托或操作,缺乏直接的异步编程功能。 3. Task在底层使用ThreadPool来执行任务,但它提供了更优秀的性能和资源管理机制。通过使用Task,我们可以利用TPL提供的任务调度器,智能化地管理线程池的大小、工作窃取算法和任务优先级。这样一来,我们能够更有效地利用系统资源,并获得更好的性能表现。 4. Task拥有强大的任务关联和组合功能。我们可以使用Task的 `ContinueWith()` 、 `When()` 、`WhenAll()` 、`Wait()`等方法定义任务之间的依赖关系,以及在不同任务完成后执行的操作。这种任务组合方式使并发编程更加灵活且易于管理。 5. Task提供了更好的异常处理和取消支持机制。我们可以利用Task的异常处理机制捕获和处理任务中的异常,而不会导致整个应用程序崩溃。此外,Task还引入 `CancellationToken` 的概念,可用于取消任务的执行,从而更好地控制并发操作。 所以,尽管ThreadPool在某些情况下仍然有其用途,但在C\#编程中,使用Task替代ThreadPool已变为通用实践,推荐优先考虑使用Task来处理并发任务。
相关 Java并发编程困惑:线程池为何会出现死锁 线程池出现死锁并不是直接由线程池机制引起的,而是与其他并发问题相关。 死锁通常发生在以下情况: 1. **资源竞争**:多个线程同时申请并使用相同的资源(如锁),但没有按照 矫情吗;*/ 2024年10月27日 19:06/ 0 赞/ 50 阅读
相关 Java并发编程:线程池为何常出现问题 线程池在Java并发编程中被广泛使用,其优点包括资源复用、响应式任务处理等。然而,线程池的问题也是存在的,主要表现在以下几个方面: 1. **线程池大小设置不当**:如果线程 水深无声/ 2024年09月26日 23:57/ 0 赞/ 42 阅读
相关 Java并发编程:线程池为何有时效率低? 线程池在并发处理中是一种常见的资源管理模式,它有以下优点: 1. 线程复用:多个任务可以共享同一个线程池中的线程,避免频繁创建和销毁线程。 2. 提高性能:通过预设的最大线 向右看齐/ 2024年09月25日 09:03/ 0 赞/ 41 阅读
相关 Java并发编程:理解线程池为何稳定高效 线程池是Java并发编程中的一种重要工具,它能够有效地管理和复用线程资源,从而实现系统的稳定和高效。 1. **资源管理**:线程池预先创建了固定数量的线程,这些线程在需要时 冷不防/ 2024年09月17日 20:54/ 0 赞/ 48 阅读
相关 Java并发编程:线程池为何容易出现性能问题 线程池在Java并发编程中是一种常用的资源管理方式。然而,如果使用不当,线程池确实可能引发性能问题。以下是可能出现的问题: 1. **过度配置**:如果你创建了一个非常大的线 野性酷女/ 2024年09月11日 10:54/ 0 赞/ 47 阅读
相关 并发编程 ---为何要线程池化 引言 众所周知,使用线程可以极大的提高应用程序的效率和响应性,提高用户体验,但是不可以无节制的使用线程,为什么呢? 线程的开销 线程的开销实际上是非常大的,我们从 不念不忘少年蓝@/ 2024年03月23日 19:17/ 0 赞/ 51 阅读
相关 并发编程(14)-线程池 1.Executor 为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效的进行线程控制。他们都在java.util.concurrent包中 小鱼儿/ 2022年06月05日 03:06/ 0 赞/ 318 阅读
相关 并发编程之线程池 为什么使用线程池 有时候,系统需要处理非常多的执行时间很短的请求,如果每一个请求都开启一个新线程的话,系统就要不断的进行线程的创建和销毁,有时花在创建和销毁线程上的时间会 ゞ 浴缸里的玫瑰/ 2022年05月15日 12:51/ 0 赞/ 309 阅读
相关 并发编程-线程池 newfixedthreadpool(固定线程数量的线程池) 可控制线程最大并发数,超出的线程会在队列中等待 ExecutorService newFixedT 小咪咪/ 2022年02月22日 13:14/ 0 赞/ 361 阅读
还没有评论,来说两句吧...