第三部分:使用多线程1

2008-05-11 01:21

单元模式和Windows Forms

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

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

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

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

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

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

定义单元模式

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

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

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

class Program {
  [STAThread]
  static void Main() {
  ...

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

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

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

Control.Invoke

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

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

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事件委托的方法中,下面是例子:

class Program {
  static 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来请求退出

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

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;     // 传递给 RunWorkerCopmleted
  }
 
  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);       // 从 DoWork
  }
 
  static void bw_ProgressChanged (object sender,
  ProgressChangedEventArgs e) {
     Console.WriteLine ("Reached " + e.ProgressPercentage + "%");
  }
}

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%
Reached 60%
Reached 80%
Reached 100%
Complete – 123

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%

You cancelled!

BackgroundWorker的子类

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

public class Client {
  Dictionary <string,int> GetFinancialTotals (int foo, int bar) { ... }
  ...
}

我们可以如此来实现:

public class Client {
  public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) {
     return new FinancialWorker (foo, bar);
  }
}
 
public class FinancialWorker : BackgroundWorker {
  public Dictionary <string,int> Result;    // 我们增加类型字段
  public volatile int Foo, Bar;             // 我们甚至可以暴露它们
                                            // 通过锁的属性!
  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的用法有效地回避了旧有的“基于事件的异步模式”。

ReaderWriterLock类

通常来讲,一个类型的实例对于并行的读操作是线程安全的,但是并行地根性操作则不是(并行地读和更新也不是)。这对于资源也是一样的,比如一个文件。当保护类型的实例安全时,使用一个简单的排它锁即解决问题,但是当有很多的读操作而偶然的更新操作这就很不合理的限制了并发。一个例子就是这在一个业务程序服务器中,为了快速查找把数据缓存到静态字段中。在这个方案中,ReaderWriterLock类被设计成提供最大容量的锁定。

ReaderWriterLock为读和写的锁提供了不同的方法——AcquireReaderLockAcquireWriterLock。两个方法都需要一个超时参数,并且在超时发生后抛出ApplicationException异常(不同于大多数线程类的返回false等效的方法)。超时发生相当容易在资源争用严重的时候。

调用 ReleaseReaderLockReleaseWriterLock释放锁。这些方法支持嵌套锁,ReleaseLock方法也支持一次清除所有嵌套级别的锁。(你可以随后调用RestoreLock类重新锁定相同的级别,它在ReleaseLock之前执行——如此来模仿Monitor.Wait锁定切换行为)。

你可以调用AcquireReaderLock开始一个read-lock ,然后通过UpgradeToWriterLock把它升级为write-lock。这个方法返回一个可能被用于调用DowngradeFromWriterLock的信息。这个方式允许读程序临时地请求写访问同时不必必须在降级之后重新排队列。

在接下来的这个例子中,4个线程被启动:一个不停地往列表中增加项目;另一个不停地从列表中移除项目;其它两个不停地报告列表中项目的个数。前两者获得写的锁,后两者获得读的锁。每个锁的超时参数为10秒。(异常处理一般要使用来捕捉ApplicationException,这个例子中出于方便而省略了)

class Program {
  static ReaderWriterLock rw = new ReaderWriterLock ();
  static List <int> items = new List <int> ();
  static Random rand = new Random ();
 
  static void Main (string[] args) {
     new Thread (delegate() { while (true) AppendItem(); } ).Start();
     new Thread (delegate() { while (true) RemoveItem(); } ).Start();
     new Thread (delegate() { while (true) WriteTotal(); } ).Start();
     new Thread (delegate() { while (true) WriteTotal(); } ).Start();
  }
 
  static int GetRandNum (int max) { lock (rand) return rand.Next (max); }
 
  static void WriteTotal() {
     rw.AcquireReaderLock (10000);
     int tot = 0; foreach (int i in items) tot += i;
     Console.WriteLine (tot);
     rw.ReleaseReaderLock();
  }
 
 static void AppendItem () {
     rw.AcquireWriterLock (10000);
     items.Add (GetRandNum (1000));
     Thread.SpinWait (400);
     rw.ReleaseWriterLock();
  }
 
  static void RemoveItem () {
     rw.AcquireWriterLock (10000);
     if (items.Count > 0)
       items.RemoveAt (GetRandNum (items.Count));
     rw.ReleaseWriterLock();
  }
}

List中加项目要比移除快一些,这个例子在AppendItem中包含了SpinWait来保持项目总数平衡。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值