现在的位置: 首页 > 综合 > 正文

为什么选择多线程?(收藏)

2012年07月08日 ⁄ 综合 ⁄ 共 7804字 ⁄ 字号 评论关闭
为什么选择多线程?
多线程处理可以使您能够通过确保程序“永不睡眠”从而保持 UI 的快速响应。
在多线程下,耗时较长的任务就可以在其自己的线程中运行,这些线程通常称为辅助线程。因为只有辅助线程受到阻止,所以阻塞操作不再导致用户界面冻结。
其基本原则是,负责响应用户输入和保持用户界面为最新的线程(通常称为 UI 线程)不应该用于执行任何耗时较长的操作。惯常做法是,任何耗时超过 30ms 的操作都要考虑从 UI 线程中移除。
如果想让用户界面保持响应迅速,则任何阻塞操作都应该在辅助线程中执行不管是机械等待某事发生(例如,等待 CD-ROM 启动或者硬盘定位数据),还是等待来自网络的响应。
 
异步委托调用
在辅助线程中运行代码的最简单方式是使用异步委托调用(所有委托都提供该功能)。委托通常是以同步方式进行调用,即,在调用委托时,只有包装方法返回后该调用才会返回。要以异步方式调用委托,请调用 BeginInvoke 方法,这样会对该方法排队以在系统线程池的线程中运行。调用线程会立即返回,而不用等待该方法完成。这比较适合于 UI 程序,因为可以用它来启动耗时较长的作业,而不会使用户界面反应变慢。
在以下代码中,System.Windows.Forms.MethodInvoker 类型是一个系统定义的委托,用于调用不带参数的方法。
private void StartSomeWorkFromUIThread () {
    // The work we want to do is too slow for the UI
    // thread, so let's farm it out to a worker thread.
 
    MethodInvoker mi = new MethodInvoker(
        RunsOnWorkerThread);
    mi.BeginInvoke(null, null); // This will not block.
}
 
// The slow work is done here, on a thread
// from the system thread pool.
private void RunsOnWorkerThread() {
    DoSomethingSlow();
}
如果想要传递参数,可以选择合适的系统定义的委托类型,或者自己来定义委托。

调用 BeginInvoke 会使该方法在系统线程池的线程中运行,而不会阻塞 UI 线程以便其可执行其他操作。
如果您需要该方法返回的结果,则 BeginInvoke 的返回值很重要,并且您可能不传递空参数。
然而,对于大多数 UI 应用程序而言,这种“启动后就不管”的风格是最有效的。
应该注意到,BeginInvoke 将返回一个 IAsyncResult。这可以和委托的 EndInvoke 方法一起使用,

以在该方法调用完毕后检索调用结果。

 
线程和控件

 Windows
窗体中最重要的一条线程规则:除了极少数的例外情况,否则都不要在它的创建线程以外的线程中使用控件的任何成员。规则的结果是一个被包含的控件(如,包含在一个表单中的按钮)必须与包含它控件位处于同一个线程中。也就是说,一个窗口中的所有控件属于同一个 UI 线程。大部分 Windows 窗体应用程序最终都只有一个线程,所有 UI 活动都发生在这个线程上。这个线程通常称为 UI 线程。这意味着您不能调用用户界面中任意控件上的任何方法,除非在该方法的文档说明中指出可以调用。
注意,以下代码是非法的:
// Created on UI thread
private Label lblStatus;
...
// Doesn't run on UI thread
private void RunsOnWorkerThread() {
    DoSomethingSlow();
    lblStatus.Text = "Finished!";    // BAD!!
}
这就是多线程错误中的主要问题,即它们并不会立即显现出来。甚至当出现了一些错误时,在第一次演示程序之前一切看起来也都很正常。
 
在正确的线程中调用控件
 
