C#异步多线程——ThreadPool线程池

C#实现异步多线程的方式有多种,以下总结的是ThreadPool的用法。

线程池的特点

线程池受CLR管理,线程的生命周期,任务调度等细节都不需要我们操心了,我们只需要专注于任务实现,使用ThreadPool提供的静态方法把我们的任务添加到任务队列中,剩下的请放心交给CLR。

  • 线程重用:和其它池化资源有着共同特点,当创建某个对象,创建和销毁代价太高得情况,而且这个对象又可以反复利用,往往我们可以准备一个容器,这个容器可以保存一批这样的对象,如果需要使用这个对象,不需要创建,可以去池中去拿,拿到就用,用完再放回池中,这样就减少了创建和销毁的开销,性能可以得到提升。
  • ThreadPool使用简单,没有直接操作线程的API,例如暂停,恢复,销毁线程,设置前台后台线程(默认都是后台线程),设置优先级等。
  • 有线程数限制,无法无限使用。

环境准备

Visual Studio 创建测试项目,我使用的是Windows Forms App(.NET Framework)模板创建。简单的添加几个测试按钮,在Click事件中进行简单测试。并且准备一段模拟非常耗时操作的代码,例如:

private void DoSomethingLong(string name)
{Console.WriteLine($"********** DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");long lResult = 0;//2000000000? 能看出明显延时就好for (int i = 0; i < 2000000000; i++){lResult += i;}Console.WriteLine($"********** DoSomethingLong   End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

认识下ThreadPool

两种常用启动方式

  • WaitCallback:本质是一个委托,没有返回值,有一个object类型的参数。如果任务不需要传入参数可以使用:public static bool QueueUserWorkItem(WaitCallback callBack)
  • 如果需要传入参数可以使用另外一个重载方法,传入的object类型参数会自动传入到WaitCallback委托中的object参数中:public static bool QueueUserWorkItem(WaitCallback callBack, object state)
private void BtnThreadPool_Click(object sender, EventArgs e)
{Console.WriteLine("");Console.WriteLine($"********** BtnThreadPool_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");WaitCallback waitCallbackWithoutParam = state => Console.WriteLine($"waitCallbackWithoutParam {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");ThreadPool.QueueUserWorkItem(waitCallbackWithoutParam);WaitCallback waitCallbackWithParam = state => Console.WriteLine($"waitCallbackWithParam state={state} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");ThreadPool.QueueUserWorkItem(waitCallbackWithParam, "ParameterizedThreadPool");Console.WriteLine($"********** BtnThreadPool_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

设置&获取线程数

  • SetMaxThreads:设置最大线程数:
  • SetMinThreads:设置最小线程数:
  • GetMaxThreads:获取最大线程数:
  • GetMinThreads:获取最小线程数:
private void BtnThreadPoolInfo_Click(object sender, EventArgs e)
{{//默认线程数ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);Console.WriteLine($"默认线程数:workerThreadsMax={workerThreadsMax} workerThreadsMin={workerThreadsMin}");}{//设置线程数ThreadPool.SetMaxThreads(16, 16);    //设置最大线程数ThreadPool.SetMinThreads(12, 12);    //设置最小线程数ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);Console.WriteLine($"设置线程数:workerThreadsMax={workerThreadsMax} workerThreadsMin={workerThreadsMin}");}
}

输出结果:
在这里插入图片描述
我用的电脑是6核12线程的,这个分配是不是很合理?所以CLR是很“聪明”的。如果没有什么极端的需求通常我们不需要自作聪明的去设置他们;

线程等待

ThreadPool没有提供等待线程完成的API,可以使用ManualResetEvent:

private void BtnThreadPoolWait_Click(object sender, EventArgs e)
{Console.WriteLine("");Console.WriteLine($"********** BtnThreadPoolWait_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");ManualResetEvent manualResetEvent = new ManualResetEvent(false);ThreadPool.QueueUserWorkItem(state =>{this.DoSomethingLong("BtnThreadPoolWait_Click");manualResetEvent.Set();});for (int i = 0; i < 1000000000; i++){}Console.WriteLine($"********** 主线程可以先干点别的 {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");manualResetEvent.WaitOne();Console.WriteLine($"********** BtnThreadPoolWait_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

为什么死锁了?

看下下面使用线程池发生死锁的例子:

private void BtnThreadPoolDeadLock_Click(object sender, EventArgs e)
{Console.WriteLine("");Console.WriteLine($"********** BtnThreadPoolDeadLock_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");ThreadPool.SetMaxThreads(16, 16);ManualResetEvent manualResetEvent = new ManualResetEvent(false);for (int i = 0; i < 20; i++){int k = i;ThreadPool.QueueUserWorkItem(state =>{Console.WriteLine($"k={k} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");if (k < 18){manualResetEvent.WaitOne();}else{manualResetEvent.Set();}});}if (manualResetEvent.WaitOne()){Console.WriteLine("没有死锁");}Console.WriteLine($"********** BtnThreadPoolDeadLock_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

  • 程序开始最大线程数被设置为16,线程池中所有线程都在阻塞等待信号,导致发生死锁,程序已经卡死,因为没有线程可用了,发送信号的任务无法执行。
  • 一般不要阻塞线程池的线程。
  • 不要把最大线程数设置的很小,使用默认即可。

如何回调

上一篇文章我们讲委托的异步调用时,提到BeginInvoke可以传入回调,在线程执行完后自动执行这个回调,可是ThreadPool并没有提供传入回调的API,我们可以自己动手去封装一个,只要启动线程时先完成线程任务委托的调用,再执行回调就可以了,例如:

private void ThreadPoolWithCallback(Action act, Action callback)
{ThreadPool.QueueUserWorkItem(state =>{act.Invoke();callback.Invoke();});
}private void BtnThreadPoolWithCallback_Click(object sender, EventArgs e)
{this.ThreadPoolWithCallback(() =>{Console.WriteLine($"这里是Action Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");for (int i = 0; i < 2000000000; i++){}Console.WriteLine($"这里是Action   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");}, () =>{Console.WriteLine($"这里是Callback Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");for (int i = 0; i < 2000000000; i++){}Console.WriteLine($"这里是Callback   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");});
}

输出结果:
在这里插入图片描述

如何获取返回值

上一篇文章我们讲委托的异步调用时,提到EndInvoke可以等待线程结束并获取返回值,可是ThreadPool没有提供获取线程返回值的API,传入的委托都是无返回值的,我们可以自己封装,先看下有问题的封装:

private T ThreadPoolWithReturn<T>(Func<T> func)
{T t = default(T);ManualResetEvent manualResetEvent = new ManualResetEvent(false);ThreadPool.QueueUserWorkItem(state =>{t = func.Invoke();manualResetEvent.Set();});manualResetEvent.WaitOne();return t;
}private void BtnThreadPoolWithReturn_Click(object sender, EventArgs e)
{int result = this.ThreadPoolWithReturn<int>(() =>{Console.WriteLine($"这里是func Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");for (int i = 0; i < 2000000000; i++){}Console.WriteLine($"这里是func   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");return DateTime.Now.Millisecond;});Console.WriteLine($"result={result} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
}

输出结果:
在这里插入图片描述
应该看出来虽然拿到了结果,但仔细看这个异步是“假异步”,线程刚启动就开始阻塞,等待线程拿到结果,这不和同步一样了吗?这里好像有点矛盾,又要结果,又不想阻塞,是不可能的,要结果就要等计算完成,但这个等待的时机我们可以好好斟酌一下,没必要子线程刚启动,主线程就阻塞等待,我们可以在子线程启动后,主线程先干点别的,等主线程真正要拿结果的时候再等待。那要如何实现?可以在子线程启动后返回一个委托,等需要拿结果的时候我们再调用这个委托。

private Func<T> ThreadPoolWithReturn<T>(Func<T> func)
{T t = default(T);ManualResetEvent manualResetEvent = new ManualResetEvent(false);ThreadPool.QueueUserWorkItem(state =>{t = func.Invoke();manualResetEvent.Set();});//返回委托在这里可以立即结束不需要阻塞,只有要结果的时候才会等待完成//因为这个委托中持有上下文环境比如 manualResetEvent 比如t,所以外部调用委托的时候可以拿到这些对象return () =>{manualResetEvent.WaitOne();return t;};
}private void BtnThreadPoolWithReturn_Click(object sender, EventArgs e)
{Console.WriteLine("");Console.WriteLine($"********** BtnThreadPoolWithReturn_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");Func<int> func = this.ThreadPoolWithReturn<int>(() =>{Console.WriteLine($"这里是func Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");for (int i = 0; i < 2000000000; i++){}Console.WriteLine($"这里是func   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");return DateTime.Now.Millisecond;});//假如拿结果前做了一个耗时的计算for (int i = 0; i < 1000000000; i++){}Console.WriteLine($"主线程拿结果前可以在这里干点别的... {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");int result = func.Invoke();Console.WriteLine($"result={result} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");Console.WriteLine($"********** BtnThreadPoolWithReturn_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

  • 返回委托并不会阻塞,只有在调用这个委托才会真正等待结果。
  • 拿结果前主线程可以和子线程可以并发运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/504298.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

68.基于SpringBoot + Vue实现的前后端分离-心灵治愈交流平台系统(项目 + 论文PPT)

项目介绍 本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述心灵治愈交流平台的当前背景以及系统开发的目的&#xff0c;后续章节将严格按照软件开发流程&#xff0c;对系统进…

Linux(上):基本知识篇

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、Linux初识1 Linux简介2 Linux学习环境配置(1)安装Linux(2)FinalShell远程连接Linux服务器二、Linux基础命令1 Linux目录结构,根目录 /2 Linux命令基础(1)什么是命令、命令行?(2)…

Python中的可变对象与不可变对象;Python中的六大标准数据类型哪些属于可变对象,哪些属于不可变对象

Python中的可变对象与不可变对象&#xff1b;Python中的六大标准数据类型哪些属于可变对象&#xff0c;哪些属于不可变对象 Python中的可变对象与不可变对象一、Python的六大标准数据类型1. 数字类型 (Number)2. 字符串 (String)3. 列表 (List)4. 元组 (Tuple)5. 集合 (Set)6. …

VSCode Live Server 插件安装和使用

VSCode Live Server是一个由Ritwick Dey开发的Visual Studio Code扩展插件&#xff0c;它提供了一个带有实时重载功能的本地开发服务器。在VSCode中安装和使用Live Server插件进行实时预览和调试Web应用程序。这将大大提高前端开发效率&#xff0c;使网页设计和开发变得更为流畅…

Personal APP

1、Matlab 2023b https://www.bilibili.com/opus/887246540317392920 https://blog.csdn.net/qq_25719943/article/details/138096918 https://www.jokerdown.com/22886.html 2、Jlink使用技巧 J-Scope虚拟示波器功能 Jlink使用技巧之J-Scope虚拟示波器功能 - 知乎 (zhihu.…

【马来西亚理工大学主办,ACM出版】2025年大数据、通信技术与计算机应用国际学术会议(BDCTA 2025)

2025年大数据、通信技术与计算机应用国际学术会议&#xff08;BDCTA 2025) 2025 International Conference on Big Data, Communication Technology and Computer Applications 2025年2月14-16日 | 马来西亚-吉隆坡 大会官网&#xff1a;更多详情【论文投稿】 主办单位&…

Sprint Boot教程之五十:Spring Boot JpaRepository 示例

Spring Boot JpaRepository 示例 Spring Boot建立在 Spring 之上&#xff0c;包含 Spring 的所有功能。由于其快速的生产就绪环境&#xff0c;使开发人员能够直接专注于逻辑&#xff0c;而不必费力配置和设置&#xff0c;因此如今它正成为开发人员的最爱。Spring Boot 是一个基…

超完整Docker学习记录,Docker常用命令详解

前言 关于国内拉取不到docker镜像的问题&#xff0c;可以利用Github Action将需要的镜像转存到阿里云私有仓库&#xff0c;然后再通过阿里云私有仓库去拉取就可以了。 参考项目地址&#xff1a;使用Github Action将国外的Docker镜像转存到阿里云私有仓库 一、Docker简介 Do…

左神算法基础巩固--3

文章目录 二叉树二叉树的遍历先序遍历中序遍历后序遍历 解答二叉树的宽度优先遍历 在这里插入图片描述 一颗完全二叉树具有以下特征&#xff1a;1.不存在任何一个节点具有右子树但不存在左子树.2.不存在任何一个节点在满足1的情况下左右子树不全且其后续节点不为叶子节点 根据以…

推动多语言语音科技迈向新高度:INTERSPEECH 2025 ML-SUPERB 2.0 挑战赛

随着语音技术在各领域应用的迅速扩展&#xff0c;全球语言与口音的多样性成为技术进一步突破的重大挑战。为了应对这一难题&#xff0c;来自卡内基梅隆大学&#xff08;CMU&#xff09;、斯坦福大学&#xff08;Stanford University&#xff09;、乔治梅森大学(George Mason Un…

IvorySQL 升级指南:从 3.x 到 4.0 的平滑过渡

日前&#xff0c;IvorySQL 4.0 重磅发布&#xff0c;全面支持 PostgreSQL 17&#xff0c;并且增强了对 Oracle 的兼容性。关于 IvorySQL 4.0 的介绍&#xff0c;各位小伙伴可以通过这篇文章回顾&#xff1a;IvorySQL 4.0 发布&#xff1a;全面支持 PostgreSQL 17. 在 IvorySQL…

flink的EventTime和Watermark

时间机制 Flink中的时间机制主要用在判断是否触发时间窗口window的计算。 在Flink中有三种时间概念&#xff1a;ProcessTime、IngestionTime、EventTime。 ProcessTime&#xff1a;是在数据抵达算子产生的时间&#xff08;Flink默认使用ProcessTime&#xff09; IngestionT…

Windows11环境下设置MySQL8字符集utf8mb4_unicode_ci

1.关闭MySQL8的服务CTRLshiftESC&#xff0c;找到MySQL关闭服务即可 2.找到配置文件路径&#xff08;msi版本默认&#xff09; C:\ProgramData\MySQL\MySQL Server 8.0 3.使用管理员权限编辑my.ini文件并保存 # Other default tuning values # MySQL Server Instance Config…

python学习笔记—14—函数

1. 函数 (1) len与my_len str "supercarrydoinb"def my_len(tmp_str):cnt 0for i in tmp_str:cnt 1return cntstr_len_1 len(str) str_len_2 my_len(str) print(f"len {str_len_1}") print(f"my_len {str_len_2}") (2) 函数传参数量不受…

Flink源码解析之:Flink on k8s 客户端提交任务源码分析

Flink on k8s 客户端提交任务源码分析 当我们需要在代码中提交Flink job到kubernetes上时&#xff0c;需要如何做呢&#xff1f;要引入什么第三方依赖&#xff1f;需要提供什么内容&#xff1f;flink是如何将job提交到k8s上的&#xff1f;经过了什么样的流程&#xff0c;内部有…

React Native 项目 Error: EMFILE: too many open files, watch

硬件&#xff1a;MacBook Pro (Retina, 13-inch, Mid 2014) OS版本&#xff1a;MacOS BigSur 11.7.10 (20G1427) 更新: 删除modules的方法会有反弹&#xff0c;最后还是手动安装了预编译版本的watchman。 React Native 项目运行npm run web&#xff0c;出现如下错误&#xff1a…

51单片机——定时器中断(重点)

STC89C5X含有3个定时器&#xff1a;定时器0、定时器1、定时器2 注意&#xff1a;51系列单片机一定有基本的2个定时器&#xff08;定时器0和定时器1&#xff09;&#xff0c;但不全有3个中断&#xff0c;需要查看芯片手册&#xff0c;通常我们使用的是基本的2个定时器&#xff…

kubernetes第五天

1.Probe&#xff08;探针&#xff09;之readinessProbe就绪探针&#xff0c;可用性检查 readinessProbe此探针如果检查失败&#xff0c;pod会处于未就绪状态 1.exec方式检查 #通过rc资源创建了三个pod,然后使用services资源&#xff0c;对外提供三个pod的容器的访问入口。 ap…

优化提示词改善答疑机器人回答质量

1.通过优化提示词来调整大模型的回答 1.1使用场景 默认提示词无法满足业务要求。 回答的内容太简单/困难&#xff0c;输出内容/格式/语气达不到要求等 1.2llama-index 的提示词模版 1.2.1llama-index 的默认模板 from llama_index.llms.dashscope import DashScope from lla…

计算机网络 笔记 物理层

物理层的目的:主要为了实现相邻节点之间的数据的传输(01010....) 通信基础概念 信源:信号的发送方 信宿:信号的接收方 信道:信号的通道,通常一个物理的线路包含了两个:发送信道和接受信道 信号:数据的载体,有两种分别是 数字信号:离散的信号值 模拟信号:连续的信号值 马元…