第二十一篇 .NET高级技术之使用多线程(三)

1.  单元模式和 Windows Forms

       单元模式线程是一个自动线程安全机制, 很是贴近于COM——Microsoft的遗留下的组件对象模型。尽管.NET最大地放弃摆脱了遗留下的模型,但不少时候它也会忽然出现,这是由于有必要 与旧的API 进行通讯。单元模式线程与Windows Forms最相关,由于大多Windows Forms使用或包装了长期存在的Win32 API——连同它的单元传统。php

       单元是多线程的逻辑上的“容器”,单元产生两种容量——“单的”和“多的”。单线 程单元只包含一个线程;多线程单元能够包含任何数量的线程。单线程模式更广泛 而且能与二者有互操做性。web

       就像包含线程同样,单元也包含对象,当对象在一个单元内被建立后,在它的生命周期中它将一直存在在那,永远也“居家不出”地与那些驻留线程在一块儿。这相似 于被包含在.NET 同步环境中 ,除了同步环境中没有本身的或包含线程。任何线程能够访问在任何同步环境中的对象 ——在排它锁的控制中。可是单元内的对象只有单元内的线程才能够访问。缓存

       想象一个图书馆,每本书都象征着一个对象;借出书是不被容许的,书都在图书馆 建立并直到它寿终正寝。此外,咱们用一我的来象征一个线程。安全

       一个同步内容的图书馆容许任何人进入,同时同一时刻只容许一我的进入,在图书馆外会造成队列。服务器

       单元模式的图书馆有常驻维护人员——对于单线程模式的图书馆有一个图书管理员, 对于多线程模式的图书馆则有一个团队的管理员。没人被容许除了隶属与维护人员的人 ——资助人想要完成研究就必须给图书管理员发信号,而后告诉管理员去作工做!给管理员发信号被称为调度编组——资助人经过调度把方法依次读出给一个隶属管 理员的人(或,某个隶属管理员的人!)。 调度编组是自动的,在Windows Forms经过信息泵被实如今库结尾。这就是操做系统常常检查键盘和鼠标的机制。若是信息到达的太快了,以至不能被处理,它们将造成消息队列,因此它们可 以以它们到达的顺序被处理。cookie

 

1.1  定义单元模式多线程

 

        .NET线程在进入单元核心Win32或旧的COM代码前自动地给单元赋值,它被默认地指定为多线程单元模式,除非须要一个单线程单元模式,就像下面的同样:并发

1
2
Thread t = new  Thread (...);
t.SetApartmentState (ApartmentState.STA);

        你也能够用STAThread特性标在主线程上来让它与单线程单元相结合:dom