理论上讲,可以使用低级的同步原理和池化技术来生成自己的机制,但幸运的是,因为有一个以 Control 类的 Invoke 方法形式存在的解决方案,所以不需要借助于如此低级的工作方式。
Invoke 方法是 Control 类中少数几个有文档记录的线程规则例外之一:它始终可以对来自任何线程的 Control 进行 Invoke 调用。Invoke 方法本身只是简单地携带委托以及可选的参数列表,并在 UI 线程中为您调用委托,而不考虑 Invoke 调用是由哪个线程发出的。实际上,为控件获取任何方法以在正确的线程上运行非常简单。但应该注意,只有在 UI 线程当前未受到阻塞时,这种机制才有效 — 调用只有在 UI 线程准备处理用户输入时才能通过。Invoke 方法会进行测试以了解调用线程是否就是 UI 线程。如果是,它就直接调用委托。否则,它将安排线程切换,并在 UI 线程上调用委托。无论是哪种情况,委托所包装的方法都会在 UI 线程中运行,并且只有当该方法完成时,Invoke 才会返回。
Control 类也支持异步版本的 Invoke,它会立即返回并安排该方法以便在将来某一时间在 UI 线程上运行。这称为 BeginInvoke,它与异步委托调用很相似,与委托的明显区别在于:委托调用以异步方式在线程池的某个线程上运行,BeginInvoke以异步方式在 UI 线程上运行。
Control 的 Invoke、BeginInvoke 和 EndInvoke 方法,以及 InvokeRequired 属性都是 ISynchronizeInvoke 接口的成员。该接口可由任何需要控制其事件传递方式的类实现。由于 BeginInvoke 不容易造成死锁,所以尽可能多用该方法;而少用 Invoke 方法。因为 Invoke 是同步的,所以它会阻塞辅助线程,直到 UI 线程可用。
回顾一下前面的代码。首先,必须将一个委托传递给 Control 的 BeginInvoke 方法,以便可以在 UI 线程中运行对线程敏感的代码。这意味着应该将该代码放在它自己的方法中。(前面所展示的代码片段的合法版本)
// Created on UI thread
private Label lblStatus;
•••
// Doesn't run on UI thread
private void RunsOnWorkerThread() {
    DoSomethingSlow();
    // Do UI update on UI thread
    object[] pList = { this, System.EventArgs.Empty };
    lblStatus.BeginInvoke(
      new System.EventHandler(UpdateUI), pList);
}
•••
// Code to be run back on the UI thread
// (using System.EventHandler signature
// so we don't need to define a new
// delegate type here)
private void UpdateUI(object o, System.EventArgs e) {
    // Now OK - this method will be called via
    // Control.Invoke, so we are allowed to do
    // things to the UI.
    lblStatus.Text = "Finished!";
}
 
一旦辅助线程完成缓慢的工作后,它就会调用 Label 中的 BeginInvoke,以便在其 UI 线程上运行某段代码。通过这样,它可以更新用户界面。
包装 Control.Invoke
如果辅助线程希望在结束时提供更多的反馈信息,而不是简单地给出“Finished!”消息,则 BeginInvoke 过于复杂的使用方法会令人生畏。为了传达其他消息,例如“正在处理”、“一切顺利”等等,需要设法向 UpdateUI 函数传递一个参数。可能还需要添加一个进度栏以提高反馈能力。这么多次调用 BeginInvoke 可能导致辅助线程受该代码支配。这样不仅会造成不便,而且考虑到辅助线程与 UI 的协调性,这样设计也不好。 怎么办呢?使用包装函数!基于上述要求,上面的代码改进如下:
public class MyForm : System.Windows.Forms.Form {
    ...
    public void ShowProgress(string msg, int percentDone) {
        // Wrap the parameters in some EventArgs-derived custom class:
        System.EventArgs e = new MyProgressEvents(msg, percentDone);
        object[] pList = { this, e };
        // Invoke the method. This class is derived
        // from Form, so we can just call BeginInvoke
        // to get to the UI thread.
        BeginInvoke(new MyProgressEventsHandler(UpdateUI), pList);
    }
    private delegate void MyProgressEventsHandler(
        object sender, MyProgressEvents e);
    private void UpdateUI(object sender, MyProgressEvents e) {
        lblStatus.Text = e.Msg;
        myProgressControl.Value = e.PercentDone;
    }
}
这里定义了自己的方法,该方法违背了“必须在 UI 线程上进行调用”这一规则,因为它进而只调用不受该规则约束的其他方法。这种技术会引出一个较为常见的话题:为什么不在控件上编写公共方法呢(这些方法记录为 UI 线程规则的例外)?
刚好 Control 类为这样的方法提供了一个有用的工具。如果我提供一个设计为可从任何线程调用的公共方法,则完全有可能某人会从 UI 线程调用这个方法。在这种情况下,没必要调用 BeginInvoke,因为我已经处于正确的线程中。调用 Invoke 完全是浪费时间和资源,不如直接调用适当的方法。为了避免这种情况,Control 类将公开一个称为 InvokeRequired 的属性。这是“只限 UI 线程”规则的另一个例外。它可从任何线程读取,如果调用线程是 UI 线程,则返回假,其他线程则返回真。
public void ShowProgress(string msg, int percentDone) {
    if (InvokeRequired) {
        // As before
        ...
    } else {
        // We're already on the UI thread just
        // call straight through.
        UpdateUI(this, new MyProgressEvents(msg,
            PercentDone));
    }
}
ShowProgress 现在可以记录为可从任何线程调用的公共方法。这并没有消除复杂性 — 执行 BeginInvoke 的代码依然存在,它还占有一席之地。不幸的是,没有简单的方法可以完全摆脱它(郁闷)。
锁定
如果两个线程在同一时间、在同一个位置执行写入操作,则在同步写入操作发生之后,所有从该位置读取数据的线程就有可能看到一堆垃圾数据。为了避免这种问题,必须采取措施来确保一次只有一个线程可以读取或写入某个对象的状态。      防止这些问题出现所采用的方式是,使用运行时的锁定功能。C# 可以让您利用这些功能、通过锁定关键字来保护代码(Visual Basic 也有类似构造,称为 SyncLock)。规则是,任何想要在多个线程中调用其方法的对象在每次访问其字段时(不管是读取还是写入)都应该使用锁定构造
还是看个例子:
// This field could be modified and read on any thread, so all access 
// must be protected by locking the object.
 
