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代码之前以下二者之一发生了:
- 它将调度编组成一个单线程单元
- 它将崩溃
在多线程的Windows Forms程序中,通过非创建控件的线程调用控件的的属性和方法是非法的。所有跨进程的调用必须被明确地排列至创建控件的线程中(通常为主线程),利用 Control.Invoke 或 Control.BeginInvoke方法。你不能依赖自动调度编组因为它发生的太晚了,仅当执行刚好进入了非托管的代码它才发生,而.NET已有足够 的时间来运行“错误的”线程代码,那些非线程安全的代码。
一个优秀的管理Windows Forms程序的方案是使用BackgroundWorker, 这个类包装了需要报道进度和完成度的工作线程,并自动地调用Control.Invoke方法作为需要。
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 RecursiveUpgradeCount { get ; } public int RecursiveWriteCount { get ; } |
有时候,在一个原子操作里面交换读写锁是非常有用的,比如,当某个item不在list中的时候,添加此item进去。最好的情况是,最小化写如锁的时间,例如像下面这样处理:
1 获得一个读取锁
2 测试list是否包含item,如果是,则返回
3 释放读取锁
4 获得一个写入锁
5 写入item到list中,释放写入锁。
但 是在步骤3、4之间,当另外一个线程可能偷偷修改List(比如说添加同样一个Item),ReaderWriterLockSlim通过提供第三种锁来 解决这个问题,这就是upgradeable lock。一个可升级锁和read lock 类似,只是它能够通过一个原子操作,被提升为write lock。使用方法如下:
-
- 调用 EnterUpgradeableReadLock
- 读操作(e.g. test if item already present in list)
- 调用 EnterWriteLock (this converts the upgradeable lock to a write lock)
- 写操作(e.g. add item to list)
- 调用ExitWriteLock (this converts the write lock back to an upgradeable lock)
- 其他读取的过程
- 调用ExitUpgradeableReadLock
从调用者的角度,这非常想递归(嵌套)锁。实际上第三步的时候,通过一个原子操作,释放了read lock 并获得了一个新的write lock.
upgradeable locks 和read locks之间另外还有一个重要的区别,尽管一个upgradeable locks 能够和任意多个read locks共存,但是一个时刻,只能有一个upgradeable lock自己被使用。这防止了死锁。这和SQL Server的Update lock类似
我们可以改变前面例子的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... } } |
除了等待句柄和委托之外,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 (); 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. // Here's where any exceptions will be thrown: string s1 = download1.EndInvoke (cookie1); string s2 = download2.EndInvoke (cookie2); Console.WriteLine (s1 == s2 ? "Same" : "Different" ); } |
我们以声明和实例化我们想要异步运行的方法开始。在这个例子中,我们需要两个委托,每个引用不同的WebClient的对象(WebClient 不允许并行的访问,如果它允许,我们就只需一个委托了)。
我们然后调用BeginInvoke,这开始执行并立刻返回控制器给调用者。依照我们的委托,我们必须传递一个字符串给 BeginInvoke (编译器由生产BeginInvoke 和 EndInvoke在委托类型强迫实现这个).
BeginInvoke 还需要两个参数:一个可选callback和数据对象;它们通常不需要而被设置为null, BeginInvoke返回一个 IASynchResult对象,它担当着调用 EndInvoke所用的数据。IASynchResult 同时有一个IsCompleted属性来检查进度。
之后我们在委托上调用EndInvoke ,得到需要的结果。如果有必要,EndInvoke会等待, 直到方法完成,然后返回方法返回的值作为委托指定的(这里是字符串)。 EndInvoke一个好的特性是DownloadString有任何的引用或输出参数, 它们会在 EndInvoke结构赋值,允许通过调用者多个值被返回。
在异步方法的执行中的任何点发生了未处理的异常,它会重新在调用线程在EndInvoke中抛出。 这提供了精简的方式来管理返回给调用者的异常。
如果你异步调用的方法没有返回值,你也(理论上)应该调用EndInvoke,在部分意义上 在开放了误判;MSDN上辩论着这个话题。如果你选择不调用EndInvoke,你需要考虑在工作方法中的异常。
4.1 异步方法
.NET Framework 中的一些类型提供了某些它们方法的异步版本,它们使用"Begin" 和 "End"开头。它们被称之为异步方法,它们有与异步委托类似的特性,但异步委托存在着一些待解决的困难的问题:允许比你所拥有的线程还多的并发活动率。 比如一个web或TCP Socket服务器,如果用NetworkStream.BeginRead 和 NetworkStream.BeginWrite 来写的话,就可能在仅仅线程池线程中处理数百个并发的请求。
除非你正在写一个专门的高并发程序,否则不应该过多地使用异步方法。理由如下:
- 不像异步委托,异步方法实际上可能没有与调用者同时执行
- 如果你未能小心翼翼地遵从它的模式异步方法的好处被侵腐或消失了,
- 当你恰当地遵从了它的模式,事情立刻变的复杂了
如果你只是像简单地获得并行执行的结果,你最好远离调用异步版本的方法(比如NetworkStream.Read) 而通过异步委托。另一个选项是使用ThreadPool.QueueUserWorkItem或BackgroundWorker,又或者只是简单地创建 新的线程。
4.2 异步事件
另一种模式存在,就是为什么类型可以提供异步版本的方法。这就是所谓的“基于事件的异步模式”,这些的方法以"Async"结束,相对应的事件 以"Completed"结束。WebClient使用这个模式在它的DownloadStringAsync 方法中。 为了使用它,你要首先处理"Completed" 事件(例如:DownloadStringCompleted),然后调用"Async"方法(例如:DownloadStringAsync)。当方法 完成后,它调用你事件句柄。不幸的是,WebClient的实现是有缺陷的:像DownloadStringAsync 这样的方法对于下载的一部分时间阻止了调用者的线程。
基于事件的模式也提供了报道进度和取消操作,被友好地设计成可对Windows程序可更新forms和控件。如果在某个类型中你需要这些特性 ,而它却不支持(或支持的不好)基于事件的模式,你没必要去自己实现它(你也根本不想去做!)。尽管如此,所有的这些通过 BackgroundWorker这个帮助类便可轻松完成。
5. 计时器
周期性的执行某个方法最简单的方法就是使用一个计时器,比如System.Threading 命名空间下Timer类。线程计时器利用了线程池,允许多个计时器被创建而没有额外的线程开销。 Timer 算是相当简易的类,它有一个构造器和两个方法(这对于极简主义者来说是最高兴不过的了)。
1
2
3
4
5
6
7
8
9
|
public sealed class Timer : MarshalByRefObject, IDisposable { public Timer (TimerCallback tick, object state, 1st, subsequent); public bool Change (1st, subsequent); // To change the interval public void Dispose(); // To kill the timer } 1st = time to the first tick in milliseconds or a TimeSpan subsequent = subsequent intervals in milliseconds or a TimeSpan (use Timeout.Infinite for a one-off callback) |
接下来这个例子,计时器5秒钟之后调用了Tick 的方法,它写"tick...",然后每秒写一个,直到用户敲 Enter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using System; using System.Threading; class Program { static void Main() { Timer tmr = new Timer (Tick, "tick..." , 5000, 1000); Console.ReadLine(); tmr.Dispose(); // End the timer } static void Tick ( object data) { // This runs on a pooled thread Console.WriteLine (data); // Writes "tick..." } } |
.NET framework在System.Timers命名空间下提供了另一个计时器类。它完全包装自System.Threading.Timer,在使用相同的线程池时提供了额外的便利——相同的底层引擎。下面是增加的特性的摘要:
- 实现了Component,允许它被放置到Visual Studio设计器中
- Interval属性代替了Change方法
- Elapsed 事件代替了callback委托
- Enabled属性开始或暂停计时器
- 提够Start 和 Stop方法,万一对Enabled感到迷惑
- AutoReset标志来指示是否循环(默认为true)
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using System; using System.Timers; // Timers namespace rather than Threading class SystemTimer { static void Main() { Timer tmr = new Timer(); // Doesn't require any args tmr.Interval = 500; tmr.Elapsed += tmr_Elapsed; // Uses an event instead of a delegate tmr.Start(); // Start the timer Console.ReadLine(); tmr.Stop(); // Pause the timer Console.ReadLine(); tmr.Start(); // Resume the timer Console.ReadLine(); tmr.Dispose(); // Permanently stop the timer } static void tmr_Elapsed ( object sender, EventArgs e) { Console.WriteLine ( "Tick" ); } } |
.NET framework 还提供了第三个计时器——在System.Windows.Forms 命名空间下。虽然类似于System.Timers.Timer 的接口,但功能特性上有根本的不同。一个Windows Forms 计时器不能使用线程池,代替为总是在最初创建它的线程上触发 "Tick"事件。假定这是主线程——负责实例化所有Windows Forms程序中的forms和控件,计时器的事件能够操作forms和控件而不违反线程安全——或者强加单元线程模式。Control.Invoke是不需要的。它实质上是一个单线程timer
Windows Forms计时器必须迅速地执行来更新用户接口。迅速地执行是非常重要的,因为Tick事件被主线程调用,如果它有停顿, 将使用户接口变的没有响应。
6. 局部存储
每个线程与其它线程数据存储是隔离的,这对于“不相干的区域”的存储是有益的,它支持执行路径的基础结构,如通信,事务和安全令牌。 通过方法参数传递这些数据是十分笨拙的。存储这些数据到静态域意味着这些数据可以被所有线程共享。
Thread.GetData从一个线程的隔离数据中读,Thread.SetData 写入数据。 两个方法需要一个LocalDataStoreSlot对象来识别内存槽——这包装自一个内存槽的名称的字符串,这个名称 你可以跨所有的线程使用,它们将得到不各自的值,看这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class ... { // 相同的LocalDataStoreSlot 对象可以用于跨所有线程 LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot ( "securityLevel" ); // 这个属性每个线程有不同的值 int SecurityLevel { get { object data = Thread.GetData (secSlot); return data == null ? 0 : ( int ) data; // null == 未初始化 } set { Thread.SetData (secSlot, value); } } ... |
Thread.FreeNamedDataSlot将释放给定的数据槽,它跨所有的线程——但只有一次,当所有相同名字 LocalDataStoreSlot对象作为垃圾被回收时退出作用域时发生。这确保了线程不得到数据槽从它们的脚底下撤出——也保持了引用适当的使用之 中的LocalDataStoreSlot对象。