c# 线程第四部分:高级话题3

来源:互联网 发布:linux vim 配置 编辑:程序博客网 时间:2024/05/18 03:20
2008-05-11 01:26

终止线程

一个线程可以通过Abort方法被强制终止:

class Abort {
  static void Main() {
     Thread t = new Thread (delegate() {while(true);});    // 永远轮询
     t.Start();
     Thread.Sleep (1000);         // 让它运行几秒...
     t.Abort();                   // 然后终止它
  }
}

线程在被终止之前立即进入AbortRequested 状态。如果它如预期一样终止了,它进入Stopped状态。调用者可以通过调用Join来等待这个过程完成:

class Abort {
  static void Main() {
     Thread t = new Thread (delegate() { while (true); });
     Console.WriteLine (t.ThreadState);      // Unstarted
 
     t.Start();
     Thread.Sleep (1000);
     Console.WriteLine (t.ThreadState);      // Running
 
     t.Abort();
     Console.WriteLine (t.ThreadState);      // AbortRequested
 
     t.Join();
     Console.WriteLine (t.ThreadState);      // Stopped
  }
}

Abort引起ThreadAbortException 异常被抛出在目标线程上,大多数情况下就使线程执行的那个时候。线程被终止可以选择处理异常,但是异常会自动在catch语句最后被重新抛出(来帮助保证线程确实如期望的结束了)。尽管如此,可能避免自动地重新抛出通过调用Thread.ResetAbort在catch语句块内。线程然后重新进入Running 状态(由于它可能潜在被又一次终止)。在下面的例子中,工作线程每次从死恢复回来每当Abort试图终止的时候:

class Terminator {
  static void Main() {
     Thread t = new Thread (Work);
     t.Start();
     Thread.Sleep (1000); t.Abort();
     Thread.Sleep (1000); t.Abort();
     Thread.Sleep (1000); t.Abort();
  }
 
  static void Work() {
     while (true) {
       try { while (true); }
       catch (ThreadAbortException) { Thread.ResetAbort(); }
       Console.WriteLine ("I will not die!");
     }
  }
}

ThreadAbortException被运行时处理过,导致它当没有处理时不会引起整个程序结束,而不像其它类型的线程。

Abort几乎对处于任何状态的线程都有效, running,blocked,suspended或 stopped。尽管挂起的线程会失败,但ThreadStateException会被抛出,这时在正调用的线程中,异常终止不会踢开直到线程随后恢复,这演示了如何终止一个挂起的线程:

try { suspendedThread.Abort(); }
catch (ThreadStateException) { suspendedThread.Resume(); }
//现在suspendedThread将被终止

Thread.Abort的复杂因素

假设一个被终止的线程没有调用ResetAbort,你可能期待它正确地并迅速地结束。但是争先它所发生的那样,懂法规的线程可能驻留在死那行一段时间!有一点原因可能保持它延迟在AbortRequested状态:

  • 静态类的构造器执行一半是不能被终止的(免得可能破坏类对于程序域内的生命周期)
  • 所有的catch/finally语句块被尊重,不会在这期间终止
  • 如果线程正执行到非托管的代码时进行终止,执行会继续直到下次进入托管的代码中

最后因素尤为麻烦,.NET framework本身提供了调用非托管的代码,有时候会持续很长的时间。一个例子就是当使用网络或数据库类的时候。如果网资源或数据库死了或很慢的相应,有可能执行完全的保留在非托管的代码中,至于多长时间依赖于类的实现。在这些情况,确定你不能用 Join 来终止线程——至少不能没有超时!

终止纯.NET代码是很少有问题的,在try/finallyusing语句组成合体来保证正确时,终止发生应抛出ThreadAbortException异常。但是,即使那样,还是容易出错。比如考虑下面的代码:

using (StreamWriter w = File.CreateText ("myfile.txt"))
  w.Write ("Abort-Safe?");

C#using语句是简洁地语法操作,可以扩充为下面的:

StreamWriter w;
w = File.CreateText ("myfile.txt");
try      { w.Write ("Abort-Safe"); }
finally { w.Dispose();             }  

有可能Abort引发在StreamWriter创建之后,但是在try 之前,实际上根据挖掘IL,你可以看到也有可能它引发在 StreamWriter被创建和赋值给w之间:

IL_0001:  ldstr       "myfile.txt"
IL_0006:  call        class [mscorlib]System.IO.StreamWriter
                      [mscorlib]System.IO.File::CreateText(string)
