stand on the shoulders of giants

Threading in C# - Getting Started

1. Overview

进程是可执行单元,进程由线程组成。进程之间彼此独立,线程共用进程的堆内存,线程有独立的栈内存。现有的系统一般是多进程,多线程的。

a. How threading works?

线程由thread scheduler来管理,由系统分成time-slicing快速轮转执行(rapidly switching execution between each of the active threads)
注意:block的线程是不消耗CPU时间的。XP系统中Time-slicing的级别是数十毫秒的,轮转消耗的级别是几微妙。

b. When to use?

简单的来说,就是需要后台执行耗时任务。
MSDN这样建议:
---“UI 线程即主线程不应该用于执行任何耗时较长的操作,惯常做法是,任何耗时超过 30ms 的操作都要考虑从 UI 线程中移除”。 
---“如果鼠标单击和相应的 UI 响应(例如,重新绘制)之间的延迟超过 30ms,那么操作与显示之间就会稍显不连贯,并因此产生如同影片断帧那样令人心烦的感觉。为了达到完全高质量的响应效果,上限必须是 30ms。另一方面,如果您确实不介意感觉稍显不连贯,但也不想因为停顿过长而激怒用户,则可按照通常用户所能容忍的限度将该间隔设为 100ms。”
---“则任何阻塞操作都应该在辅助线程中执行 — 不管是机械等待某事发生(例如,等待 CD-ROM 启动或者硬盘定位数据),还是等待来自网络的响应。”
---“如下图上部分,UI 是个单线程程序,单线程不可能在等待 CD-ROM驱动器读取操作的同时处理用户输入,所以UI会没有响应一段时间;像这样耗时较长的任务就可以在其自己的线程中运行,这些线程通常称为Worker Thread。因为只有Worker Thread受到阻止,所以阻塞操作不再导致用户界面冻结。应用程序的主线程可以继续处理用户的鼠标和键盘输入的同时,受阻的另一个线程将等待 CD-ROM 读取,或执行Worker Thread可能做的任何耗时操作。

c. When not to use threads

多线程的缺点就是使你的程序复杂,易出错。当然,过度使用也会有resource and CPU cost in allocating and switching threads
例如,如果有频繁的I/O操作时,如果用单或者两个线程还优于多线程。

d. Samples

        static void Main(string[] args)
        {
            Thread t 
= new Thread(WriteX);
            t.Start();
            
            
while (true)
                Console.Write(
"Y");
        }

        
static void WriteX()
        {
            
while(true)
                Console.Write(
"X");
        }

程序输出是XXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYYYYYYYXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYY
形象说明了线程以时间片快速轮转的方式执行。

// 线程安全
// 线程之间通过共享字段(实例,静态)来共享数据,此时要注意线程安全
// 线程公共字段:实例字段,静态字段
// Done一般只被打印一次
class Program
    {
       
bool done;

        
static void Main(string[] args)
        {
            Program p 
= new Program();
            Thread t 
= new Thread(p.Go);   //调用实例方法,共享实例字段
            t.Start();
            p.Go();              
        }

        
void Go()
        {
            
while (!done)
            { 
                done 
= true
                Console.Write(
"Done");                               
            }
        }       
    }

//或者
class Program
    {
       
static bool done;                 // 静态字段被线程共享

        
static void Main(string[] args)
        {            
            Thread t 
= new Thread(Go);
            t.Start();
            Go();              
        }

        
static void Go()
        {
            
while (!done)
            {                
                done 
= true
                Console.Write(
"Done");                
            }
        }

如果在done=true前增加 System.Threading.Thread.Sleep(100)模拟一个work; Done被打印两次。
这是因为一个快一点的线程进入while后工作sleep,另一个线程进入while并且得到true

这就是线程安全,如何保证任何时间只有一个线程进入critical section of code ?
解决方法:当读写公共字段的时候,使用排它锁 exclusive lock

class ThreadSafe {
  
static bool done;
  
static object locker = new object();
 
  
static void Main() {
    
new Thread (Go).Start();
    Go();
  }
 
  
static void Go() {
    
lock (locker) {
      
if (!done) { Console.WriteLine ("Done"); done = true; }
    }
  }
}
// 当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被block, until the lock becomes available


除了用排他锁使线程暂停之外,还可以用Thread.Sleep(TimeSpan.FromSeconds (30));   
或者是Join method:
Thread t = new Thread (Go);  // Assume Go is some static method
t.Start();
t.Join();                                // Wait (block) until thread t ends

2. Creating and Starting Threads

a. 一般

线程由Thread类的构造函数创建,传入ThreadStart委托实例,调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。
public delegate void ThreadStart();

class ThreadTest {
  
static void Main() {
    Thread t 
= new Thread (new ThreadStart (Go));
    t.Start();   
// Run Go() on the new thread.
    Go();        // Simultaneously run Go() in the main thread.
  }
  
static void Go() { Console.WriteLine ("hello!"); }

 

b. Using C# shortcut syntax for instantiating delegates:

static void Main() {
  Thread t 
= new Thread (Go);    // No need to explicitly use ThreadStart
  t.Start();
  
}
static void Go() {  }

在这种情况,ThreadStart委托被编译器自动推断出来,另一个快捷的方式是使用匿名方法来启动线程:

c. 匿名方法

static void Main() {
  Thread t 
= new Thread (delegate() { Console.WriteLine ("Hello!"); });
  t.Start();
}

线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。一个线程一旦结束便不能re-start了。

d. Passing data to ThreadStart

不能使用ThreadStart委托,因为它不接受参数,所幸的是,.NET framework定义了另一个版本的委托叫做ParameterizedThreadStart, 它可以接收一个单独的object类型参数:
public delegate void ParameterizedThreadStart (object obj); 

class ThreadTest {
  
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!");
  }

编译器自动推断Go是ParameterizedThreadStart委托
完整写法:
Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true);
另一种方式是匿名函数: 

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();
}
// 这里存在一个怪异的现象,如果紧跟在thread starting后改变了outer variable的值,线程内也发生变化
// 上面输出 After

