C# .NET中的多线程编程
.NET为多线程编程提供了丰富的类型和机制,程序员需要做的就是掌握这些类型和机制的使用方法和运行原理。
2.1 如何在.NET程序中手动控制多个线程?
.NET中提供了多种实现多线程程序的方法,但最直接且灵活性最大的,莫过于主动创建、运行、结束所有线程。
(1)第一个多线程程序
.NET提供了非常直接的控制线程类型的类型:System.Threading.Thread类。使用该类型可以直观地创建、控制和结束线程。下面是一个简单的多线程程序:
class Program { static void Main(string[] args) { Console.WriteLine("进入多线程工作模式:"); for (int i = 0; i < 10; i++) { Thread newThread = new Thread(Work); // 开启新线程 newThread.Start(); } Console.ReadKey(); } static void Work() { Console.WriteLine("线程开始"); // 模拟做了一些工作,耗费1s时间 Thread.Sleep(1000); Console.WriteLine("线程结束"); } }
在主线程中,该代码创建了10个新的线程,这个10个线程的工作互不干扰,宏观上来看它们应该是并行运行的,执行的结果也证实了这一点:
PS:这里再次强调一点,当new了一个Thread类型对象并不意味着生成了一个线程,事实上线程的生成是在调用Thread的Start方法的时候。另外在之前的介绍中,这里的线程并不一定是操作系统层面上产生的一个真正线程!
(2)控制线程的状态
很多时候,我们需要主动关心线程当前所处的状态。在任意时刻,.NET中的线程都会处于如下图所示的几个状态中的某一个状态上,该图也直观地展示了一个线程可能经过的状态转换过程(该图并没有列出所有的状态转换途径/原因):
下面的示例代码则展示了我们如何手动地查看和控制一个线程的状态:
class Program { static void Main(string[] args) { Console.WriteLine("开始测试线程1"); // 初始化一个线程 thread1 Thread thread1 = new Thread(Work1); // 这时状态:UnStarted PrintState(thread1); // 启动线程 Console.WriteLine("现在启动线程"); thread1.Start(); // 这时状态:Running PrintState(thread1); // 让线程飞一会 3s Thread.Sleep(3 * 1000); // 让线程挂起 Console.WriteLine("现在挂起线程"); thread1.Suspend(); // 给线程足够的时间来挂起,否则状态可能是SuspendRequested Thread.Sleep(1000); // 这时状态:Suspend PrintState(thread1); // 继续线程 Console.WriteLine("现在继续线程"); thread1.Resume(); // 这时状态:Running PrintState(thread1); // 停止线程 Console.WriteLine("现在停止线程"); thread1.Abort(); // 给线程足够的时间来终止,否则的话可能是AbortRequested Thread.Sleep(1000); // 这时状态:Stopped PrintState(thread1); Console.WriteLine("------------------------------"); Console.WriteLine("开始测试线程2"); // 初始化一个线程 thread2 Thread thread2 = new Thread(Work2); // 这时状态:UnStarted PrintState(thread2); // 启动线程 thread2.Start(); Thread.Sleep(2 * 1000); // 这时状态:WaitSleepJoin PrintState(thread2); // 给线程足够的时间结束 Thread.Sleep(10 * 1000); // 这时状态:Stopped PrintState(thread2); Console.ReadKey(); } // 普通线程方法:一直在运行从未被超越 private static void Work1() { Console.WriteLine("线程运行中..."); // 模拟线程运行,但不改变线程状态 // 采用忙等状态 while (true) { } } // 文艺线程方法:运行10s就结束 private static void Work2() { Console.WriteLine("线程开始睡眠:"); // 睡眠10s Thread.Sleep(10 * 1000); Console.WriteLine("线程恢复运行"); } // 打印线程的状态 private static void PrintState(Thread thread) { Console.WriteLine("线程的状态是:{0}", thread.ThreadState.ToString()); } }
上述代码的执行结果如下图所示:
PS:为了演示方便,上述代码刻意地使线程处于各个状态并打印出来。在.NET Framework 4.0 及之后的版本中,已经不再鼓励使用线程的挂起状态,以及Suspend和Resume方法了。
2.2 如何使用.NET中的线程池?
(1).NET中的线程池是神马
我们都知道,线程的创建和销毁需要很大的性能开销,在Windows NT内核的操作系统中,每个进程都会包含一个线程池。而在.NET中呢,也有自己的线程池,它是由CLR负责管理的。
线程池相当于一个缓存的概念,在该池中已经存在了一些没有被销毁的线程,而当应用程序需要一个新的线程时,就可以从线程池中直接获取一个已经存在的线程。相对应的,当一个线程被使用完毕后并不会立刻被销毁,而是放入线程池中等待下一次使用。
.NET中的线程池由CLR管理,管理的策略是灵活可变的,因此线程池中的线程数量也是可变的,使用者只需向线程池提交需求即可,下图则直观地展示了CLR是如何处理线程池需求的:
PS:线程池中运行的线程均为后台线程(即线程的 IsBackground 属性被设为true),所谓的后台线程是指这些线程的运行不会阻碍应用程序的结束。相反的,应用程序的结束则必须等待所有前台线程结束后才能退出。
(2)在.NET中使用线程池
在.NET中通过 System.Threading.ThreadPool 类型来提供关于线程池的操作,ThreadPool 类型提供了几个静态方法,来允许使用者插入一个工作线程的需求。常用的有以下三个静态方法:
① static bool QueueUserWorkItem(WaitCallback callback)
② static bool QueueUserWorkItem(WaitCallback callback, Object state)
③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)
有了这几个方法,我们只需要将线程要处理的方法作为参数传入上述方法即可,随后的工作都由CLR的线程池管理程序来完成。其中,WaitCallback 是一个委托类型,该委托方法接受一个Object类型的参数,并且没有返回值。下面的代码展示了如何使用线程池来编写多线程的程序:
class Program { static void Main(string[] args) { string taskInfo = "运行10秒"; // 插入一个新的请求到线程池 bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo); // 分配线程有可能会失败 if (!result) { Console.WriteLine("分配线程失败"); } else { Console.WriteLine("按回车键结束程序"); } Console.ReadKey(); } private static void DoWork(object state) { // 模拟做了一些操作,耗时10s for (int i = 0; i < 10; i++) { Console.WriteLine("工作者线程的任务是:{0}", state); Thread.Sleep(1000); } } }
上述代码执行后,如果不输入任何字符,那么会得到如下图所示的执行结果:
PS:事实上,UnsafeQueueWorkItem方法实现了完全相同的功能,二者的差别在于UnsafeQueueWorkItem方法不会将调用线程的堆栈传递给辅助线程,这就意味着主线程的权限限制不会传递给辅助线程。UnsafeQueueWorkItem由于不进行这样的传递,因此会得到更高的运行效率,但是潜在地提升了辅助线程的权限,也就有可能会成为一个潜在的安全漏洞。
2.3 如何查看和设置线程池的上下限?
线程池的线程数是有限制的,通常情况下,我们无需修改默认的配置。但在一些场合,我们可能需要了解线程池的上下限和剩余的线程数。线程池作为一个缓冲池,有着其上下限。在通常情况下,当线程池中的线程数小于线程池设置的下限时,线程池会设法创建新的线程,而当线程池中的线程数大于线程池设置的上限时,线程池将销毁多余的线程。
PS:在.NET Framework 4.0中,每个CPU默认的工作者线程数量最大值为250个,最小值为2个。而IO线程的默认最大值为1000个,最小值为2个。
在.NET中,通过 ThreadPool 类型提供的5个静态方法可以获取和设置线程池的上限和下限,同时它还额外地提供了一个方法来让程序员获知当前可用的线程数量,下面是这五个方法的签名:
① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)
② static void GetMinThreads(out int workerThreads, out int completionPortThreads)
③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)
④ static bool SetMinThreads(int workerThreads, int completionPortThreads)
⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)
下面的代码示例演示了如何查询线程池的上下限阈值和可用线程数量:
class Program { static void Main(string[] args) { // 打印阈值和可用数量 GetLimitation(); GetAvailable(); // 使用掉其中三个线程 Console.WriteLine("此处申请使用3个线程..."); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); Thread.Sleep(1000); // 打印阈值和可用数量 GetLimitation(); GetAvailable(); // 设置最小值 Console.WriteLine("此处修改了线程池的最小线程数量"); ThreadPool.SetMinThreads(10, 10); // 打印阈值 GetLimitation(); Console.ReadKey(); } // 运行10s的方法 private static void Work(object o) { Thread.Sleep(10 * 1000); } // 打印线程池的上下限阈值 private static void GetLimitation() { int maxWork, minWork, maxIO, minIO; // 得到阈值上限 ThreadPool.GetMaxThreads(out maxWork, out maxIO); // 得到阈值下限 ThreadPool.GetMinThreads(out minWork, out minIO); // 打印阈值上限 Console.WriteLine("线程池最多有{0}个工作者线程,{1}个IO线程", maxWork.ToString(), maxIO.ToString()); // 打印阈值下限 Console.WriteLine("线程池最少有{0}个工作者线程,{1}个IO线程", minWork.ToString(), minIO.ToString()); Console.WriteLine("------------------------------------"); } // 打印可用线程数量 private static void GetAvailable() { int remainWork, remainIO; // 得到当前可用线程数量 ThreadPool.GetAvailableThreads(out remainWork, out remainIO); // 打印可用线程数量 Console.WriteLine("线程池中当前有{0}个工作者线程可用,{1}个IO线程可用", remainWork.ToString(), remainIO.ToString()); Console.WriteLine("------------------------------------"); } }
该实例的执行结果如下图所示:
PS:上面代码示例在不同的计算机上运行可能会得到不同的结果,线程池中的可用数码不会再初始时达到最大值,事实上CLR会尝试以一定的时间间隔来逐一地创建新线程,但这个时间间隔非常短。
2.4 如何定义线程独享的全局数据?
线程和进程最大的一个区别就在于线程间可以共享数据和资源,而进程则充分地隔离。在很多场合,即使同一进程的多个线程之间拥有相同的内存空间,也需要在逻辑上为某些线程分配独享的数据。例如,在实际开发中往往会针对一些ORM如EF一类的上下文实体做线程内唯一实例的设置,这时就需要用到下面提到的技术。
(1)线程本地存储(Thread Local Storage,TLS)
很多时候,程序员可能会希望拥有线程内可见的变量,而不希望其他线程对其进行访问和修改(传统方式中的静态变量是对整个应用程序域可见的),这就需要用到TLS的概念。所谓的线程本地存储(TLS)是指存储在线程环境块内的一个结构,用来存放该线程内独享的数据。进程内的线程不能访问不属于自己的TLS,这就保证了TLS内的数据在线程内是全局共享的,而对于线程外确实不可见的。
(2)定义和使用TLS变量
在.NET中提供了下列连个方法来存取线程独享的数据,它们都定义在System.Threading.Thread类型中:
① object GetData(LocalDataStoreSlot slot)
② void SetData(LocalDataStoreSlot slot, object data)
下面的代码示例则展示了这个机制的使用方法:
class Program { static void Main(string[] args) { Console.WriteLine("开始测试数据插槽:"); // 创建五个线程来同时运行,但是这里不适合用线程池, // 因为线程池内的线程会被反复使用导致线程ID一致 for (int i = 0; i < 5; i++) { Thread thread = new Thread(ThreadDataSlot.Work); thread.Start(); } Console.ReadKey(); } } /// <summary> /// 包含线程方法和数据插槽 /// </summary> public class ThreadDataSlot { // 分配一个数据插槽,注意插槽本身是全局可见的,因为这里的分配是在所有线程 // 的TLS内创建数据块 private static LocalDataStoreSlot localSlot = Thread.AllocateDataSlot(); // 线程要执行的方法,操作数据插槽来存放数据 public static void Work() { // 将线程ID注册到数据插槽中,一个应用程序内线程ID不会重复 Thread.SetData(localSlot, Thread.CurrentThread.ManagedThreadId); // 查看一下刚刚插入的数据 Console.WriteLine("线程{0}内的数据是:{1}",Thread.CurrentThread.ManagedThreadId.ToString(),Thread.GetData(localSlot).ToString()); // 这里线程休眠1秒 Thread.Sleep(1000); // 查看其他线程的运行是否干扰了当前线程数据插槽内的数据 Console.WriteLine("线程{0}内的数据是:{1}", Thread.CurrentThread.ManagedThreadId.ToString(), Thread.GetData(localSlot).ToString()); } }
该实例的执行结果如下图所示,从下图可以看出多线程的并行运行并没有破坏每个线程插槽内的数据,这就是TLS所提供的功能。
PS:LocalDataStoreSlot对象本身并不是线程共享的,初始化一个LocalDataStoreSlot对象意味着在应用程序域内的每个线程上都分配了一个数据插槽。
(3)ThreadStaticAttribute特性的使用
除了使用上面说到的数据槽之外,我们还有另一种方式,即ThreadStaticAttribute特性。申明了该特性的变量,会被.NET作为线程独享的数据来使用。我们可以将其理解为一种被.NET封装了的TLS机制,本质上,它仍然使用了线程环境块来存放数据。
下面的示例代码展示了ThreadStaticAttribute特性的使用:
class Program { static void Main(string[] args) { Console.WriteLine("开始测试数据插槽:"); // 创建五个线程来同时运行,但是这里不适合用线程池, // 因为线程池内的线程会被反复使用导致线程ID一致 for (int i = 0; i < 5; i++) { Thread thread = new Thread(ThreadStatic.Work); thread.Start(); } Console.ReadKey(); } } /// <summary> /// 包含线程静态数据 /// </summary> public class ThreadStatic { // 值类型的线程静态数据 [ThreadStatic] private static int threadId = 0; // 引用类型的线程静态数据 private static Ref refThreadId = new Ref(); /// <summary> /// 线程执行的方法,操作线程静态数据 /// </summary> public static void Work() { // 存储线程ID,一个应用程序域内线程ID不会重复 threadId = Thread.CurrentThread.ManagedThreadId; refThreadId.Id = Thread.CurrentThread.ManagedThreadId; // 查看一下刚刚插入的数据 Console.WriteLine("[线程{0}]:线程静态值变量:{1},线程静态引用变量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString()); // 睡眠1s Thread.Sleep(1000); // 查看其他线程的运行是否干扰了当前线程静态数据 Console.WriteLine("[线程{0}]:线程静态值变量:{1},线程静态引用变量:{2}", Thread.CurrentThread.ManagedThreadId.ToString(), threadId, refThreadId.Id.ToString()); } } /// <summary> /// 简单引用类型 /// </summary> public class Ref { private int id; public int Id { get { return id; } set { id = value; } } }
该实例的执行结果如下图所示,正如我们所看到的,对于使用了ThreadStatic特性的字段,.NET会将其作为线程独享的数据来处理,当某个线程对一个使用了ThreadStatic特性的字段进行赋值后,这个值只有这个线程自己可以看到并访问修改,该值对于其他线程时不可见的。相反,没有标记该特性的,则会被多个线程所共享。
2.5 如何使用异步模式读取一个文件?
异步模式是在处理流类型时经常采用的一种方式,其应用的领域相当广阔,包括读写文件、网络传输、读写数据库,甚至可以采用异步模式来做任何计算工作。相对于手动编写线程代码,异步模式是一个高效的编程模式。
(1)所谓异步模式是个什么鬼?
所谓的异步模式,是指在启动一个操作之后可以继续执行其他工作而不会发生阻塞。以读取文件为例,在同步模式下,当程序执行到Read方法时,需要等到读取动作结束后才能继续往下执行。而异步模式则可以简单地通知开始读取任务之后,继续其他的操作。 异步模式的优点就在于不需要使当前线程等待,而可以充分地利用CPU时间。
PS:异步模式区别于线程池机制的地方在于其允许程序查看操作的执行状态,而如果利用线程池的后台线程,则无法确切地知道操作的进行状态以及其是否已经结束。
使用异步模式可以通过一些异步聚集技巧来查看异步操作的结果,所谓的聚集技巧是指查看操作是否结束的方法,常用的方式是:在调用BeingXXX方法时传入操作结束后需要执行的方法(又称为回调方法),同时把执行异步操作的对象传入以便执行EndXXX方法。
(2)使用异步模式读取一个文件
下面的示例代码中:
① 主线程中负责开始异步读取并传入聚集时需要使用的方法和状态对象:
partial class Program { // 测试文件 private const string testFile = @"C:\AsyncReadTest.txt"; private const int bufferSize = 1024; static void Main(string[] args) { // 删除已存在文件 if (File.Exists(testFile)) { File.Delete(testFile); } // 写入一些东西以便后面读取 using (FileStream stream = File.Create(testFile)) { string content = "我是文件具体内容,我是不是帅得掉渣?"; byte[] contentByte = Encoding.UTF8.GetBytes(content); stream.Write(contentByte, 0, contentByte.Length); } // 开始异步读取文件具体内容 using (FileStream stream = new FileStream(testFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.Asynchronous)) { byte[] data = new byte[bufferSize]; // 将自定义类型对象实例作为参数 ReadFileClass rfc = new ReadFileClass(stream, data); // 开始异步读取 IAsyncResult result = stream.BeginRead(data, 0, data.Length, FinshCallBack, rfc); // 模拟做了一些其他的操作 Thread.Sleep(3 * 1000); Console.WriteLine("主线程执行完毕,按回车键退出程序"); } Console.ReadKey(); } }
② 定义了完成异步操作读取之后需要调用的方法,其逻辑是简单地打印出文件的内容:
partial class Program { /// <summary> /// 完成异步操作后的回调方法 /// </summary> /// <param name="result">状态对象</param> private static void FinshCallBack(IAsyncResult result) { ReadFileClass rfc = result.AsyncState as ReadFileClass; if (rfc != null) { // 必须的步骤:让异步读取占用的资源被释放掉 int length = rfc.stream.EndRead(result); // 获取读取到的文件内容 byte[] fileData = new byte[length]; Array.Copy(rfc.data, 0, fileData, 0, fileData.Length); string content = Encoding.UTF8.GetString(fileData); // 打印读取到的文件基本信息 Console.WriteLine("读取文件结束:文件长度为[{0}],文件内容为[{1}]", length.ToString(), content); } } }
③ 定义了作为状态对象传递的类型,这个类型对所有需要传递的数据包进行打包:
/// <summary> /// 传递给异步操作的回调方法 /// </summary> public class ReadFileClass { // 以便回调方法中释放异步读取的文件流 public FileStream stream; // 文件内容 public byte[] data; public ReadFileClass(FileStream stream,byte[] data) { this.stream = stream; this.data = data; } }
下图展示了该实例的执行结果:
如上面的实例,使用回调方法的异步模式需要花费一点额外的代码量,因为它需要将异步操作的对象及操作的结果数据都打包到一个类型里以便能够传递回给回调的委托方法,这样在委托方法中才能够有机会处理操作的结果,并且调用EndXXX方法以释放资源。
2.6 如何阻止线程执行上下文的传递?
(1)何为线程的执行上下文
在.NET中,每一个线程都会包含一个执行上下文,执行上下文是指线程运行中某时刻的上下文概念,类似于一个动态过程的快照(SnapShot)。在.NET中,System.Threading中的ExecutionContext类型代表了一个执行上下文,该执行上下文会包含:安全上下文、调用上下文、本地化上下文、事务上下文和CLR宿主上下文等等。通常情况下,我们将所有这些综合成为线程的上下文。
(2)执行上下文的流动
当程序中新建一个线程时,执行上下文会自动地从当前线程流入到新建的线程之中,这样做可以保证新建的线程天生就就有和主线程相同的安全设置和文化等设置。下面的示例代码通过修改安全上下文来展示线程上下文的流动性,主要使用到ExecutionContext类的Capture方法来捕获当前想成的执行上下文。
① 首先定义一些辅助犯法,封装了文件的创建、删除和文件访问权限检查:
partial class Program { private static void CreateTestFile() { if (!File.Exists(testFile)) { FileStream stream = File.Create(testFile); stream.Dispose(); } } private static void DeleteTestFile() { if (File.Exists(testFile)) { File.Delete(testFile); } } // 尝试访问测试文件来测试安全上下文 private static void JudgePermission(object state) { try { // 尝试访问文件 File.GetCreationTime(testFile); // 如果没有异常则测试通过 Console.WriteLine("权限测试通过"); } catch (SecurityException) { // 如果出现异常则测试通过 Console.WriteLine("权限测试没有通过"); } finally { Console.WriteLine("------------------------"); } } }
② 其次在入口方法中使主线程和创建的子线程访问指定文件来查看权限上下文流动到子线程中的情况:(这里需要注意的是由于在.NET 4.0及以上版本中FileIOPermission的Deny方法已过时,为了方便测试,将程序的.NET版本调整为了3.5)
partial class Program { private const string testFile = @"C:\TestContext.txt"; static void Main(string[] args) { try { CreateTestFile(); // 测试当前线程的安全上下文 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 创建一个子线程 subThread1 Console.WriteLine("子线程权限测试:"); Thread subThread1 = new Thread(JudgePermission); subThread1.Start(); subThread1.Join(); // 现在修改安全上下文,阻止文件访问 FileIOPermission fip = new FileIOPermission(FileIOPermissionAccess.AllAccess, testFile); fip.Deny(); Console.WriteLine("已成功阻止文件访问"); // 测试当前线程的安全上下文 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 创建一个子线程 subThread2 Console.WriteLine("子线程权限测试:"); Thread subThread2 = new Thread(JudgePermission); subThread2.Start(); subThread2.Join(); // 现在修改安全上下文,允许文件访问 SecurityPermission.RevertDeny(); Console.WriteLine("已成功恢复文件访问"); // 测试当前线程安全上下文 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 创建一个子线程 subThread3 Console.WriteLine("子线程权限测试:"); Thread subThread3 = new Thread(JudgePermission); subThread3.Start(); subThread3.Join(); Console.ReadKey(); } finally { DeleteTestFile(); } } }
该实例的执行结果如下图所示,从图中可以看出程序中通过FileIOPermission对象来控制对主线程对文件的访问权限,并且通过新建子线程来查看主线程的安全上下文的改变是否会影响到子线程。
正如刚刚说到,主线程的安全上下文将作为执行上下文的一部分由主线程传递给子线程。
(3)阻止上下文的流动
有的时候,系统需要子线程拥有新的上下文。抛开功能上的需求,执行上下文的流动确实使得程序的执行效率下降很多,线程上下文的包装是一个成本较高的工作,而有的时候这样的包装并不是必须的。在这种情况下,我们如果需要手动地防止线程上下文的流动,常用的有下列两种方法:
① System.Threading.ThreadPool类中的UnsafeQueueUserWorkItem方法
② ExecutionContext类中的SuppressFlow方法
下面的代码示例展示了如何使用上面两种方法阻止执行上下文的流动:
partial class Program { private const string testFile = @"C:\TestContext.txt"; static void Main(string[] args) { try { CreateTestFile(); // 现在修改安全上下文,阻止文件访问 FileIOPermission fip = new FileIOPermission(FileIOPermissionAccess.AllAccess, testFile); fip.Deny(); Console.WriteLine("已成功阻止文件访问"); // 主线程权限测试 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 使用UnsafeQueueUserWorkItem方法创建一个子线程 Console.WriteLine("子线程权限测试:"); ThreadPool.UnsafeQueueUserWorkItem(JudgePermission, null); Thread.Sleep(1000); // 使用SuppressFlow方法 using (var afc = ExecutionContext.SuppressFlow()) { // 测试当前线程安全上下文 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 创建一个子线程 subThread1 Console.WriteLine("子线程权限测试:"); Thread subThread1 = new Thread(JudgePermission); subThread1.Start(); subThread1.Join(); } // 现在修改安全上下文,允许文件访问 SecurityPermission.RevertDeny(); Console.WriteLine("已成功恢复文件访问"); // 测试当前线程安全上下文 Console.WriteLine("主线程权限测试:"); JudgePermission(null); // 创建一个子线程 subThread2 Console.WriteLine("子线程权限测试:"); Thread subThread2 = new Thread(JudgePermission); subThread2.Start(); subThread2.Join(); Console.ReadKey(); } finally { DeleteTestFile(); } } }
该实例的执行结果如下图所示,可以看出,通过前面的两种方式有效地阻止了主线程的执行上下文流动到新建的线程之中,这样的机制对于性能的提高有一定的帮助。
出处:http://edisonchou.cnblogs.com