C# 对 .NET 托管线程的理解
创建并启动线程
C#的线程对象(Thread)位于命名空间 System.Threading 中,创建一个线程对象即创建了一个托管线程,线程的创建要求传入一个 ThreadStart 对象,这个对象指定线程执行的方法(通过构造函数传入委托)。创建线程后,可以通过 Start 方法启动线程。例如下面的代码:
public class Program { static void StaticMethod() { Console.WriteLine("静态方法"); } void InstanceMethod() { Console.WriteLine("实例方法"); } static void Main(string[] args) { //创建线程(静态方法) Thread thread1 = new Thread(new ThreadStart(StaticMethod)); thread1.Start(); //创建线程(实例方法) Program p = new Program(); Thread thread2 = new Thread(new ThreadStart(p.InstanceMethod)); thread2.Start(); thread2.Join(); Console.ReadKey(); } }
可以看到C#的线程不仅支持静态方法,也支持对象方法。通过 Start 方法启动线程后,不会等待线程执行完毕,如果需要这么做,可以使用 Join 方法,该方法阻塞直到线程结束。上面的线程是没有传入参数的,你可以通过 ParameterizedThreadStart 传入含有一个Object类型参数的方法,并在使用 Start 方法启动线程时传入参数,例如:
static void MethodWithPara(object obj) { Console.WriteLine(obj); } static void Main(string[] args) { //创建线程并传参 Thread thread = new Thread(new ParameterizedThreadStart(MethodWithPara)); thread.Start("传参"); }
需要注意的是,由于必须使用Object类型,所以这样传入参数是类型不安全的,更好的办法是自己创建一个类,将线程参数包装为类的属性,线程通过访问类的属性获取数据。
线程的常用操作
- Thread.CurrentThread:静态属性,获取当前运行的线程对象
- Thread.Sleep:静态方法,传入一个毫秒时间,休眠调用该方法的线程
Thread 实例方法:
- Start:启动线程
- Join:(阻塞)等待线程结束
- Suspend:挂起线程
- Resume:恢复挂起的线程
- Interrupt:中断属于 WaitSleepJoin 状态的线程,并且CLR会抛出ThreadInterruptedExecption异常
- Abort:强制销毁线程,并且CLR会抛出ThreadAbortException异常
Thread 实例属性:
- Name:读取或设置线程的名字,用于备注
- isAlive:获取线程的执行状态,当线程已经开始且未正常停止或销毁时,返回真
- State:获取线程的状态(ThreadState),详见ThreadState Enum
- isBackground:读取或设置线程是否为后台线程
- Priority:读取或设置线程的调度优先级(ThreadPriority)
线程的优先级:使用 Priority 属性 (ThreadPriority)指定,所有线程默认为 Normal 优先级,这个优先级决定了操作系统如何在线程之间进行调度,但这是一个期待值,操作系统可能会动态调整线程的优先级(比如用户在程序前台/后台切换时)。
后台线程:使用 isBackground 属性指定,.NET将托管线程分为两类——前台线程和后台线程,它们的唯一区别是,后台线程不保持进程的运行,如果该进程所有前台线程都停止了,那么进程会结束所有后台线程,并结束自身,只有存在至少一个运行的前台线程,进程才会继续运行。默认情况下,主线程和通过 Thread 构造函数创建的线程都是前台线程,而ThreadPool线程和非托管的线程是后台线程。
线程挂起与休眠的区别:从用户的角度来看,这两种状态几乎没有区别,在挂起和休眠状态,线程都不会占用处理器的时间片,即放弃操作系统的线程调度。从编写程序的角度来看,Sleep 总是休眠调用它的线程,这意味着你不能在一个线程中休眠另一个线程,但是 Suspend 可以。另一个区别是休眠的线程会在指定时间结束后自动恢复(但是这个间隔不一定准确,Sleep只是让线程暂时放弃调度,时间结束后重新参与调度,但线程是否立即执行仍取决于操作系统的调度策略),而挂起的线程必须手动恢复。需要注意的是,无论是休眠的线程还是挂起的线程,都会一直持有锁。(实际上 Suspend 方法已经被弃用,因为用它来同步线程状态是不推荐的)
暂停线程:可以使用Sleep方法或者Interrupt方法,如果需要强制停止,可以使用Abort方法。下面是 Interrupt 方法的一个例子:
class StayAwake { bool sleepSwitch = false; public bool SleepSwitch { set { sleepSwitch = value; } } public StayAwake() { } public void ThreadMethod() { Console.WriteLine("进程开始执行"); while (!sleepSwitch) { //自旋 Thread.SpinWait(10000000); } try { Console.WriteLine("进程即将休眠"); Thread.Sleep(Timeout.Infinite); } catch (ThreadInterruptedException e) { Console.WriteLine("休眠被中断"); } Console.WriteLine("线程继续执行"); } } public class Program { static void Main(string[] args) { StayAwake stayAwake = new StayAwake(); Thread thread = new Thread(stayAwake.ThreadMethod); thread.Start(); thread.Interrupt(); stayAwake.SleepSwitch = true; thread.Join(); Console.ReadKey(); } }
托管线程 vs 系统线程
.NET 的托管线程和操作系统的线程(例如使用WIN32 API操作的线程)是不一样的,托管代码在托管线程中执行,而托管线程是在CLR虚拟机上执行的,即托管线程实际上是CLR虚拟机对操作系统线程的再一次抽象,在Windows平台,它的内部实际上也是对WIN32 API的封装。
创建一个 Thread 对象,实际上就创建了一个托管线程,但是托管线程和系统线程并不一定是一对一的关系。例如当托管线程未启动时,操作系统显然没有执行任何新的线程,而即便是托管线程处于执行状态,从原则上也不保证它对应某一个特定的系统线程(即便大部分情况下是这样),因为.NET允许一个托管线程在多个系统线程进行切换。
这就是为什么托管线程对象并没有提供诸如线程句柄或者线程ID等WIN32的线程属性,而是提供了 ManagedThreadId 属性,为托管线程实例指定一个唯一的标识符,所谓的前台/后台线程也是.NET自己设定的概念。当然,.NET也允许外部的非托管线程(通过COM)进入托管环境。
关于WIN32 API与CLR托管线程的映射如下:
Win32 | CLR |
---|---|
CreateThread | Combination of Thread and ThreadStart |
TerminateThread | Thread.Abort |
SuspendThread | Thread.Suspend |
ResumeThread | Thread.Resume |
Sleep | Thread.Sleep |
WaitForSingleObject on the thread handle | Thread.Join |
ExitThread | No equivalent |
GetCurrentThread | Thread.CurrentThread |
SetThreadPriority | Thread.Priority |
No equivalent | Thread.Name |
No equivalent | Thread.IsBackground |
Close to CoInitializeEx (OLE32.DLL) | Thread.ApartmentState |
在CLR虚拟机,还允许着几种特殊的托管线程供CLR自己使用:
- Finalizer Thread:用于对象的终结和回收。
- GC Thread:用于垃圾回收(GC)。
- Debug Thread:用于调试。
- AppDomain-Unload Thread:用于AppDomain相关。
- ThreadPool Threads:内部维护的一个快速线程池,用于.NET的异步、IO读取等。
可以看出,在CLR,即便是单线程程序,它的运行也需要上述这些线程来维护。