1
2
3
4
class  Program {
   [STAThread]
static  void  Main() {
   ...

        线程单元设置对纯.NET代码没有效果,换言之,即便两个线程都有STA 的单元状态,也能够被相同的对象同时调用相同的方法,就没有自动的信号编组或锁定发生了, 只有在执行非托管的代码时,这才会发生。异步

在System.Windows.Forms名称空间下的类型,普遍地调用Win32代码, 在单线程单元下工做。因为这个缘由,一个Windos Forms程序应该在它的主方法上贴上 [STAThread]特性,除非在执行Win32 UI代码以前如下两者之一发生了:

  • 它将调度编组成一个单线程单元
  • 它将崩溃

 

1.2  Control.Invoke

 

在多线程的Windows Forms程序中,经过非建立控件的线程调用控件的的属性和方法是非法的。全部跨进程的调用必须被明确地排列至建立控件的线程中(一般为主线程),利用 Control.Invoke 或 Control.BeginInvoke方法。你不能依赖自动调度编组由于它发生的太晚了,仅当执行恰好进入了非托管的代码它才发生,而.NET已有足够 的时间来运行“错误的”线程代码,那些非线程安全的代码。

一个优秀的管理Windows Forms程序的方案是使用BackgroundWorker, 这个类包装了须要报道进度和完成度的工做线程,并自动地调用Control.Invoke方法做为须要。

 

1.3  BackgroundWorker

 

BackgroundWorker是一个在System.ComponentModel命名空间 下帮助类,它管理着工做线程。它提供了如下特性:

  • "cancel" 标记,对于给工做线程打信号让它结束而没有使用 Abort的状况
  • 提供报道进度,完成度和退出的标准方案
  • 实现了IComponent接口,容许它参与Visual Studio设计器
  • 在工做线程之上作异常处理
  • 更新Windows Forms控件以应答工做进度或完成度的能力

     最后两个特性是至关地有用:意味着你再也不须要将try/catch语句块放到 你的工做线程中了,而且更新Windows Forms控件不须要调用 Control.Invoke了。BackgroundWorker使用线程池工做, 对于每一个新任务,它循环使用避免线程们获得休息。这意味着你不能在 BackgroundWorker线程上调用 Abort了。

     下面是使用BackgroundWorker最少的步骤:

  • 实例化 BackgroundWorker,为DoWork事件增长委托。
  • 调用RunWorkerAsync方法,使用一个随便的object参数。

     这就设置好了它,任何被传入RunWorkerAsync的参数将经过事件参数的Argument属性,传到DoWork事件委托的方法中,下面是例子:

1
2
3
4
5
6
7
8
9
10
11
12
class  Program {
s   tatic BackgroundWorker bw = new  BackgroundWorker();
static  void  Main() {
         bw.DoWork += bw_DoWork;
         bw.RunWorkerAsync ( "Message to worker" );    
     Console.ReadLine();
   }
static  void  bw_DoWork ( object  sender, DoWorkEventArgs e) {
// 这被工做线程调用
     Console.WriteLine (e.Argument);        // 写"Message to worker"
     // 执行耗时的任务...
   }

      BackgroundWorker也提供了RunWorkerCompleted事件,它在DoWork事件完成后触发,处理 RunWorkerCompleted事件并非强制的,可是为了查询到DoWork中的异常,你一般会这么作的。RunWorkerCompleted 中的代码能够更新Windows Forms 控件,而不用显示的信号编组,而DoWork中就能够这么作。

添加进程报告支持:

  • 设置WorkerReportsProgress属性为true
  • 在DoWork中使用“完成百分比”周期地调用ReportProgress方法,以及可选用户状态对象
  • 处理ProgressChanged事件,查询它的事件参数的 ProgressPercentage属性

      ProgressChanged中的代码就像RunWorkerCompleted同样能够自由地与UI控件进行交互,这在更性进度栏尤其有用。

添加退出报告支持:

  • 设置WorkerSupportsCancellation属性为true
  • 在DoWork中周期地检查CancellationPending属性:若是为true,就设置事件参数的Cancel属性为true,而后返 回。(工做线程可能会设置Cancel为true,而且不经过CancellationPending进行提示——若是断定工做太过困难而且它不能继续运 行)
  • 调用CancelAsync来请求退出

下面的例子实现了上面描述的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using  System;
using  System.Threading;
using  System.ComponentModel;
  
class  Program {
   static  BackgroundWorker bw;
   static  void  Main() {
     bw = new  BackgroundWorker();
     bw.WorkerReportsProgress = true ;
     bw.WorkerSupportsCancellation = true ;
 
     bw.DoWork += bw_DoWork;
     bw.ProgressChanged += bw_ProgressChanged;
     bw.RunWorkerCompleted += bw_RunWorkerCompleted;
  
     bw.RunWorkerAsync ( "Hello to worker" );
     
     Console.WriteLine ( "Press Enter in the next 5 seconds to cancel" );
     Console.ReadLine();
     if  (bw.IsBusy) bw.CancelAsync();
     Console.ReadLine();
   }
  
   static  void  bw_DoWork ( object  sender, DoWorkEventArgs e) {
     for  ( int  i = 0; i <= 100; i += 20) {
       if  (bw.CancellationPending) {
         e.Cancel = true ;
         return ;
       }
       bw.ReportProgress (i);
       Thread.Sleep (1000);
     }
     e.Result = 123;    // This gets passed to RunWorkerCompleted
   }
  
   static  void  bw_RunWorkerCompleted ( object  sender,
   RunWorkerCompletedEventArgs e) {
     if  (e.Cancelled)
       Console.WriteLine ( "You cancelled!" );
     else  if  (e.Error != null )
       Console.WriteLine ( "Worker exception: "  + e.Error.ToString());
     else
       Console.WriteLine ( "Complete - "  + e.Result);      // from DoWork
   }
  
   static  void  bw_ProgressChanged ( object  sender,
   ProgressChangedEventArgs e) {
     Console.WriteLine ( "Reached "  + e.ProgressPercentage + "%" );
   }
}

image

 

1.4  BackgroundWorker的子类

  

       BackgroundWorker不是密封类,它提供OnDoWork为虚方法,暗示着另外一个模式能够它。 当写一个可能耗时的方法,你能够或最好写个返回BackgroundWorker子类的等方法,预配置完成异步的工做。使用者只要处理 RunWorkerCompleted事件和ProgressChanged事件。好比,设想咱们写一个耗时 的方法叫作GetFinancialTotals:

1
2
3
4
5
public  class  Client {
   Dictionary < string , int > GetFinancialTotals ( int  foo, int  bar) { ... }
   ...
 
}

      咱们能够如此来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public  class  Client {
   public  FinancialWorker GetFinancialTotalsBackground ( int  foo, int  bar) {
     return  new  FinancialWorker (foo, bar);
   }
}
  
public  class  FinancialWorker : BackgroundWorker {
   public  Dictionary < string , int > Result;   // We can add typed fields.
   public  volatile  int  Foo, Bar;            // We could even expose them
                                            // via properties with locks!
   public  FinancialWorker() {
     WorkerReportsProgress = true ;
     WorkerSupportsCancellation = true ;
   }
  
   public  FinancialWorker ( int  foo, int  bar) : this () {
     this .Foo = foo; this .Bar = bar;
   }
  
   protected  override  void  OnDoWork (DoWorkEventArgs e) {
     ReportProgress (0, "Working hard on this report..." );
     Initialize financial report data
  
     while  (!finished report ) {
       if  (CancellationPending) {
         e.Cancel = true ;
         return ;
       }
       Perform another calculation step
       ReportProgress (percentCompleteCalc, "Getting there..." );
     }     
     ReportProgress (100, "Done!" );
     e.Result = Result = completed report data;
   }
}

   

      不管谁调用GetFinancialTotalsBackground都会获得一个FinancialWorker——一个用真实地可用地包装了管理后台 操做。它能够报告进度,被取消,与Windows Forms交互而不用使用Control.Invoke。它也有异常句柄,而且使用了标准的协议(与使用BackgroundWorker没任何区别!)

     这种BackgroundWorker的用法有效地回避了旧有的“基于事件的异步模式”。

 

2  ReaderWriterLockSlim

 

     //注意还有一个老的ReaderWriterLock类,Slim类为.net 3.5新增,提升了性能。

     一般来说,一个类型的实例对于并行的读操做是线程安全的,可是并行地更新操做则不是(并行地读与更新也不是)。 这对于资源(好比一个文件)也是同样的。使用一个简单的独占锁来锁定全部可能的访问可以解决实例的线程安全为问题,可是当有不少的读操做而只是偶然的更新 操做的时候,这就很不合理的限制了并发。一个例子就是这在一个业务程序服务器中,为了快速查找把数据缓存到静态字段中。在这样的状况 下,ReaderWriterLockSlim类被设计成提供最大可能的锁定。

     ReaderWriterLockSlim有两种基本的Lock方法:一个独占的Wirte Lock ,和一个与其余Read lock相容的读锁定。

     因此,当一个线程拥有一个Write Lock的时候,会阻塞全部其余线程得到读写锁。可是当没有线程得到WriteLock时,能够有多个线程同时得到ReadLock,进行读操做。

     ReaderWriterLockSlim提供了下面四个方法来获得和释放读写锁:

1
2
3
4
public  void  EnterReadLock();
public  void  ExitReadLock();
public  void  EnterWriteLock();
public  void  ExitWriteLock();

 

     另外对于全部的EnterXXX方法,还有”Try”版本的方法,它们接收timeOut参数,就像Monitor.TryEnter同样(在资源争用严重的时候超时发生至关容易)。另外ReaderWriterLock提供了其余相似的AcquireXXX 和 ReleaseXXX方法,它们超时退出的时候抛出异常而不是返回false。

       下面的程序展现了ReaderWriterLockSlim——三个线程循环地枚举一个List,同时另外两个线程每一秒钟添加一个随机数到List中。一个read lock保护List的读取线程,同时一个write lock保护写线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class  SlimDemo
{
   static  ReaderWriterLockSlim rw = new  ReaderWriterLockSlim();
   static  List< int > items = new  List< int >();
   static  Random rand = new  Random();
 
   static  void  Main()
   {
     new  Thread (Read).Start();
     new  Thread (Read).Start();
     new  Thread (Read).Start();
 
     new  Thread (Write).Start ( "A" );
     new  Thread (Write).Start ( "B" );
   }
 
   static  void  Read()
   {
     while  ( true )
     {
       rw.EnterReadLock();
       foreach  ( int  i in  items) Thread.Sleep (10);
       rw.ExitReadLock();
     }
   }
 
   static  void  Write ( object  threadID)
   {
     while  ( true )
     {              
       int  newNumber = GetRandNum (100);
       rw.EnterWriteLock();
       items.Add (newNumber);
       rw.ExitWriteLock();
       Console.WriteLine ( "Thread "  + threadID + " added "  + newNumber);
       Thread.Sleep (100);
     }
   }
 
   static  int  GetRandNum ( int  max) { lock  (rand) return  rand.Next (max); }
}
<em><span style= "font-family: YaHei Consolas Hybrid;" > //在实际的代码中添加try/finally,保证异常状况写lock也会被释放。</span></em>

结果为:

Thread B added 61 Thread A added 83 Thread B added 55 Thread A added 33 ...

      ReaderWriterLockSlim比简单的Lock容许更大的并发读能力。咱们可以添加一行代码到Write方法,在While循环的开始:

1
Console.WriteLine (rw.CurrentReadCount + " concurrent readers" );

       基本上老是会返回“3 concurrent readers”(读方法花费了更多的时间在Foreach循环),ReaderWriterLockSlim还提供了许多与CurrentReadCount属性相似的属性来监视lock的状况:

1
2
3
4
5
6
7
8
9
10
11
public  bool  IsReadLockHeld            { get ; }
public  bool  IsUpgradeableReadLockHeld { get ; }
public  bool  IsWriteLockHeld           { get ; }
 
public  int   WaitingReadCount          { get ; }
public  int   WaitingUpgradeCount       { get ; }
public  int   WaitingWriteCount         { get ; }
 
public  int   RecursiveReadCount        { get ; }
public  int   RecursiveUpgradeCount     { get ; }
public  int   RecursiveWriteCount       { get ; }

      有时候,在一个原子操做里面交换读写锁是很是有用的,好比,当某个item不在list中的时候,添加此item进去。最好的状况是,最小化写如锁的时间,例如像下面这样处理:

    1 得到一个读取锁

    2 测试list是否包含item,若是是,则返回

    3 释放读取锁

    4 得到一个写入锁

    5 写入item到list中,释放写入锁。

     但 是在步骤三、4之间,当另一个线程可能偷偷修改List(好比说添加一样一个Item),ReaderWriterLockSlim经过提供第三种锁来 解决这个问题,这就是upgradeable lock。一个可升级锁和read lock 相似,只是它可以经过一个原子操做,被提高为write lock。使用方法以下:

  1.  
    1. 调用 EnterUpgradeableReadLock
    2. 读操做(e.g. test if item already present in list)
    3. 调用 EnterWriteLock (this converts the upgradeable lock to a write lock)
    4. 写操做(e.g. add item to list)
    5. 调用ExitWriteLock (this converts the write lock back to an upgradeable lock)
    6. 其余读取的过程
    7. 调用ExitUpgradeableReadLock

      从调用者的角度,这很是想递归(嵌套)锁。实际上第三步的时候,经过一个原子操做,释放了read lock 并得到了一个新的write lock.

      upgradeable locks 和read locks之间另外还有一个重要的区别,尽管一个upgradeable locks 可以和任意多个read locks共存,可是一个时刻,只能有一个upgradeable lock本身被使用。这防止了死锁。这和SQL Server的Update lock相似

image

      咱们能够改变前面例子的Write方法来展现upgradeable lock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while  ( true )
{
   int  newNumber = GetRandNum (100);
   rw.EnterUpgradeableReadLock();
   if  (!items.Contains (newNumber))
   {
     rw.EnterWriteLock();
     items.Add (newNumber);
     rw.ExitWriteLock();
     Console.WriteLine ( "Thread "  + threadID + " added "  + newNumber);
   }
   rw.ExitUpgradeableReadLock();
   Thread.Sleep (100);
}

ReaderWriterLock 没有提供upgradeable locks的功能。

 

2.1  递归锁 Lock recursion

Ordinarily, nested or recursive locking is prohibited with ReaderWriterLockSlim. Hence, the following throws an exception:

默认状况下,递归(嵌入)锁被ReaderWriterLockSlim禁止,由于下面的代码可能抛出异常。

1
2
3
4
5
var  rw = new  ReaderWriterLockSlim();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();

可是显示地声明容许嵌套的话,就能正常工做,不过这带来了没必要要的复杂性。

1
var  rw = new  ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);

 

1
2
3
4
5
6
rw.EnterWriteLock();
rw.EnterReadLock();
Console.WriteLine (rw.IsReadLockHeld);     // True
Console.WriteLine (rw.IsWriteLockHeld);    // True
rw.ExitReadLock();
rw.ExitWriteLock();

   使用锁的顺序大体为:Read Lock  -->  Upgradeable Lock  -->  Write Lock

 

3   线程池

 

        若是你的程序有不少线程,致使花费了大多时间在等待句柄的阻止上,你能够经过 线程池来削减负担。线程池经过合并不少等待句柄在不多的线程上来节省时间。

        使用线程池,你须要注册一个连同将被执行的委托的Wait Handle,在Wait Handle发信号时。这个工做经过调用ThreadPool.RegisterWaitForSingleObject来完成,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class  Test {
   static  ManualResetEvent starter = new  ManualResetEvent ( false );
  
   public  static  void  Main() {
     ThreadPool.RegisterWaitForSingleObject (starter, Go, "hello" , -1, true );
     Thread.Sleep (5000);
     Console.WriteLine ( "Signaling worker..." );
     starter.Set();
     Console.ReadLine();
   }
  
   public  static  void  Go ( object  data, bool  timedOut) {
     Console.WriteLine ( "Started "  + data);
     // Perform task...
   }
}

image

      除了等待句柄和委托以外,RegisterWaitForSingleObject也接收一个“黑盒”对象,它被传递到你的委托方法中( 就像用ParameterizedThreadStart同样),拥有一个毫秒级的超时参数(-1意味着没有超时)和布尔标志来指明请求是一次性的仍是循环的。

       全部进入线程池的线程都是后台的线程,这意味着 它们在程序的前台线程终止后将自动的被终止。但你若是想等待进入线程池的线程都完成它们的重要工做在退出程序以前,在它们上调用Join是不行的,由于进 入线程池的线程历来不会结束!意思是说,它们被改成循环,直到父进程终止后才结束。因此为知道运行在线程池中的线程是否完成,你必须发信号——好比用另外一 个Wait Handle。

      在线程池中的线程上调用Abort 是一个坏主意,线程须要在程序域的生命周期中循环。

      你也能够用QueueUserWorkItem方法而不用等待句柄来使用线程池,它定义了一个当即执行的委托。你没必要在多个任务中节省共享线程,但有一个 惯例:线程池保持一个线程总数的封顶(默认为25),在任务数达到这个顶值后将自动排队。这就像程序范围的有25个消费者的生产者/消费者队列。在下面的例子中,100个任务入列到线程池中,而一次只执行 25个,主线程使用Wait 和 Pulse来等待全部的任务完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class  Test {
   static  object  workerLocker = new  object  ();
   static  int  runningWorkers = 100;
  
   public  static  void  Main() {
     for  ( int  i = 0; i < 100; i++) {
       ThreadPool.QueueUserWorkItem (Go, i);
     }
     Console.WriteLine ( "Waiting for threads to complete..." );
     lock  (workerLocker) {
       while  (runningWorkers > 0) Monitor.Wait (workerLocker);
     }
     Console.WriteLine ( "Complete!" );
     Console.ReadLine();
   }
  
   public  static  void  Go ( object  instance) {
     Console.WriteLine ( "Started: "  + instance);
     Thread.Sleep (1000);
     Console.WriteLine ( "Ended: "  + instance);
     lock  (workerLocker) {
       runningWorkers--; Monitor.Pulse (workerLocker);
     }
   }
}

     为了传递多个对象给目标方法,你能够定义个拥有全部须要属性的自定义对象,或者调用一个匿名方法。好比若是Go方法接收两个整型参数,会像下面这样:

1
ThreadPool.QueueUserWorkItem ( delegate  ( object  notUsed) { Go (23,34); });

另外一个进入线程池的方式是经过异步委托。

 

4.   异步委托

 

     在第一部分咱们描述如何使用 ParameterizedThreadStart把数据传入线程中。有时候 你须要经过另外一种方式,来从线程中获得它完成后的返回值。异步委托提供了一个便利的机制,容许许多参数在两个方向上传递 。此外,未处理的异常在异步委托中在原始线程上被从新抛出,所以在工做线程上不须要明确的处理了。异步委托也提供了计入 线程池的另外一种方式。

     对此你必须付出的代价是要跟从异步模型。为了看看这意味着什么,咱们首先讨论更常见的同步模型。咱们假设咱们想比较 两个web页面,咱们按顺序取得它们,而后像下面这样比较它们的输出:

1
2
3
4
5
6
static  void  ComparePages() {
   WebClient wc = new  WebClient ();
   string  s1 = wc.DownloadString ( "http://www.oreilly.com" );
   string  s2 = wc.DownloadString ( "http://oreilly.com" );
   Console.WriteLine (s1 == s2 ? "Same"  : "Different" );
}

    若是两个页面同时下载固然会更快了。问题在于当页面正在下载时DownloadString阻止了继续调用方法。若是咱们能 调用 DownloadString在一个非阻止的异步方式中会变的更好,换言之:

1. 咱们告诉 DownloadString 开始执行

2. 在它执行时咱们执行其它任务,好比说下载另外一个页面

3. 咱们询问DownloadString的全部结果

    WebClient类实际上提供一个被称为DownloadStringAsync的内建方法 ,它提供了就像异步函数的功能。而眼下,咱们忽略这个问题,集中精力在任何方法均可以被异步调用的机制上。

    第三步使异步委托变的有用。调用者聚集了工做线程获得结果和容许任何异常被从新抛出。没有这步,咱们只有普通多线程。虽然也可能不用聚集方式使用异步委托,你能够用ThreadPool.QueueWorkerItem 或 BackgroundWorker。

    下面咱们用异步委托来下载两个web页面,同时实现一个计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
delegate  string  DownloadString ( string  uri);
  
static  void  ComparePages() {
  
   // Instantiate delegates with DownloadString's signature:
   DownloadString download1 = new  WebClient().DownloadString;
   DownloadString download2 = new  WebClient().DownloadString;
   
   // Start the downloads:
   IAsyncResult cookie1 = download1.BeginInvoke (uri1, null , null );
   IAsyncResult cookie2 = download2.BeginInvoke (uri2, null , null );
   
   // Perform some random calculation:
   double  seed = 1.23;
   for  ( int  i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
   
   // Get the results of the downloads, waiting for completion if necessary.
相关文章
相关标签/搜索