C#中的线程(三) 使用多线程

第三部分:使用多线程

 

1.  单元模式和Windows Forms

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

       单元是多线程的逻辑上的“容器”,单元产生两种容量——“单的”和“多的”。单线 程单元只包含一个线程;多线程单元可以包含任何数量的线程。单线程模式更普遍 并且能与两者有互操作性。

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

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

       一个同步内容的图书馆允许任何人进入,同时同一时刻只允许一个人进入,在图书馆外会形成队列。

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

 

1.1  定义单元模式

 

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

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

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

?
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 + "%" );
   }
}

 

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 

你可能感兴趣的:(C#中的线程(三) 使用多线程)