欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > 编程深水区之并发⑤:C#的Thread线程

编程深水区之并发⑤:C#的Thread线程

2024/10/25 2:26:03 来源:https://blog.csdn.net/2401_85195613/article/details/141070494  浏览:    关键词:编程深水区之并发⑤:C#的Thread线程

Windows、Linux和MacOS三大操作系统的进程和线程机制,实现上有一些差异,但大体的原理是差不多的。本章节讨论的进程和线程,以Windows操作系统为准。

一、再次深入进程和线程

不晓得大家有没有用过Windows95(用过的在评论区扣个1)?那个时代,最无奈的按键应该就是主机上的Reset。Windows95,是从16位过渡到32位的第一代系统,同时兼容16位和32位,所以仍然存在很多16位时代的老毛病,爱死机。主要原因是,16位时代的Windows是单线程系统,其中任何一个任务陷入死循环,就会造成死机,当然也有可能是这个任务还在执行、但你看不到任何响应的假死机。所以,Win95之后的Windows系统都是多线程操作系统(抢占式)。

1.1 进程和内存

首先出现的是进程,在一个进程中运行应用程序的实例。进程是应用程序实例需要使用的资源的集合,包括开辟一个独立的虚拟地址空间、加载应用程序的EXE、DLL等文件到内存。进程之间、以及进程与操作系统之间的资源相互独立,不能相互访问,所以系统变得更加安全和健壮。
创建进程的开销是很大的,它需要将应用程序的文件加载到内存中,往往体现在打开应用的速度上。此外,如你所见,进程是与内存相关的概念。而线程要做的事情,是跑应用程序的代码,所以它和CPU相关。

1.2 线程和CPU

