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,即便是单线程程序,它的运行也需要上述这些线程来维护。