//较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下列重写了原来的例子:
class ThreadTest {
  
bool upper;
 
  
static void Main() {
    ThreadTest instance1 
= new ThreadTest();
    instance1.upper 
= true;
    Thread t 
= new Thread (instance1.Go);
    t.Start();
    ThreadTest instance2 
= new ThreadTest();
    instance2.Go();        
// Main thread – runs with upper=false
  }
 
  
void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

 

c. Naming Thread

线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常。 

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);
  }
}
//  输出 
Hello from Main
Hello from worker
        

 

e. 前台线程 & 后台线程

线程默认为前台线程,任何前台线程在运行都会保持程序存活。
后台线程与前台线程类似,区别是后台线程不会阻止进程终止。
一旦属于某一进程的所有前台线程都终止,公共语言运行库就会通过对任何仍然处于活动状态的后台线程调用 Abort 来结束该线程,结束进程。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。 

一个实例:

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(),,当主线程结束程序立刻退出,后台线程Worker也被终止,终止了ReadLine

对于这种后台线程终止的这种方式:
1。 有时我们不想让后台线程这样“偷偷摸摸”的消失掉,方法就是用Thread.Join()明确等待任何后台工作线程完成后再结束程序。具体这个例子就是在最后加上worker.Join()
2。 有时我们不想让前台线程“干扰”我们结束程序:从这点来看,我们设置worker thread为后台线程是有益的!
注意:程序失败退出的普遍原因就是存在“被忘记”的前台线程。

 public Form1()
        {
            InitializeComponent();
            Thread t 
= new Thread(Go);           
            t.Start();
        }

        
private void Go()
        {
            
while (true)
            {
            }
        }


例如这个Form程序,因为有新New的前台线程,所以X掉Form后,frm是消失了,但是任务管理器中还是有这个进程。也就是说有任何前台线程运行,进程就不会结束。如果某个worker线程无法finish,可以Abort它,如果失败了,设置它die with process, 即设置为后台线程。
具体到这个例子,解决方法是:设置新创建的线程为后台:t.IsBackground = true;或者frm closing事件调用Applicaiton.Exit(0)

f. Thread Priority

线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 其实是属于最高优先级别等级的:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。

g. Exception Handling

 

public static void Main() {
  
try {
    
new Thread (Go).Start();
  }
  
catch (Exception ex) {
    
// We'll never get here!
    Console.WriteLine ("Exception!");
  }
 
  
static void Go() { throw null; }
}

新创建的线程将引发NullReferenceException异常。 这里try/catch语句一点用也没有,因为每个线程有独立的执行路径。
补救方法是在线程处理的方法内加入他们自己的异常处理:

 

public static void Main() {
   
new Thread (Go).Start();
}
 
static void Go() {
  
try {
    
    
throw null;      // this exception will get caught below
    
  }
  
catch (Exception ex) {
    记录异常,通知其他线程,这里发生错误
    
  }

 从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着不能忽略异常了。因此try/catch块需要出现在每个线程entry method内.

对于经常使用“全局”异常处理的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) {
    Log exception, then either exit the app or 
continue
  }
}

Application.ThreadException事件在异常被抛出时触发。一种虚假的安全感——所有的异常都被中央异常处理捕捉到了,一个Windows Forms程序的几乎所有代码产生的异常,都能被捕获。
但是,由工作线程抛出的异常便是一个没有被Application.ThreadException捕捉到的很好的例外。
.NET framework为全局异常处理提供了一个更低级别的事件:AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)的任何线程有任何未处理的异常触发。尽管它提供了好的不得已的异常处理解决机制,但是这不意味着这能保证程序不崩溃,也不意味着能取消.NET异常对话框。

Reference:
http://www.albahari.com/threading/
http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html

 

posted @ 2009-05-27 14:31  DylanWind  阅读(458)  评论(0编辑  收藏  举报