现在继续解决应用程序发生死循环导致死机的问题。如果CPU只能执行完一个线程后再执行下一个线程,它就永远无法解决爱死机的问题。所以,Windows将CPU虚拟化,具体的做法就是将CPU的执行时间划分为一个个细小的时间片,通常只有几十毫秒,我们称它为逻辑CPU,而操作系统的任务,就是统一调度逻辑CPU来执行线程,逻辑CPU的执行和切换速度很快,所以你感觉不到上一刻帮你处理事情的CPU,实际上已经跑到其它地方去了。Windows为每个进程都分配了线程,应用程序的代码进入死循环时,那个代码相关联的进程会冻结,CPU会让出时间片,并执行其它等待的线程。同时,线程还允许用户使用另外一个应用程序(比如任务管理器)强制终止可能已经冻结的应用程序,从此Reset键变成了Ctrl+Alt+Delete键。
线程运行在进程环境中,共享进程的资源,创建线程的代价比进程低很多,但也不是完全没有开销。线程的开销主要包括以下几个方面:

  • **创建和销毁线程的开销:**创建内核对象(包括线程上下文)、环境块、用户模式栈、内核模式栈、DLL连接与分离(有些程序可能有几百个DLL),这里面包含了空间和时间的开销。
  • **线程切换的开销:**线程分配给CPU后,运行一个时间片,时间片到期,立即切换到下一个线程。切换线程时,①当前运行线程中CPU寄存器保存的结果要转移到内核对象的线程上下文中,②然后操作系统选择一个线程供调度,如果这个线程属于另外一个进程,还需要切换到另一个虚拟地址空间;③将新线程上下文结构中的值加载到CPU寄存器中。上下文切换的开销比想象中大,线程上下文保存在内存中,CPU执行时需要加载到寄存器中,而30ms之后,一次新的切换又发生了。
  • **垃圾回收机制GC和调试:**GC执行时,CLR(包含C#的运行时)必须挂起所有线程,干完活后,再恢复所有线程,挂起和恢复都需要额外开销,所以GC的性能和线程数量息息相关。调试程序时,也有类似行为。

补充一点,线程机制刚出来时,电脑都还是单核CPU,但现在是多核时代,还有超线程技术加持,CPU得以实现真正的并行,极大提升了操作系统的性能和响应能力。但是,单个CPU的运行机制仍然没有改变,且分配到某个CPU上执行的线程,会一直待在这个CPU上。

1.3 线程优先级

本节纯碎了解一下,对理解线程影响不大,一般也不需要去设置线程优先级。
前面有提到,现在Windows操作系统是抢占式的多线程操作系统,线程在任何时间都有可能停止并切换到另外一个线程,那Windows是如何决定什么时候执行哪个线程呢?
首先,检查前面提到的内核对象(上下文在里面),挑选出适合调度的线程;然后,在这些备选的线程中,执行优先级高的线程。早期版本中线程的优先级,划分为0-31级,31级的优先级最高。有一个默认的0级线程,由操作系统在启动时创建,它是内存的清道夫,但没有其它任何线程可执行时,系统就会执行这个0级线程,将所有空闲的内存清零。
这个0-31级的优先级,我们是无法控制的,但Windows公开了优先级的一个抽象层。进程被划分为:Idle、BelowNormal、Normal、AboveNormal、High和Realtime6个层级,线程被划分为Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest和Time-Critical7个层级,它们两两相交,确定了一个线程的层级,比如一个Normal进程中的Normal线程,它映射的优先级为8。我们开发应用程序,一般都有宿主环境,比如基于AspNetCore的应用,所有应用进程的优先级都是受宿主环境约束的。

二、Thread线程

2.1 创建线程的方式

在C#中,我们可以非常方便的创建和使用线程,实现并发(异步)编程。如前所述,C#的多线程实际上是由操作系统进行统一调度和管理的,CLR只是公开了相应的操作API。目前使用多线程的方式,主要有三种,一是使用Thread创建前台线程;二是使用CLR管理的线程池;三是在线程池基础上的TPL。本章节先说Thread,但需要提醒的是,任何时候,都应该优先考虑使用线程池。

2.2 创建Thread线程

//Thread的构造方法有多个重载,参数是委托类型
//1、传入方法===================================================
//以下代码输出 1 7,其中1为主线程的ID,7为新线程的ID
//如果将Thread.Sleep(500)注释打开,输出7 1
//Thread.Sleep()方法阻塞当前线程,让当前线程等待
public class Program
{static void Main(string[] args){var mythread = new Thread(MyThreadMethod);//创建新线程mythread.Start();//启动线程//Thread.Sleep(500); //阻塞当前线程(主线程)Thread thread = Thread.CurrentThread;//获取当前主线程对象Console.WriteLine(thread.ManagedThreadId);//输出主线程ID}//在新线程中执行的方法static void MyThreadMethod(){Thread thread = Thread.CurrentThread;//获取新线程对象Console.WriteLine(thread.ManagedThreadId);//输出新线程ID}
}//2、传入Lambda=================================================
public class Program
{static void Main(string[] args){var mythread = new Thread(() =>{Thread thread = Thread.CurrentThread;Console.WriteLine(thread.ManagedThreadId);});mythread.Start();Thread thread = Thread.CurrentThread;Console.WriteLine(thread.ManagedThreadId);}
}//3、创建新线程时传参============================================
//注意,委托的参数只有一个,且必须是object类型,在新线程内要进行转换
public class Program
{static void Main(string[] args){var mythread = new Thread((object? obj) => {if (obj is not null) {int num;var sucess = int.TryParse(obj.ToString(), out num);if (sucess){Console.WriteLine($"输入参数为:{num}");}}});mythread.Start(5);//传入参数}
}

2.3 当前线程状态Thread.CurrentThread

//通过Thread的静态属性CurrentThread,获取当前线程对象
public class Program
{static void Main(string[] args){var currentThread = Thread.CurrentThread;//获取当前线程对象currentThread.Name = "主线程";//设置当前线程名称Console.WriteLine(currentThread.Name);//线程名称Console.WriteLine(currentThread.ManagedThreadId);//线程IDConsole.WriteLine(currentThread.CurrentCulture);//线程区域Console.WriteLine(currentThread.CurrentUICulture);//线程语言Console.WriteLine(currentThread.IsAlive);//是否存活Console.WriteLine(currentThread.IsBackground);//是否是后台线程Console.WriteLine(currentThread.IsThreadPoolThread);//是否是线程池线程}
}
/*输出:
主线程
1
zh-CN
zh-CN
True
False
False
*/

2.4 线程阻塞,Thread.Sleep()和线程对象的Join()方法

//1、Thread.Sleep()静态方法======================================
//Thread.Sleep(),会阻塞当前线程,让当前线程等待规定时间
//Task.Delay()可以实现类似功能,但它不会阻塞,两者区别在下个章节展开
public class Program
{static void Main(string[] args){Console.WriteLine(Thread.CurrentThread.ManagedThreadId);Console.WriteLine(DateTime.Now);Thread.Sleep(1000);Console.WriteLine(DateTime.Now);Console.WriteLine(Thread.CurrentThread.ManagedThreadId);}
}
/*输出
1
2024/8/1 15:03:06
2024/8/1 15:03:07
1
*///1、Join(),Thread的实例方法====================================
//当在一个线程中调用另一个线程对象的 Join() 方法时
//当前线程会被阻塞,直到被调用 Join() 方法的线程执行完毕。
public class Program
{static void Main(){var newthread = new Thread(NewThreadMethod);newthread.Start();//主线程调用newthread的Join方法,等待newthread执行完毕newthread.Join();Console.WriteLine("newthread执行完毕,主线程继续执行");}static void NewThreadMethod(){Console.WriteLine("子线程开始执行");Thread.Sleep(3000);//模拟耗时操作Console.WriteLine("子线程执行结束");}
}

2.5 前台线程和后台线程

使用Thread创建的线程,默认和主线程一样,IsBackground值为false,即前台线程,而使用CLR线程池创建立的线程,默认为后台线程。前台线程和后台线程的区别为:当一个进程的所有前台线程都停止时,CLR强制终止仍在运行的所有后台进程,并退出进程。随意使用Thread创建一个长时运行的前台线程是很危险的,比如你在GUI的Button事件上创建了一个长时运行的Thread前台线程,当你关闭UI窗口后,会发现任务管理器中,仍然在运行着这个应用。Thread创建线程时,是可以设置为后台线程的,如下:

public class Program
{static void Main(){var newthread = new Thread(NewThreadMethod);newthread.IsBackground = true;newthread.Start();Console.ReadKey();}static void NewThreadMethod(){var currentThread = Thread.CurrentThread;Console.WriteLine(currentThread.IsBackground);Console.WriteLine(currentThread.ManagedThreadId);}
}
/*输出
true
7
*/

2.6 线程终止

最优情况下,Thread线程应该可以自然终止,这样线程退出时能够安全的清理资源。除此之外,也可以通过代码强行终止线程。.NET Framework,可以使用thread.Abort()强行终止,但它不安全,可能导致资源泄漏或不一致状态。所以,到了.NETCore和现在的.NET时代,移除了这个API。如果要强行退出线程,推荐使用共享变量或CancellationToken,两种用法相似。CancellationToken常用于TPL任务并行库(Task Parallel Library),放到下个章节。以下为使用共享变量:

//调用Stop()方法,线程退出
//CancellationToken原理和这个类似
private bool _shouldStop = false;
public void DoWork()
{while (!_shouldStop){// 执行线程的工作}// 清理资源
}public void Stop()
{_shouldStop = true;
}

*这是一个系列文章,将全面介绍多线程、用户态协程和单线程事件循环机制,建议收藏、点赞哦!
*你在并发编程过程中碰到了哪些难题?欢迎评论区交流~~~


我是functionMC > function MyClass(){…}
C#/TS/鸿蒙/AI等技术问题,以及如何写Bug、防脱发、送外卖等高深问题,都可以私信提问哦!

image.png

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com