IL_000b:  stloc.0
.try
{
  ...

无论那种,在finally中的Dispose方法,导致抛弃了打开文件的句柄 ——阻止了任何后来的试图创建myfile.txt 直到应用程序域结束。

实际上,这个例子情况可能更复杂,因为Abort可能发证在实现File.CreateText中。这引用了不透明的代码——我们没有的代码。幸运的是,.NET代码从来没有真正的不透明:我们可以再次滚动ILDASM,或更好的用 Lutz Roeder的 Reflector ,找到framework的汇编,看到它调用 StreamWriter的构造器,有如下的逻辑:

public StreamWriter (string path, bool append, ...)
{
  ...
  ...
  Stream stream1 = StreamWriter.CreateFile (path, append);
  this.Init (stream1, ...);
}

这个构造函数里无处有try/catch语句,意味着如果Abort发生在(非平凡)Init方法内,最近创建的流将被抛弃,绝不会关闭最近的文件句柄。 Nowhere in this constructor is there a try/catch block, meaning that if the Abort fires anywhere within the (non-trivial) Init method, the newly created stream will be abandoned, with no way of closing the underlying file handle.

因为反编译每个请求CLR的调用是不现实的,这就出现了你如何着手写一个“有好终止”的方法。最普遍的是方式根本就不要终止另一个线程——但除了增加一个自定义的布尔字段在工作线程类里,告诉它应该终止。工作线程周期性检查这个标志,如果为true就温和地退出。令人讽刺的是,最温和的退出工作线程是通过调用在它自己的线程上调用Abort——尽管明确地抛出异常,也可以很好的工作。这确保了线程正常终止,在执行任何catch/finally语句的时候——相当像从另一个线程调用终止,除了异常是在设计的地方抛出的:

class ProLife {
  public static void Main() {
     RulyWorker w = new RulyWorker();
     Thread t = new Thread (w.Work);
     t.Start();
     Thread.Sleep (500);
     w.Abort();
  }
 
  public class RulyWorker {
     // The volatile keyword ensures abort is not cached by a thread
     volatile bool abort;  
 
     public void Abort() { abort = true; }
 
     public void Work() {
       while (true) {
         CheckAbort();
         // Do stuff...
         try       { OtherMethod(); }
         finally  { /* any required cleanup */ }
       }
     }
 
     void OtherMethod() {
      // Do stuff...
       CheckAbort();
     }
 
     void CheckAbort() { if (abort) Thread.CurrentThread.Abort(); }
  }
}
 

某个线程本身上调用终止是完全安全的。另一个是你的终止使用了一段特别的代码,通常是用同步机制比如Wait Handle 或 Monitor.Wait。第三个终止线程是安全是是你随后终止线程所在的程序域或进程。

结束应用程序域

另一个方式实现有好的终止工作线程是通过终止持有它的应用程序域。在调用 Abort后,你简单地销毁应用程序域,因此释放了任何不正确引用的资源。

严格地来讲,第一步——终止线程——是不必要的,因为当一个应用程序域卸载之后,所有期内线程都被终止了。尽管如此,依赖这个特性的缺点是如果被终止的线程没有即时的退出(可能归咎于finally的代码,或之前讨论的理由)应用程序域不会卸载,CannotUnloadAppDomainException异常将被抛出。因此最好明确终止线程,然后在卸载应用程序域之前带着超时参数(受你所控)调用Join方法。

在下面的例子里,工作线程访问一个死循环,使用非终止安全的File.CreateText方法创建并关闭一个文件。主线程然后重复地开始和终止工作线程。它总是在一或两次迭代中失败,CreateText在获取了终止部分通过它的内部实现机制,留下了一个被抛弃的打开文件的句柄:

using System;
using System.IO;
using System.Threading;
 
class Program {
  static void Main() {
     while (true) {
       Thread t = new Thread (Work);
       t.Start();
       Thread.Sleep (100);
       t.Abort();
       Console.WriteLine ("Aborted");
     }
  }
 
  static void Work() {
     while (true)
       using (StreamWriter w = File.CreateText ("myfile.txt")) { }
  }
}

Aborted
Aborted
IOException: The process cannot access the file 'myfile.txt' because it
is being used by another process.

下面是一个经修改类似的例子,工作线程在它自己的应用程序域中运行,应用程序域在线程被终止后被卸载掉。它会永远的运行而没有错误,因为卸载应用程序域释放了被抛弃的文件句柄:

class Program {
  static void Main (string [] args) {
     while (true) {
       AppDomain ad = AppDomain.CreateDomain ("worker");
       Thread t = new Thread (delegate() { ad.DoCallBack (Work); });
       t.Start();
       Thread.Sleep (100);
       t.Abort();
       if (!t.Join (2000)) {
         // 线程不会结束——这里我们可以放置一些操作,
         // 如果,实际上,我们不能做任何事,幸运地是
         // 这种情况,我们期待*线程*总是能结束。
       }
       AppDomain.Unload (ad);             // 卸载“受污染”的应用程序域
       Console.WriteLine ("Aborted");
     }
  }
 
  static void Work() {
     while (true)
       using (StreamWriter w = File.CreateText ("myfile.txt")) { }
  }
}

Aborted
Aborted
Aborted
Aborted
...
...

创建和结束一个应用程序域在线程的世界里是被分类到相关耗时的操作的(数毫秒),所以应该不定期的使用它,而不是把它放入循环中。同时,实行分离,由应用程序域推出的另一项内容可以带来有利或不利,这取决于多线程程序展示出来的实现。在单元测试方面,比如,在分离的应用程序域中运行线程,可以带来极大的好处。

结束进程

另一个线程结束的方式是通过它的父进程被终止掉。这方面的一个例子是当工作线程的IsBackground 属性被设置为true,当工作线程还在运行的时候主线结束了。后台线程不能够保持应用程序存活,所以进程带着后台新城一起被终止。

当一个线程由于它的父进程被终止了,它突然停止,不会有finally被执行。

相同的情形在一个用户通过Windows任务管理器或一个进程被编程的方式通过Process.Kill时发生。