C#多线程学习之Thread、ThreadPool、Task、Parallel四者区别
作者:Alan.hsiang 发布时间:2023-08-27 05:32:14
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。本文以一些简单的小例子,简述多线程的发展历程【Thread,ThreadPool,Task,Parallel】,仅供学习分享使用,如有不足之处,还请指正。
Thread
Thread做为早期【.Net Framework1.0】的.Net提供的多线程方案,提供了很多的封装方法,来操作线程。具体如下所示:
Start方法,用于启动一个线程。线程的状态变更为Running。
Suspend方法,挂起一个线程,或者如果线程状态为已挂起,则不起作用。
Resume方法,如果线程为已挂起,这继续运行。
Join方法,等待线程,直到线程结束,也可以设置等待时间。
Abort方法,强制终止线程,跑出ThreadAbortException异常。
其他线程属性:IsBackground是否后台线程,IsThreadPoolThread是否线程池线程,IsAlive线程是否运行,Priority线程优先级,ThreadState当前线程状态,ManagedThreadId线程唯一标识等
通过Thread可以单独的开启一个线程,通过构造函数来创建线程对象,可以是无参数也可以是带参数。其中参数ThreadStart是一个无参数委托,ParameterizedThreadStart为一个带参数委托。
无参数,示例如下所示:
private void btnThread_Click(object sender, EventArgs e)
{
ThreadStart threadStart = new ThreadStart(DoSomethingLong);
Thread thread = new Thread(threadStart);
thread.Start();
}
private void DoSomethingLong() {
string name = "Thread";
Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
//CPU计算累加和
long rest = 0;
for (int i = 0; i < 1000000000; i++)
{
rest += i;
}
Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
}
示例结果如下所示:
带参数示例,如下所示:
private void btnThread2_Click(object sender, EventArgs e)
{
ParameterizedThreadStart threadStart = new ParameterizedThreadStart(DoSomethingLongWithParam);
Thread thread = new Thread(threadStart);
string name = "Param";
thread.Start(name);
}
private void DoSomethingLongWithParam(object name) {
Console.WriteLine("************DoSomethingLongWithParam 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
//CPU计算累加和
long rest = 0;
for (int i = 0; i < 1000000000; i++)
{
rest += i;
}
Console.WriteLine("************DoSomethingLongWithParam 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
}
带参数示例结果,如下所示:
通过对以上示例进行分析,得出结论如下所示:
线程是由操作系统进行创建的,Thread提供的方法只是对底层方法的封装。比如执行对应方法后,操作系统并不会立即执行相应的操作,而要CPU时间片轮转后才会执行响应。
Thread创建线程过于松散,缺乏管理,例如,如果同时创建10000个线程,程序也不会报错,但是系统可能无法承载如此多的线程而导致崩溃。
Thread的频繁创建和销毁,也会消耗系统资源。
ThreadPool
为了应对Thread创建缺乏管理的问题,在后续版本【.Net Framework2.0】中推出了线程池的概念。那什么是线程池呢?
池化资源管理设计思想:线程是一种资源,之前每次需要线程,都是去创建线程,使用完成后,再释放掉。池化,就是做一个容器,容器提前申请指定数量的线程,需要用到线程的时候,直接到线程池中取,用完之后再放回容器【通过控制状态标识线程是否正在被使用】。避免频繁的创建和销毁,容器还会根据限制的数量取申请和释放。
关于通过线程池创建多线程,具体示例如下:
private void btnThread3_Click(object sender, EventArgs e)
{
WaitCallback waitCallback = new WaitCallback(DoSomethingLongWithParam);
string name = "ThreadPool";
ThreadPool.QueueUserWorkItem(waitCallback,name);
}
线程池示例执行结果如下所示:
通过对线程池执行结果进行分析,得出结论如下:
两次执行结果,均为同一个线程ID,说明线程用完并未销毁,而是放回线程池子,待下次使用时重新取出,继续使用。
通过线程池可以有效的控制线程并发的数量,避免资源的浪费。
通过分析源码发现,ThreadPool线程池提供的接口较少,在线程等待和交互方面不太友好。
Task
随着.Net版本的演化,后续版本【.Net Framework3.0】推出了Task做为多线程解决方案。默认情况下,可以通过构造函数创建Task,示例如下:
private void btnTask_Click(object sender, EventArgs e)
{
Action action = new Action(DoSomethingLong);
Task task = new Task(action);
task.Start();
}
默认Task示例,执行结果如下:
通过对以上Task示例和源码进行分析,得出结论如下:
Task产生的线程,全部都是线程池线程。
Task提供的丰富的API,便于开发实践。
Parallel
Parallel提供对并行线程的支持,可以通过Parallel同时发起多个线程,在某些方面具有应用优势,默认示例如下所示:
private void btnParallel_Click(object sender, EventArgs e)
{
Action action = new Action(DoSomethingLong);
Parallel.Invoke(action,action,action);
}
Parallel的Invoke方法执行,结果如下:
通过对Parallel的Invoke示例方法进行分析,得出结论如下:
Parallel的Invoke方法,可以同时开启多个线程,同时主线程【线程ID=1】也会参与计算,即页面也会卡住。
Parallel可以通过ParallelOptions.MaxDegreeOfParallelism指定并发数量。
Task专讲
以下面的一个场景为例进行说明:
假如开发一个系统,流程如下:
1. 前期的需求调研,需求分析,系统设计,详细设计(顺序执行,是开发编码的前提)
2.按模块开发【中间阶段,可多人同时工作】
3.测试【顺序执行,是开发编码的后续工作】
分析:以上三个阶段,每一个阶段又可以细分数个小阶段,其中有些阶段是顺序执行的,有些阶段又可以并行执行。
以代码的形式进行描述,如下所示:
private void btnTask2_Click(object sender, EventArgs e)
{
//开发前的工作
Console.WriteLine("组建团队");
Console.WriteLine("需求分析");
Console.WriteLine("系统设计");
Console.WriteLine("详细设计");
//开始开发
Task.Run(() => { Coding("张三", "接口"); });
Task.Run(() => { Coding("李四", "前端页面"); });
Task.Run(() => { Coding("王五", "手机App"); });
Task.Run(() => { Coding("刘大", "后端业务"); });
//开发后的工作
Console.WriteLine("alpha测试");
Console.WriteLine("beta测试");
Console.WriteLine("uat测试");
Console.WriteLine("系统上线");
}
private void Coding(string developer,string model) {
Console.WriteLine("【Begin】在{0},{1}开始开发{2},线程id为{3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), developer,model, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("【 End 】在{0},{1}完成开发{2},线程id为{3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), developer, model, Thread.CurrentThread.ManagedThreadId);
}
示例运行结果,如下所示:
通过分析以上示例,发现程序并未按照预期的运行,很明显的一点:测试跑到了开发前面。
为了解决上述顺序错乱的问题,Task提供了WaitAll方法,如下所示:
private void btnTask2_Click(object sender, EventArgs e)
{
//开发前的工作
Console.WriteLine("组建团队");
Console.WriteLine("需求分析");
Console.WriteLine("系统设计");
Console.WriteLine("详细设计");
//开始开发
List<Task> tasks = new List<Task>();
tasks.Add(Task.Run(() => { Coding("张三", "接口"); }));
tasks.Add(Task.Run(() => { Coding("李四", "前端页面"); }));
tasks.Add(Task.Run(() => { Coding("王五", "手机App"); }));
tasks.Add(Task.Run(() => { Coding("刘大", "后端业务"); }));
Task.WaitAll(tasks.ToArray());
//开发后的工作
Console.WriteLine("alpha测试");
Console.WriteLine("beta测试");
Console.WriteLine("uat测试");
Console.WriteLine("系统上线");
}
运行示例,结果如下所示:
通过运行以上示例,发现:顺序确实符合预期,可以满足要求,但是程序会卡住,这点不太友好。
如何才能优雅的控制先后顺序呢?Task还提供了TaskFactory,如下所示:
private void btnTask2_Click(object sender, EventArgs e)
{
//开发前的工作
Console.WriteLine("组建团队");
Console.WriteLine("需求分析");
Console.WriteLine("系统设计");
Console.WriteLine("详细设计");
//开始开发
List<Task> tasks = new List<Task>();
tasks.Add(Task.Run(() => { Coding("张三", "接口"); }));
tasks.Add(Task.Run(() => { Coding("李四", "前端页面"); }));
tasks.Add(Task.Run(() => { Coding("王五", "手机App"); }));
tasks.Add(Task.Run(() => { Coding("刘大", "后端业务"); }));
TaskFactory taskFactory = new TaskFactory();
taskFactory.ContinueWhenAll(tasks.ToArray(), new Action<Task[]>((taskArray) => {
//开发后的工作
Console.WriteLine("alpha测试");
Console.WriteLine("beta测试");
Console.WriteLine("uat测试");
Console.WriteLine("系统上线");
}));
}
TaskFactory示例测试,如下所示:
通过对示例进行分析,得出如下结论:
业务逻辑上已要求,页面也不会卡顿,优雅的实现了多线程的操作。
Task产生的线程,为线程池线程。
TaskFactory不仅提供了ContinueWhenAll等待所有线程,还提供了ContinueWhenAny等待任意线程。
来源:https://www.cnblogs.com/hsiang/p/15690604.html


猜你喜欢
- /// <summary>/// 人民币大写/// </summary>/// <param name=&qu
- 文档中的设置有序或无序列表是一种反应内容上下级关系或者内容相同属性的方式,与单纯的文字叙述相比,它能有效增强文档内容的条理性,突出重点。因此
- System.Threading.Timer 是由线程池调用的。所有的Timer对象只使用了一个线程来管理。这个线程知道下一个Timer对象
- 本文实例为大家分享了android实现圆环倒计时控件的具体代码,供大家参考,具体内容如下1.自定义属性<?xml version=&q
- Java中,将字节数组转成图片的有很多种方式,今天在这里记录其中一种,方便以后查询,也可以提供给没有接触的童鞋做一个参考。首先是将图片转成字
- 简介目的:Optional的出现主要是为了解决null指针问题,也叫NPE(NullPointerException)外形:Optional
- NullPointerException是当您尝试使用指向内存中空位置的引用(null)时发生的异常,就好像它引用了一个对象一样。当我们声明
- 在编写ui界面时因为手机分辨率大小不同,所以展现出来的效果也是不同的,这个时候就需要考虑适配器,让根据手机分辨率自动适配相应尺寸来展示界面,
- List集合相信大家在开发过程中几乎都会用到。有时候难免会遇到集合里的数据是重复的,需要进行去除。然而,去重方式有好几种方式,你用的是哪种方
- 一、什么是轻量级锁轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用moni
- 序列化简介Java 的对象序列化将那些实现 Serializable 接口的对象转换成一个字节序列,并能在之后将这个字节序列完全恢复为原来的
- 本文介绍Android中Intent的各种常见作用。1 Intent.ACTION_MAINString: android.intent.a
- 一、前言写今天这篇文章的缘由,其实是来自于前段时间和粉丝的一个聊天,最近他打算参加游戏创作大赛,问我需要准备学习什么知识,以及参加比赛的注意
- 这篇文章主要介绍了Java如何利用return结束方法调用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要
- 近日,Eclipse经常挂掉,都是由于JVM崩溃的原因。每次都有以下错误日志:## A fatal error has been detec
- 现在很多Android应用在首次安装完都会有指引如何使用该应用的某些功能的指引界面,这样会获得很好的用户体验,能够帮助用户更好使用应用的某些
- Java中字符串中子串的查找共有四种方法(indexof()) indexOf 方法返回一个整数值,指出 String 对象内子字符串的开始
- HashMap 在不同的 JDK 版本下的实现是不同的,在 JDK 1.7 时,HashMap 底层是通过数组 + 链表实现的;而在 JDK
- dynamic关键字和动态语言运行时(DLR)是.Net 4.0中新增的功能。什么是"动态"?编程语言有时可以划分为静态
- 实践过程效果代码public partial class Form1 : Form{ public Form1()