代码改变世界

Creating and Starting Threads

2010-12-16 13:10  RyanXiang  阅读(1116)  评论(6编辑  收藏  举报

Creating and Starting Threads(创建和运行线程)

 

    通过多线程基本概念的介绍,我们知道线程是通过传入一个ThreadStart的委托给Thread类的构造函数创建的。下面是ThreadStart类的定义。

1:  public delegate void ThreadStart(); 

 

    通过调用线程的Start方法使线程运行。线程将会一直运行到方法返回。下面是使用ThreadStart委托来创建一个线程 。

 1:  class ThreadTest
 2:   {
 3:      static void Main()
 4:      {
 5:          Thread t = new Thread(new ThreadStart(Go));
 6:   
 7:          t.Start();   // Run Go() on the new thread.
 8:          Go();        // Simultaneously run Go() in the main thread.
 9:      }
10:   
11:      static void Go()
12:      {
13:          Console.WriteLine("hello!");
14:      }
15:  }

 

 

    在这个例子中,线程t 调用Go()方法,几乎在同一时间主线程(main Thread)也在调用Go()方法。结果几乎同时打印出两个 “hello”。可以省略掉new ThreadStart(部分),下面这条语句与上例第5行代码是一样的—C#能推断出ThreadStart委托。


 

1:  Thread t = new Thread(Go);    // No need to explicitly use ThreadStart

 

 

    另外一种简单的写法是利用Lambda表达式或者匿名函数:

1:  static void Main()
2:  {
3:      Thread t = new Thread(() => Console.WriteLine("Hello!"));
4:      t.Start();
5:  }

 


1、给线程传递参数(Passing Data to a Thread

    给线程目标方法传递参数,最简单的方法是利用Lambda表达式:

 

 1:  static void Main()
 2:  {
 3:      Thread t = new Thread(() => Print("Hello from t!"));
 4:      t.Start();
 5:  }
 6:   
 7:  static void Print(string message)
 8:  {
 9:      Console.WriteLine(message);
10:  }

 

    通过这种方式,你可以给方法传递任意个数的参数,你甚至可以把所有的语句都包装在Lambda表达式中:

 

1:  new Thread (() =>
2:  {
3:    Console.WriteLine ("I'm running on another thread!");
4:    Console.WriteLine ("This is so easy!");
5:  }).Start();

 

    在C#2.0中,你可以利用匿名函数做同样的事情:


1:  new Thread (delegate()
2:  {
3:    ...
4:  }).Start();

 

    另外一种传递参数的方法是通过Thread’s Start 方法:

 

 1:  static void Main()
 2:  {
 3:      Thread t = new Thread(Print);
 4:      t.Start("Hello from t!");
 5:  }
 6:   
 7:  static void Print(object messageObj)
 8:  {
 9:      string message = (string)messageObj;   // We need to cast here
10:      Console.WriteLine(message);
11:  } 

 

    上述代码之所以能够运行,是因为Thread的构造函数被重载成支持下面两种类型的委托:

1:  public delegate void ThreadStart();
2:  public delegate void ParameterizedThreadStart(object obj);

 

    ParameterizedThreadStart 传递参数个数有限制,只能接收一个参数,并且由于其类型为object,通常需要转型。


 

Lambda expressions and captured variables

   如上所述,Lambda 表达式向thread传递参数的方式很强大, 然而在线程启动后你必须小心对待变量的修改,因为这些变量是共享的,思考下面这段代码: 

1:  for (int i = 0; i < 10; i++)
2:    new Thread (() => Console.Write (i)).Start();

 

   输出时很神奇的!下面是可能出现的结果:

飞信截屏未命名

    出现这种情况的原因是由于变量i在整个循环的生命周期内,引用的是同一部分内存。因此每个线程在调用 Console.Write 时,值都可能在运行的时候改变!

 

 

    利用临时变量可以解决这一问题:

1:  for (int i = 0; i < 10; i++)
2:  {
3:    int temp = i;
4:    new Thread (() => Console.Write (temp)).Start();
5:  }

 

    变量temp是循环体内的局部变量。因此,每个thread都获得不同栈空间,这样是没有问题的。我们可以用更简单的方法来阐述这个问题如下:

1:  string text = "t1";
2:  Thread t1 = new Thread ( () => Console.WriteLine (text) );
3:   
4:  text = "t2";
5:  Thread t2 = new Thread ( () => Console.WriteLine (text) );
6:   
7:  t1.Start();
8:  t2.Start();

 

    因为两个Lambda表达式共享text变量,所以t2被打印了两次。

2

 

2、 给线程设置名称 (Naming Threads

    每个线程都有一个Name属性,我们在调试的时候可能会用到。你可以给线程设置一个名称,在程序抛出异常的时候改变它。这个属性在VS中特别有用,你可以在调试窗口中看到线程名称。

    静态方法Thread.CurrentThread 可以取得当前正在运行的线程,在下面这个例子我们将会给主线程设置一个名称:

 1:  class ThreadNaming
 2:  {
 3:    static void Main()
 4:    {
 5:      Thread.CurrentThread.Name = "main";
 6:      Thread worker = new Thread (Go);
 7:      worker.Name = "worker";
 8:      worker.Start();
 9:      Go();
10:    }
11:   
12:    static void Go()
13:    {
14:      Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
15:    }
16:  }

 

 

3、 前台线程和后台线程 (Foreground and Background Threads ) 

默认的情况下,所有被创建的线程都是前台线程。后台线程不会使托管执行环境处于运行状态,除此之外,后台线程与前台线程是一样的。

一旦所有前台线程在托管进程(其中 .exe 文件是托管程序集)中被停止,系统将停止所有后台线程并关闭。


前台线程和后台线程与线程的优先级和CPU的执行时间无关。

 

你可以利用IsBackground属性来查询一个线程是不是后台线程也可以将前台线程转换为后台线程。例如:

 1:  class PriorityTest
 2:  {
 3:    static void Main (string[] args)
 4:    {
 5:      Thread worker = new Thread ( () => Console.ReadLine() );
 6:      if (args.Length > 0) worker.IsBackground = true;
 7:      worker.Start();
 8:    }
 9:  }

 

 

    上面代码中如果Main没有传入参数,前台线程worker会继续等待用户输入,同时主线程执行结束,但应用程序会继续运行,因为仍有一个前台线程处于运行状态。如果传入一个参数给Main函数,前台线程将会被转换为后台线程,程序几乎会和主线程一起结束。
 

      当进程以这种方式终止了,所有的后台线程中的finallly块都会终止。如果你的finally快执行清理工作如释放资源或者删除临时文件,这时就会有问题产生。如果在应用程序中存在后台线程,为了避免问题出现,你可以采用如下两种方式来解决:

  • 如果你是自己创建的线程可以调用Join方法。
  • 如果你是使用线程池,可以利用event wait handle来避免这种情况。

    在任何一种情况下,你应该设置超时时间,因此这种方法让你能够放弃一些因为某些原因永远不会结束的线程。这是你的备选策略:在最后你想关闭你的应用程序而不是通过任务管理器来寻求帮助!

 

如果用户使用任务管理器来强制结束一个.Net进程,那么所有的线程都会向后台线程一样被  ”drop dead”,这是被观察到而不是被记录的,并且它很大程度依赖于系统和CLR的版本。

   虽然前台线程不需要做这样的处理,但是你必须保证没有bug会导致线程无法正常结束。通常导致应用程序无法正常退出的原因是存在一个正在运行的前台线程。

 

4、线程优先级Thread Priority

 

   一个线程的优先级决定了该线程相对于其它活动线程能在操作系统中获得执行时间大小的多少,下面是优先级列表

1:  enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest } 

 

 

在提高线程的优先级时,应该仔细考虑清楚---因为有可能引发一些问题,比如:让其它线程由于无法获得CPU资源从而僵死。

    提高线程的优先级,并不一定能让你的应用程序能够胜任实时的工作,因为它还受进程的优先级的控制,你必须利用System.Diagnostics 的Process类来提高进程的优先级。(我们不在这里讨论):

1:  using (Process p = Process.GetCurrentProcess())
2:  p.PriorityClass = ProcessPriorityClass.High;

 

    ProcessPriorityClass.High 实际上是最高的优先级:实时运行。把一个进程设置成实时运行的优先级就意味着它将独占CPU的执行时间。如果你的程序意外进入了死循环,你也许会发现系统将会被锁死,如果你想继续使用系统除了按下电源按钮别无他法!由于这个原因,High优先级通常是在实时系统中使用。

    如果在实时系统中存在用户界面,提升进程优先级给界面更新更多CPU时间,将是整台电脑变慢(特别是界面比较复杂时)。降低主线程优先级的同时提高进程的优先级可以确保实时运行进程不会被屏幕刷新进程抢占,但是不能解决其他应用程序获取不到CPU时间的问题,因为OS将从整体上按比例给进程分配资源。理想的解决方案是让实时处理任务和用户界面以不同的进程优先级分别运行在单独的应用程序中,通过Remoting或者memory-mapped 文件通信。Memory-mapped文件非常适合这个任务;它们是如何工作的可以参考 C# 4.0 in a Nutshell的14章和25章。

    在托管的环境下,通过提升进程优先级来处理实时系统的复杂需求还是有一定的局限性的。除了垃圾回收机制有延时的问题以外,OS还会面临一些额外的挑战,即便对非托管的应用程序,最好的解决方案也是使用特定的硬件或者实时运行平台来实现。

5、异常处理(Exception Handling 

 

try/catch/finally 的作用范围仅在当前线程内。考虑下面的程序:

 1:  public static void Main()
 2:  {
 3:    try
 4:    {
 5:      new Thread (Go).Start();
 6:    }
 7:    catch (Exception ex)
 8:    {
 9:      // We'll never get here!
10:      Console.WriteLine ("Exception!");
11:    }
12:  }
13:   
14:  static void Go() { throw null; }   // Throws a NullReferenceException

 


try/catch 语句在这个例子中没有起作用,新创建的线程阻碍了NullReferenceException异常的处理。这种行为更确认了每个线程都有一个独立的执行路径(execution path)的说法。

 

你可以用下面的方式来处理上面的异常:

 

 1:  public static void Main()
 2:  {
 3:     new Thread (Go).Start();
 4:  }
 5:   
 6:  static void Go()
 7:  {
 8:    try
 9:    {
10:      // ...
11:      throw null;    // The NullReferenceException will get caught below
12:      // ...
13:    }
14:    catch (Exception ex)
15:    {
16:      // Typically log the exception, and/or signal another thread
17:      // that we've come unstuck
18:      // ...
19:    }
20:  }

 

   在实际产品应用程序中,你应该在所有线程的入口做异常处理---就像在你的主线程做的一样。因为一个未处理的异常可能会让整个应用都无法继续运行,还会呈现给用户一个看不懂的页面,让用户满意度下降。

 

在编写这些异常处理块时,你很少会忽视错误:通常地,你把异常细节记录进日志,或者提示对话框允许用户自动提交这些信息到你的web服务器。你或许可以关闭应用程序 - 因为可能错误已经腐蚀了应用程序状态。然而,关闭应用程序的成本是用户将丢掉最近的工作 - 举个例子来说,用户正在打开文档。

 

 

针对WPF和Winform程序的"global"异常处理事件仅仅把异常抛出到主UI线程。你仍然必须手工处理工作线程上的异常。

AppDomain.CurrentDomain.UnhandledException处理未处理异常,但是不会避免应用程序关闭。

   然而,有些时候你并需要为线程增加异常处理,因为.Net Framework已经为你做了。这些包括我们即将要介绍的部分:

  • Asynchronous delegates
  • BackgroundWorker
  • Task Parallel Library