单元模式线程是一个自动线程安全机制, 非常贴近于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命名空间 下帮助类,它管理着工作线程。它提供了以下特性:
最后两个特性是相当地有用:意味着你不再需要将try/catch语句块放到 你的工作线程中了,并且更新Windows Forms控件不需要调用 Control.Invoke了。BackgroundWorker使用线程池工作, 对于每个新任务,它循环使用避免线程们得到休息。这意味着你不能在 BackgroundWorker线程上调用 Abort了。
下面是使用BackgroundWorker最少的步骤:
这就设置好了它,任何被传入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中就可以这么做。
添加进程报告支持:
ProgressChanged中的代码就像RunWorkerCompleted一样可以自由地与UI控件进行交互,这在更性进度栏尤为有用。
添加退出报告支持:
下面的例子实现了上面描述的特性:
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的用法有效地回避了旧有的“基于事件的异步模式”。
//注意还有一个老的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
|