private double myPosition;
•••
public double Position {
    get {
        // Position could be modified from any thread, so we need to lock
        // this object to make sure we get a consistent value.
        lock (this) {
            return myPosition;
        }
    }
    set {
        lock (this) {
            myPosition = value;
        }
    }
}
 
public void MoveBy(double offset) {//这里也要锁
    // Here we are reading, checking and then modifying the value. It is
    // vitally important that this entire sequence is protected by a
    // single lock block.
    lock (this) {
        double newPos = Position + offset;
        // Check within range - MINPOS and MAXPOS
        // will be const values defined somewhere in
        // this class
        if (newPos > MAXPOS) newPos = MAXPOS;
        else if (newPos < MINPOS) newPos = MINPOS;
        Position = newPos;
    }
}
 
当所做的修改比简单的读取或写入更复杂时,整个过程必须由单独的锁语句保护。这也适用于对多个字段进行更新 — 在对象处于一致状态之前,一定不能释放该锁。如果该锁在更新状态的过程中释放,则其他线程也许能够获得它并看到不一致状态。如果您已经拥有一个锁,并调用一个试图获取该锁的方法,则不会导致问题出现,因为单独线程允许多次获得同一个锁。对于需要锁定以保护对字段的低级访问和对字段执行的高级操作的代码,这非常重要。
死锁
 
       先看例子:
public class Foo {
    public void CallBar() {
        lock (this) {
            Bar myBar = new Bar ();
            myBar.BarWork(this);
        }
    }
 
    // This will be called back on a worker thread
    public void FooWork() {
        lock (this) {
            // do some work
            •••
        }
    }
}
 
public class Bar {
    public void BarWork(Foo myFoo) {
        // Call Foo on different thread via delegate.
        MethodInvoker mi = new MethodInvoker(
            myFoo.FooWork);
        IAsyncResult ar = mi.BeginInvoke(null, null);
        // do some work
        •••
        // Now wait for delegate call to complete (DEADLOCK!)
        mi.EndInvoke(ar);
    }
}
 
         有两个或更多线程都被阻塞以等待对方进行。这里的情形和标准死锁情况还是有些不同,后者通常包括两个锁。这表明如果有某个因果性(过程调用链)超出线程界限,就会发生死锁,即使只包括一个锁!Control.Invoke 是一种跨线程调用过程的方法,这是个不争的重要事实。BeginInvoke 不会遇到这样的问题,因为它并不会使因果性跨线程。实际上,它会在某个线程池线程中启动一个全新的因果性,以允许原有的那个独立进行。然而,如果保留 BeginInvoke 返回的 IAsyncResult,并用它调用 EndInvoke,则又会出现问题,因为 EndInvoke 实际上已将两个因果性合二为一。避免这种情况的最简单方法是,当持有一个对象锁时,不要等待跨线程调用完成。要确保这一点,应该避免在锁语句中调用 Invoke EndInvoke。其结果是,当持有一个对象锁时,将无需等待其他线程完成某操作。要坚持这个规则,说起来容易做起来难。
 
最佳规则是,根本不调用 Control.Invoke EndInvoke。这就是为什么“启动后就不管”的编程风格更可取的原因,也是为什么 Control.BeginInvoke 解决方案通常比 Control.Invoke 解决方案好的原因。只要可能,在持有锁时就应该避免阻塞,因为如果不这样,死锁就难以消除。
 
使其简单
 
       到这里,我还是晕晕的,有个问题:如何既从多线程获益最大,又不会遇到困扰并发代码的棘手错误呢?
UI 代码的性质是:它从外部资源接收事件,如用户输入。它会在事件发生时对其进行处理,但却将大部分时间花在了等待事件的发生。如果可以构造辅助线程和 UI 线程之间的通信,使其适合该模型,则未必会遇到这么多问题,因为不会再有新的东西引入。
这样使事情简单化的:将辅助线程视为另一个异步事件源。如同 Button 控件传递诸如 Click MouseEnter 这样的事件,可以将辅助线程视为传递事件(如 ProgressUpdate WorkComplete)的某物。只是简单地将这看作一种类比,还是真正将辅助对象封装在一个类中,并按这种方式公开适当的事件,这完全取决于您。后一种选择可能需要更多的代码,但会使用户界面代码看起来更加统一。不管哪种情况,都需要 Control.BeginInvoke 在正确的线程上传递这些事件。
对于辅助线程,最简单的方式是将代码编写为正常顺序的代码块。但如果想要使用刚才介绍的“将辅助线程作为事件源”模型,那又该如何呢?这个模型非常适用,但它对该代码与用户界面的交互提出了限制:这个线程只能向 UI 发送消息,并不能向它提出请求。
例如,让辅助线程中途发起对话以请求完成结果需要的信息将非常困难。如果确实需要这样做,也最好是在辅助线程中发起这样的对话,而不要在主 UI 线程中发起。该约束是有利的,因为它将确保有一个非常简单且适用于两线程间通信的模型在这里简单是成功的关键。这种开发风格的优势在于,在等待另一个线程时,不会出现线程阻塞。这是避免死锁的有效策略

抱歉!评论已关闭.