C#多线程实践——创建和开始使用
线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行。ThreadStart的声明如下:
public delegate void ThreadStart();
调用Start方法后,线程开始运行,直到它所调用的方法返回后结束。
class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); Go(); } static void Go() { Console.WriteLine ("hello!"); }
一个线程可以通过C#的委托简短的语法更便利地创建出来:
static void Main() { Thread t = new Thread (Go); // 不需要显式声明使用 ThreadStart t.Start(); ... } static void Go() { ... } 在这种情况,ThreadStart被编译器自动推断出来:
另一个快捷的方式是使用匿名方法来启动线程
static void Main() { Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); }); t.Start(); }
线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。一个线程一旦结束便不能重新开始了。
将数据传入ThreadStart中
假如想更好地区分开每个线程的输出结果,如让其中一个线程输出大写字母。可以考虑传入一个状态字到Go中来完成整个任务,此时就不能使用ThreadStart委托,因为它不接受参数。不过NET framework定义了另一个版本的委托叫ParameterizedThreadStart, 它可以接收一个单独的object类型参数,委托声明如下:
public delegate void ParameterizedThreadStart (object obj);
示例如下:
class ThreadDemo { static void Main() { Thread t = new Thread (Go); // 编译器自动推断 t.Start (true); // == Go (true) Go (false); } static void Go (object upperCase) { bool upper = (bool) upperCase; Console.WriteLine (upper ? "HELLO!" : "hello!"); }
在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数,就像这样写:
Thread t = new Thread (new ParameterizedThreadStart (Go)); t.Start (true);
ParameterizedThreadStart的特性是在使用之前我们必需对我们想要的类型(这里是bool)进行装箱操作,并且它只能接收一个参数。
一个替代方案是使用一个匿名方法调用一个普通的方法如下:
static void Main() { Thread t = new Thread (delegate() { WriteText ("Hello"); }); t.Start(); } static void WriteText (string text) { Console.WriteLine (text); }
优点是目标方法(这里是WriteText)可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿名方法中,向下面的一样:
static void Main() { string text = "Before"; Thread t = new Thread (delegate() { WriteText (text); }); text = "After"; t.Start(); } static void WriteText (string text) { Console.WriteLine (text); }
匿名方法出现了一种怪异的现象:当外部变量被后面的代码修改了值的时候,线程可能会通过外部变量进行无意的互动。换个角度看,有意的互动(通常通过字段)也可以采用这种方式!一旦线程开始运行了,外部变量最好被处理成只读的——除非有人愿意使用适当的锁。
另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下重写了上节的例子:
class ThreadDemo { bool upper; static void Main() { ThreadDemo instance1 = new ThreadDemo(); instance1.upper = true; Thread t = new Thread (instance1.Go); t.Start(); ThreadDemo instance2 = new ThreadDemo(); instance2.Go(); // 主线程——运行 upper=false } void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }
命名线程
线程可以通过它的Name属性进行命名,这非常有利于调试:可以用Console.WriteLine打印出线程的名字,Microsoft Visual Studio可以将线程的名字显示在调试工具栏的位置上。线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常。
程序的主线程也可以被命名,下面例子里主线程通过CurrentThread命名:
class ThreadNaming { static void Main() { Thread.CurrentThread.Name = "main"; Thread worker = new Thread (Go); worker.Name = "worker"; worker.Start(); Go(); } static void Go() { Console.WriteLine ("Hello from " + Thread.CurrentThread.Name); } }
前台和后台线程
线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。
线程的IsBackground属性控制它的前后台状态,如下实例:
class PriorityTest { static void Main (string[] args) { Thread worker = new Thread (delegate() { Console.ReadLine(); }); if (args.Length > 0) worker.IsBackground = true; worker.Start(); } }
如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着。 另一方面如果有参数传入Main(),工作线程被赋值为后台线程,当主线程结束程序立刻退出,终止了ReadLine。后台线程这种终止方式,使任何最后操作都被规避了,这是不太合适的。好的方式是明确等待任何后台工作线程完成后再结束程序,可能用一个timeout(大多用Thread.Join)。如果因为某种原因某个工作线程无法完成,可以试图终止它,如果失败了,再抛弃线程,允许它与进程一起消亡。(记录是一个难题,但在这个场景下是有意义的)
拥有一个后台工作线程是有益的,最直接的理由是结束程序时它可能有最后的发言权,与不会消亡的前台线程一起保证程序的正常退出。抛弃一个前台工作线程风险更大,尤其对Windows Forms程序,因为程序直到主线程结束时才退出(至少对用户来说),但是它的进程仍然运行着。它将从应用程序栏消失不见,但却可以在在Windows任务管理器进程栏找到它。除非手动找到并结束它,否则将继续消耗资源,并可能阻止一个新的实例的重新开始运行或影响它的特性。
对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。
线程优先级
线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
只有多个线程同时为活动时,优先级才有作用。
设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 其实是一个时间片中的最高优先级别:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。
如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。 降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)
异常处理
任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系。考虑下面的程序:
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // 不会在这得到异常 Console.WriteLine ("Exception!"); } static void Go() { throw null; } }
这里
try
/
catch
语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的。补救方法是在线程处理的方法内加入他们自己的异常处理。
public static void Main() { new Thread (Go).Start(); } static void Go() { try { ... throw null; // 这个异常在下面会被捕捉到 ... } catch (Exception ex) { 记录异常日志,并且或通知另一个线程 我们发生错误 ... }
从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃,try/catch块需要出现在每个线程进入的方法内,至少要在产品程序中应该如此。对于经常使用“全局”异常处理的Windows Forms程序员来说,这可能有点麻烦,像下面这样:
using System; using System.Threading; using System.Windows.Forms; static class Program { static void Main() { Application.ThreadException += HandleError; Application.Run (new MainForm()); } static void HandleError (object sender, ThreadExceptionEventArgs e) { 记录异常或者退出程序或者继续运行... } }
Application.ThreadException事件在异常被抛出时触发,以一个Windows信息(比如:键盘,鼠标活着 "paint" 等信息)的方式,简言之,一个Windows Forms程序的几乎所有代码。虽然这看起来很完美,它使人产生一种虚假的安全感——所有的异常都被中央异常处理捕捉到了。由工作线程抛出的异常便是一个没有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代码,包括构造器的形式,在Windows信息开始前先执行)
.NET framework为全局异常处理提供了一个更低级别的事件:AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)的任何线程有任何未处理的异常触发。尽管它提供了好的不得已的异常处理解决机制,但是这不意味着这能保证程序不崩溃,也不意味着能取消.NET异常对话框。
在产品程序中,明确地使用异常处理在所有线程进入的方法中是必要的,可以使用包装类和帮助类来分解工作来完成任务,比如使用BackgroundWorker类。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。