线程(一)php
文章系参考转载,英文原文网址请参考:http://www.albahari.com/threading/html
做者 Joseph Albahari, 翻译 Swanky Wu程序员
中文翻译做者把原文放在了"google 协做"上面,GFW屏蔽,不能访问和查看,所以我根据译文和英文原版整理转载到园子里面。web
本系列文章能够算是一本很出色的C#线程手册,思路清晰,要点都有介绍,看了后对C#的线程及同步等有了更深刻的理解。数据库
C#支持经过多线程并行地执行代码,一个线程有它独立的执行路径,可以与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操做系统(也称为“主线程”)自动建立的,并具备多线程建立额外的线程。这里的一个简单的例子及其输出:安全
除非被指定,不然全部的例子都假定如下命名空间被引用了:
using System;
using System.Threading;服务器
1
2
3
4
5
6
7
8
9
10
11
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (WriteY);
t.Start();
// Run WriteY on the new thread
while
(
true
) Console.Write (
"x"
);
// Write 'x' forever
}
static
void
WriteY() {
while
(
true
) Console.Write (
"y"
);
// Write 'y' forever
}
}
|
主线程建立了一个新线程“t”,它运行了一个重复打印字母"y"的方法,同时主线程重复但因字母“x”。CLR分配每一个线程到它本身的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中咱们定义了一个局部变量,而后在主线程和新建立的线程上同时地调用这个方法。cookie
1
2
3
4
5
6
7
8
9
|
static
void
Main() {
new
Thread (Go).Start();
// Call Go() on a new thread
Go();
// Call Go() on the main thread
}
static
void
Go() {
// Declare and use a local variable - 'cycles'
for
(
int
cycles = 0; cycles < 5; cycles++) Console.Write (
'?'
);
}
|
变量cycles的副本分别在各自的内存堆栈中建立,输出也同样,可预见,会有10个问号输出。当线程们引用了一些公用的目标实例的时候,他们会共享数据。下面是实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
ThreadTest {
bool
done;
static
void
Main() {
ThreadTest tt =
new
ThreadTest();
// Create a common instance
new
Thread (tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void
Go() {
if
(!done) { done =
true
; Console.WriteLine (
"Done"
); }
}
}
由于在相同的<b>ThreadTest</b>实例中,两个线程都调用了<b>Go()</b>,它们共享了<b>done</b>字段,这个结果输出的是一个
"Done"
,而不是两个。
|
1
|
<a href=
"http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_6.png"
><img height=
"45"
width=
"640"
src=
"http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_thumb_2.png"
align=
"left"
alt=
"image"
border=
"0"
title=
"image"
style=
"display: inline; margin-left: 0px; margin-right: 0px; border-width: 0px;"
></a>
|
静态字段提供了另外一种在线程间共享数据的方式,下面是一个以done为静态字段的例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadTest {
static
bool
done;
// Static fields are shared between all threads
static
void
Main() {
new
Thread (Go).Start();
Go();
}
static
void
Go() {
if
(!done) { done =
true
; Console.WriteLine (
"Done"
); }
}
}
|
上述两个例子足以说明, 另外一个关键概念, 那就是线程安全(或反之,它的不足之处! ) 输出其实是不肯定的:它可能(虽然不大可能) , "Done" ,能够被打印两次。然而,若是咱们在Go方法里调换指令的顺序, "Done"被打印两次的机会会大幅地上升:
1
2
3
|
static
void
Go() {
if
(!done) { Console.WriteLine (
"Done"
); done =
true
; }
}
|
问题就是一个线程在判断if块的时候,正好另外一个线程正在执行WriteLine语句——在它将done设置为true以前。
补救措施是当读写公共字段的时候,提供一个排他锁;C#提供了lock语句来达到这个目的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
ThreadSafe {
static
bool
done;
static
object
locker =
new
object
();
static
void
Main() {
new
Thread (Go).Start();
Go();
}
static
void
Go() {
lock
(locker) {
if
(!done) { Console.WriteLine (
"Done"
); done =
true
; }
}
}
}
|
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种状况下,就确保了在同一时刻只有一个线程能进入临界区,因此"Done"只被打印了1次。代码以如此方式在不肯定的多线程环境中被叫作线程安全。
临时暂停,或阻止是多线程的协同工做,同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的缘由,另外一个缘由是线程想要暂停或Sleep一段时间:
1
|
Thread.Sleep (TimeSpan.FromSeconds (30));
// Block for 30 seconds
|
一个线程也能够使用它的Join方法来等待另外一个线程结束:
1
2
3
|
Thread t =
new
Thread (Go);
// Assume Go is some static method
t.Start();
t.Join();
// Wait (block) until thread t ends
|
一个线程,一旦被阻止,它就再也不消耗CPU的资源了。
线程是如何工做的
线程被一个线程协调程序管理着——一个CLR委托给操做系统的函数。线程协调程序确保将全部活动的线程被分配适当的执行时间;而且那些等待或阻止的线程——好比说在排它锁中、或在用户输入——都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片以后迅速地在活动的线程之间进行切换执行。这就致使“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块至关于分给线程的时间片。在Windows XP中时间片一般在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即一般在几微秒区间)
在多核的电脑中,多线程被实现成混合时间片和真实的并发——不一样的线程在不一样的CPU上运行。这几乎能够确定仍然会出现一些时间切片, 因为操做系统的须要服务本身的线程,以及一些其余的应用程序。
线程因为外部因素(好比时间片)被中断被称为被抢占,在大多数状况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。
线程 vs. 进程
属于一个单一的应用程序的全部的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操做系统单元。
线程于进程有某些类似的地方:好比说进程一般以时间片方式与其它在电脑中运行的进程的方式与一个C#程序线程运行的方式大体相同。两者的关键区别在于进程彼此是彻底隔绝的。线程与运行在相同程序其它线程共享(堆heap)内存,这就是线程为什么如此有用:一个线程能够在后台读取数据,而另外一个线程能够在前台展示已读取的数据。
什么时候使用多线程
多线程程序通常被用来在后台执行耗时的任务。主线程保持运行,而且工做线程作它的后台工做。对于Windows Forms程序来讲,若是主线程试图执行冗长的操做,键盘和鼠标的操做会变的迟钝,程序也会失去响应。因为这个缘由,应该在工做线程中运行一个耗时任务时添加一个工做线程,即便在主线程上有一个有好的提示“处理中...”,以防止工做没法继续。这就避免了程序出现由操做系统提示的“没有相应”,来诱使用户强制结束程序的进程而致使错误。模式对话框还容许实现“取消”功能,容许继续接收事件,而实际的任务已被工做线程完成。BackgroundWorker刚好能够辅助完成这一功能。
在没有用户界面的程序里,好比说Windows Service, 多线程在当一个任务有潜在的耗时,由于它在等待另台电脑的响应(好比一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工做线程完成任务意味着主线程能够当即作其它的事情。
另外一个多线程的用途是在方法中完成一个复杂的计算工做。这个方法会在多核的电脑上运行的更快,若是工做量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。
一个C#程序称为多线程的能够经过2种方式:明确地建立和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——好比BackgroundWorker类, 线程池,threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的状况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即便有这样的事情;幸运的是,应用服务器中多线程是至关广泛的;惟一值得关心的是提供适当锁机制的静态变量问题。
什么时候不要使用多线程
多线程也一样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程自己并不复杂,复杂是的线程的交互做用,这带来了不管是否交互是不是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。所以,要么多线程的交互设计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。
当用户频繁地分配和切换线程时,多线程会带来增长资源和CPU的开销。在某些状况下,太多的I/O操做是很是棘手的,当只有一个或两个工做线程要比有众多的线程在相同时间执行任务块的多。稍后咱们将实现生产者/耗费者 队列,它提供了上述功能。
线程用Thread类来建立, 经过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的:
1
|
public
delegate
void
ThreadStart();
|
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法建立TheadStart委托:
1
2
3
4
5
6
7
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (
new
ThreadStart (Go));
t.Start();
// Run Go() on the new thread.
Go();
// Simultaneously run Go() in the main thread.
}
static
void
Go() { Console.WriteLine (
"hello!"
); }
|
在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来:
一个线程能够经过C#堆委托简短的语法更便利地建立出来:
1
2
3
4
5
6
7
|
static
void
Main() {
Thread t =
new
Thread (Go);
// No need to explicitly use ThreadStart
t.Start();
...
}
static
void
Go() { ... }
在这种状况,ThreadStart被编译器自动推断出来,另外一个快捷的方式是使用匿名方法来启动线程:
|
1
2
3
4
|
static
void
Main() {
Thread t =
new
Thread (
delegate
() { Console.WriteLine (
"Hello!"
); });
t.Start();
}
|
线程有一个IsAlive属性,在调用Start()以后直到线程结束以前一直为true。一个线程一旦结束便不能从新开始了。
将数据传入ThreadStart中
话又说回来,在上面的例子里,咱们想更好地区分开每一个线程的输出结果,让其中一个线程输出大写字母。咱们传入一个状态字到Go中来完成整个任务,但咱们不能使用ThreadStart委托,由于它不接受参数,所幸的是,.NET framework定义了另外一个版本的委托叫作ParameterizedThreadStart, 它能够接收一个单独的object类型参数:
1
2
|
public
delegate
void
ParameterizedThreadStart (
object
obj);
以前的例子看起来是这样的:
|
1
|
|
1
2
3
4
5
6
7
8
9
10
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (Go);
t.Start (
true
);
// == Go (true)
Go (
false
);
}
static
void
Go (
object
upperCase) {
bool
upper = (
bool
) upperCase;
Console.WriteLine (upper ?
"HELLO!"
:
"hello!"
);
}
|
在整个例子中,编译器自动推断出ParameterizedThreadStart委托,由于Go方法接收一个单独的object参数,就像这样写:
1
2
|
Thread t =
new
Thread (
new
ParameterizedThreadStart (Go));
t.Start (
true
);
|
ParameterizedThreadStart的特性是在使用以前咱们必需对咱们想要的类型(这里是bool)进行装箱操做,而且它只能接收一个参数。
一个替代方案是使用一个匿名方法调用一个普通的方法以下:
1
2
3
4
5
|
static
void
Main() {
Thread t =
new
Thread (
delegate
() { WriteText (
"Hello"
); });
t.Start();
}
static
void
WriteText (
string
text) { Console.WriteLine (text); }
|
优势是目标方法(这里是WriteText),能够接收任意数量的参数,而且没有装箱操做。不过这须要将一个外部变量放入到匿名方法中,向下面的同样:
1
2
3
4
5
6
7
|
static
void
Main() {
string
text =
"Before"
;
Thread t =
new
Thread (
delegate
() { WriteText (text); });
text =
"After"
;
t.Start();
}
static
void
WriteText (
string
text) { Console.WriteLine (text); }
|
匿名方法打开了一种怪异的现象,当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无心的互动。有意的互动(一般经过字段)被认为是足够了!一旦线程开始运行了,外部变量最好被处理成只读的——除非有人愿意使用适当的锁。
另外一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性能够告诉线程要作什么,以下列重写了原来的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
ThreadTest {
bool
upper;
static
void
Main() {
ThreadTest instance1 =
new
ThreadTest();
instance1.upper =
true
;
Thread t =
new
Thread (instance1.Go);
t.Start();
ThreadTest instance2 =
new
ThreadTest();
instance2.Go();
// 主线程——运行 upper=false
}
void
Go() { Console.WriteLine (upper ?
"HELLO!"
:
"hello!"
); }
|
命名线程
线程能够经过它的Name属性进行命名,这非产有利于调试:能够用Console.WriteLine打印出线程的名字,Microsoft Visual Studio能够将线程的名字显示在调试工具栏的位置上。线程的名字能够在被任什么时候间设置——但只能设置一次,重命名会引起异常。
程序的主线程也能够被命名,下面例子里主线程经过CurrentThread命名:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadNaming {
static
void
Main() {
Thread.CurrentThread.Name =
"main"
;
Thread worker =
new
Thread (Go);
worker.Name =
"worker"
;
worker.Start();
Go();
}
static
void
Go() {
Console.WriteLine (
"Hello from "
+ Thread.CurrentThread.Name);
}
}
|
前台和后台线程
线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当全部前台线程结束后,它们不维持程序的存活。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。
线程的IsBackground属性控制它的先后台状态,以下实例:
1
2
3
4
5
6
7
|
class
PriorityTest {
static
void
Main (
string
[] args) {
Thread worker =
new
Thread (
delegate
() { Console.ReadLine(); });
if
(args.Length > 0) worker.IsBackground =
true
;
worker.Start();
}
}
|
若是程序被调用的时候没有任何参数,工做线程为前台线程,而且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,可是程序保持运行,由于一个前台线程仍然活着。
另外一方面若是有参数传入Main(),工做线程被赋值为后台线程,当主线程结束程序马上退出,终止了ReadLine。
后台线程终止的这种方式,使任何最后操做都被规避了,这种方式是不太合适的。好的方式是明确等待任何后台工做线程完成后再结束程序,可能用一个timeout(大多用Thread.Join)。若是由于某种缘由某个工做线程没法完成,能够用试图终止它的方式,若是失败了,再抛弃线程,容许它与 与进程一块儿消亡。(记录是一个难题,但这个场景下是有意义的)
拥有一个后台工做线程是有益的,最直接的理由是它当提到结束程序它老是可能有最后的发言权。交织以不会消亡的前台线程,保证程序的正常退出。抛弃一个前台工做线程是尤其险恶的,尤为对Windows Forms程序,由于程序直到主线程结束时才退出(至少对用户来讲),可是它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却能够在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。
对于程序失败退出的广泛缘由就是存在“被忘记”的前台线程。
线程优先级
线程的Priority 属性肯定了线程相对于其它同一进程的活动的线程拥有多少执行时间,如下是级别:
1
|
enum
ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
|
只有多个线程同时为活动时,优先级才有做用。
设置一个线程的优先级为高一些,并不意味着它能执行实时的工做,由于它受限于程序的进程的级别。要执行实时的工做,必须提高在System.Diagnostics 命名空间下Process的级别,像下面这样:(我没有告诉你如何作到这一点:))
1
|
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
|
ProcessPriorityClass.High 实际上是一个短暂缺口的过程当中的最高优先级别:Realtime。设置进程级别到Realtime通知操做系统:你不想让你的进程被抢占了。若是你的程序进入一个偶然的死循环,能够预期,操做系统被锁住了,除了关机没有什么能够拯救你了!基于此,High大致上被认为最高的有用进程级别。
若是一个实时的程序有一个用户界面,提高进程的级别是不太好的,由于当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。(虽然在写这篇文章的时候,在互联网电话程序Skype侥幸地这么作, 也许是由于它的界面至关简单吧。) 下降主线程的级别、提高进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑愈来愈慢,由于操做系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工做和用户界面在不一样的进程(拥有不一样的优先级)运行,经过Remoting或共享内存方式进行通讯,共享内存须要Win32 API中的 P/Invoking。(能够搜索看看CreateFileMapping 和 MapViewOfFile)
异常处理
任何线程建立范围内try/catch/finally块,当线程开始执行便再也不与其有任何关系。考虑下面的程序:
1
2
3
4
5
6
7
8
9
10
11
|
public
static
void
Main() {
try
{
new
Thread (Go).Start();
}
catch
(Exception ex) {
// 不会在这获得异常
Console.WriteLine (
"Exception!"
);
}
static
void
Go() {
throw
null
; }
}
|
1
|
这里
try
/
catch
语句一点用也没有,新建立的线程将引起NullReferenceException异常。当你考虑到每一个线程有独立的执行路径的时候,便知道这行为是有道理的,
|
1
|
补救方法是在线程处理的方法内加入他们本身的异常处理:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
static
void
Main() {
new
Thread (Go).Start();
}
static
void
Go() {
try
{
...
throw
null
;
// 这个异常在下面会被捕捉到
...
}
catch
(Exception ex) {
记录异常日志,而且或通知另外一个线程
咱们发生错误
...
}
|
从.NET 2.0开始,任何线程内的未处理的异常都将致使整个程序关闭,这意味着忽略异常再也不是一个选项了。所以为了不由未处理异常引发的程序崩溃,try/catch块须要出如今每一个线程进入的方法内,至少要在产品程序中应该如此。对于常用“全局”异常处理的Windows Forms程序员来讲,这可能有点麻烦,像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
using
System;
using
System.Threading;
using
System.Windows.Forms;
static
class
Program {
static
void
Main() {
Application.ThreadException += HandleError;
Application.Run (
new
MainForm());
}
static
void
HandleError (
object
sender, ThreadExceptionEventArgs e) {
记录异常或者退出程序或者继续运行...
}
}
|
Application.ThreadException事件在异常被抛出时触发,以一个Windows信息(好比:键盘,鼠标活着 "paint" 等信息)的方式,简言之,一个Windows Forms程序的几乎全部代码。虽然这看起来很完美,它令人产生一种虚假的安全感——全部的异常都被中央异常处理捕捉到了。由工做线程抛出的异常即是一个没有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代码,包括构造器的形式,在Windows信息开始前先执行)
.NET framework为全局异常处理提供了一个更低级别的事件:AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)的任何线程有任何未处理的异常触发。尽管它提供了好的不得已的异常处理解决机制,可是这不意味着这能保证程序不崩溃,也不意味着能取消.NET异常对话框。
在产品程序中,明确地使用异常处理在全部线程进入的方法中是必要的,能够使用包装类和帮助类来分解工做来完成任务,好比使用BackgroundWorker类(在第三部分进行讨论)
下面的表格列展了.NET对协调或同步线程动做的可用的工具:
简易阻止方法
构成 |
目的 |
Sleep |
阻止给定的时间周期 |
Join |
等待另外一个线程完成 |
锁系统
构成 |
目的 |
跨进程? |
速度 |
lock |
确保只有一个线程访问某个资源或某段代码。 |
否 |
快 |
Mutex |
确保只有一个线程访问某个资源或某段代码。可被用于防止一个程序的多个实例同时运行。 |
是 |
中等 |
Semaphore |
确保不超过指定数目的线程访问某个资源或某段代码。 |
是 |
中等 |
(同步的状况下也提够自动锁。)
信号系统
构成 |
目的 |
跨进程? |
速度 |
EventWaitHandle |
容许线程等待直到它受到了另外一个线程发出信号。 |
是 |
中等 |
Wait 和 Pulse* |
容许一个线程等待直到自定义阻止条件获得知足。 |
否 |
中等 |
非阻止同步系统*
构成 |
目的 |
跨进程? |
速度 |
Interlocked* |
完成简单的非阻止原子操做。 |
是(内存共享状况下) |
很是快 |
volatile* |
容许安全的非阻止在锁以外使用个别字段。 |
很是快 |
* 表明页面将转到第四部分
1.1 阻止 (Blocking)
当一个线程经过上面所列的方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程马上放弃它被分配的
CPU时间,将它的ThreadState属性添加为WaitSleepJoin状态,不在安排时间直到中止阻止。中止阻止在任意四种
状况下发生(关掉电脑的电源可不算!):
当线程经过(不建议)Suspend 方法暂停,不认为是被阻止了。
调用Thread.Sleep阻止当前的线程指定的时间(或者直到中断):
1
2
3
4
5
6
|
static
void
Main() {
Thread.Sleep (0);
// 释放CPU时间片
Thread.Sleep (1000);
// 休眠1000毫秒
Thread.Sleep (TimeSpan.FromHours (1));
// 休眠1小时
Thread.Sleep (Timeout.Infinite);
// 休眠直到中断
}
|
更确切地说,Thread.Sleep放弃了占用CPU,请求不在被分配时间直到给定的时间通过。Thread.Sleep(0)放弃
CPU的时间刚刚够其它在时间片队列里的活动线程(若是有的话)被执行。
Thread.Sleep在阻止方法中是惟一的暂停汲取Windows Forms程序的Windows消息的方法,或COM环境中用于
单元模式。这在Windows Forms程序中是一个很大的问题,任何对主UI线程的阻止都将使程序失去相应。所以通常避
免这样使用,不管信息汲取是否被“技术地”暂定与否。由COM遗留下来的宿主环境更为复杂,在一些时候它决定中止,
而却保持信息的汲取存活。微软的 Chris Brumm 在他的博客中讨论这个问题。(搜索: 'COM "Chris Brumme"')
线程类同时也提供了一个SpinWait方法,它使用轮询CPU而非放弃CPU时间的方式,保持给定的迭代次数进行“无用
地繁忙”。50迭代可能等同于停顿大约一微秒,虽然这将取决于CPU的速度和负载。从技术上讲,SpinWait并非一个阻
止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,而且也不会被其它的线程过早的中断
(Interrupt)。SpinWait不多被使用,它的做用是等待一个在极短期(可能小于一微秒)内可准备好的可预期的资源,
而不用调用Sleep方法阻止线程而浪费CPU时间。不过,这种技术的优点只有在多处理器计算机:对单一处理器的电脑,
直到轮询的线程结束了它的时间片以前,一个资源没有机会改变状态,这有违它的初衷。而且调用SpinWait常常会花费较
长的时间这自己就浪费了CPU时间。
线程能够等待某个肯定的条件来明确轮询使用一个轮询的方式,好比:
1
|
while
(!proceed);
|
或者:
1
|
while
(DateTime.Now < nextStartTime);
|
这是很是浪费CPU时间的:对于CLR和操做系统而言,线程进行了一个重要的计算,因此分配了相应的资源!在这种状态
下的轮询线程不算是阻止,不像一个线程等待一个EventWaitHandle(通常使用这样的信号任务来构建)。
阻止和轮询组合使用能够产生一些变换:
1
|
while
(!proceed) Thread.Sleep (x);
// "轮询休眠!"
|
x越大,CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。
除了稍有延迟,这种轮询和休眠的方式能够结合的很是好。(但有并发问题,在第四部分讨论)可能它最大的用处在于
程序员能够放弃使用复杂的信号结构 来工做了。
你能够经过Join方法阻止线程直到另外一个线程结束:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
JoinDemo {
static
void
Main() {
Thread t =
new
Thread (
delegate
() { Console.ReadLine();});
t.Start();
t.Join();
// 等待直到线程完成
Console.WriteLine (
"Thread t's ReadLine complete!"
);
}
}
|
Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,若是线程已终止,则返回true 。
Join所带的超时参数很是像Sleep方法,实际上下面两行代码几乎差很少:
1
2
3
|
Thread.Sleep (1000);
Thread.CurrentThread.Join (1000);
|
(他们的区别明显在于单线程的应用程序域与COM互操做性,源于先前描述Windows信息汲取部分:在阻止时,Join
保持信息汲取,Sleep暂停信息汲取。)
锁实现互斥的访问,被用于确保在同一时刻只有一个线程能够进入特殊的代码片断,考虑下面的类:
1
2
3
4
5
6
7
8
|
class
ThreadUnsafe {
static
int
val1, val2;
static
void
Go() {
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
|
这不是线程安全的:若是Go方法被两个线程同时调用,可能会获得在某个线程中除数为零的错误,由于val2可能被一个
线程设置为零,而另外一个线程恰好执行到if和Console.WriteLine语句。
下面用lock来修正这个问题:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadSafe {
static
object
locker =
new
object
();
static
int
val1, val2;
static
void
Go() {
lock
(locker) {
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
}
|
在同一时刻只有一个线程能够锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。若是有大于一个的线程竞争这个锁,那么他们将造成称为“就绪队列”的队列,以先到先得的方式受权锁。互斥锁有时被称之对由锁所保护的内容强迫串行化访问,由于一个线程的访问不能与另外一个重叠。在这个例子中,咱们保护了Go方法的逻辑,以及val1 和val2字段的逻辑。
一个等候竞争锁的线程被阻止将在ThreadState上为WaitSleepJoin状态。稍后咱们将讨论一个线程经过另外一个线程调用
Interrupt或Abort方法来强制地被释放。这是一个至关高效率的技术能够被用于结束工做线程。
C#的lock 语句其实是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在以前例
子中的Go方法:
1
2
3
4
5
6
7
8
|
Monitor.Enter (locker);
try
{
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
finally
{ Monitor.Exit (locker);
}
|
在同一个对象上,在调用第一个以前Monitor.Enter而先调用了Monitor.Exit将引起异常。
Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,若是得到了锁返回true,反之没有得到返回false,由于超时了。TryEnter也能够没有超时参数,“测试”一下锁,若是锁不能被获取的话就马上超时。
2.1 选择同步对象
任何对全部有关系的线程均可见的对象均可以做为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(好比一个私有实例字段)防止无心间从外部锁定相同的对象。服从这些规则,同步对象能够兼对象和保护两种做用。好比下面List :
1
2
3
4
5
6
7
8
9
10
11
|
class
ThreadSafe {
List <
string
> list =
new
List <
string
>();
void
Test() {
lock
(list) {
list.Add (
"Item 1"
);
...
|
一个专门字段是经常使用的(如在先前的例子中的locker) , 由于它能够精确控制锁的范围和粒度。用对象或类自己的类型做为一个同步对象,即:
lock (this) { ... }
或:
lock (typeof (Widget)) { ... } // 保护访问静态
是很差的,由于这潜在的能够在公共范围访问这些对象。
锁并无以任何方式阻止对同步对象自己的访问,换言之,x.ToString()不会因为另外一个线程调用lock(x) 而被阻止,二者都要调用ock(x) 来完成阻止工做。
2.2 嵌套锁定
线程能够重复锁定相同的对象,能够经过屡次调用Monitor.Enter或lock语句来实现。当对应编号的Monitor.Exit被调用或最外面的lock语句完成后,对象那一刻被解锁。这就容许最简单的语法实现一个方法的锁调用另外一个锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static
object
x =
new
object
();
static
void
Main() {
lock
(x) {
Console.WriteLine (
"I have the lock"
);
Nest();
Console.WriteLine (
"I still have the lock"
);
}
在这锁被释放
}
static
void
Nest() {
lock
(x) {
...
} 释放了锁?没有彻底释放!
}
|
线程只能在最开始的锁或最外面的锁时被阻止。
做为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。甚至是极日常的事情——单一字段的赋值操做,都必须考虑到同步问题。在下面的例子中Increment和Assign 都不是线程安全的:
1
2
3
4
5
|
class
ThreadUnsafe {
static
int
x;
static
void
Increment() { x++; }
static
void
Assign() { x = 123; }
}
|
下面是Increment 和 Assign 线程安全的版本:
1
2
3
4
5
6
7
|
class
ThreadUnsafe {
static
object
locker =
new
object
();
static
int
x;
static
void
Increment() {
lock
(locker) x++; }
static
void
Assign() {
lock
(locker) x = 123; }
}
|
做为锁定另外一个选择,在一些简单的状况下,你能够使用非阻止同步,在第四部分讨论(即便像这样的语句须要同步的缘由)。
若是有不少变量在一些锁中老是进行读和写的操做,那么你能够称之为原子操做。咱们假设x 和 y不停地读和赋值,他们在锁内经过
locker锁定:
lock (locker) { if (x != 0) y /= x; }
你能够认为x 和 y 经过原子的方式访问,由于代码段没有被其它的线程分开 或 抢占,别的线程改变x 和 y是无效的输出,你永远不会获得除数为零的错误,保证了x 和 y老是被相同的排他锁访问。
2.5 性能考量
锁定自己是很是快的,一个锁在没有堵塞的状况下通常只需几十纳秒(十亿分之一秒)。若是发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间以前它可能花费数毫秒(千分之一秒)。而相反,与此相形见绌的是该使用锁而没使用的结果就是带来数小时的时间,甚至超时。若是耗尽并发,锁定会带来副作用,死锁和争用锁,耗尽并发因为太多的代码被放置到锁语句中了,引发其它线程没必要要的被阻止。死锁是两线程彼此等待被锁定的内容,致使二者都没法继续下去。争用锁是两个线程任一个均可以锁定某个内容,若是“错误”的线程获取了锁,则致使程序错误。
对于太多的同步对象死锁是很是容易出现的症状,一个好的规则是开始于较少的锁,在一个可信的状况下涉及过多的阻止出现时,增长锁的粒度。
线程安全的代码是指在面对任何多线程状况下,这代码都没有不肯定的因素。线程安全首先完成锁,而后减小在线程间交互的可能性。
一个线程安全的方法,在任何状况下能够可重入式调用。通用类型在它们中不多是线程安全的,缘由以下:
所以线程安全常常只在须要实现的地方来实现,为了处理一个特定的多线程状况。
不过,有一些方法来“欺骗”,有庞大和复杂的类安全地运行在多线程环境中。一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的全部的属性、方法和字段的访问。
原始类型除外,不多的.NET framework类型实例相比于并发的只读访问,是线程安全的。责任在开放人员实现线程安全表明性地使用互斥锁。
另外一个方式欺骗是经过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每一个请求来自它本身的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法必定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,所以类不可以在每一个请求间持久保留数据。线程交互仅限于能够被选择建立的静态字段,多半是在内存里缓存经常使用数据和提供基础设施服务,例如认证和审核。
2.7线程安全与.NET Framework类型
锁定可被用于将非线程安全的代码转换成线程安全的代码。比较好的例子是在.NET framework方面,几乎全部非基本类型的实例都不是线程安全的,而若是全部的访问给定的对象都经过锁进行了保护的话,他们能够被用于多线程代码中。看这个例子,两个线程同时为相同的List增长条目,而后枚举它:.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
ThreadSafe {
static
List <
string
> list =
new
List <
string
>();
static
void
Main() {
new
Thread (AddItems).Start();
new
Thread (AddItems).Start();
}
static
void
AddItems() {
for
(
int
i = 0; i < 100; i++)
lock
(list)list.Add (
"Item "
+ list.Count);
string
[] items;
lock
(list) items = list.ToArray();
foreach
(
string
s
in
items) Console.WriteLine (s);
}
}
|
在这种状况下,咱们锁定了list对象自己,这个简单的方案是很好的。若是咱们有两个相关的list,也许咱们就要锁定一个共同的目标——单独的一个字段,若是没有其它的list出现,显然锁定它本身是明智的选择。枚举.NET的集合也不是线程安全的,在枚举的时候另外一个线程改动list的话,会抛出异常。为了避免直接锁定枚举过程,在这个例子中,咱们首先将项目复制到数组当中,这就避免了固定住锁由于咱们在枚举过程当中有潜在的耗时。
这里的一个有趣的假设:想象若是List实际上为线程安全的,如何解决呢?代码会不多!举例说明,咱们说咱们要增长一个项目到咱们假象的线程安全的list里,以下:
if (!myList.Contains (newItem)) myList.Add (newItem);
不管与否list是否为线程安全的,这个语句显然不是!(所以,能够说彻底线程安全的通用集合类是基本不存在的。.net4.0中,微软提供了一组线程安全的并行集合类,可是都是特殊的通过处理过的,访问方式都通过了限定。),上面的语句要实现线程安全,整个if语句必须放到一个锁中,用来保护抢占在判断有无和增长新的之间。上述的锁须要用于任何咱们须要修改list的地方,好比下面的语句须要被一样的锁包括住:
myList.Clear();
来保证它没有抢占以前的语句,换言之,咱们必须锁定差很少全部非线程安全的集合类们。内置的线程安全,显而易见是浪费时间!
在写自定义组件的时候,你可能会反对这个观点——为何建造线程安全让它容易的结果会变的多余呢 ?
有一个争论:在一个对象包上自定义的锁仅在全部并行的线程知道、并使用这个锁的时候才能工做,而若是锁对象在更大的范围内的时候,这个锁对象可能不在这个锁范围内。最糟糕的状况是静态成员在公共类型中出现了,好比,想象静态结构在DateTime上,DateTime.Now不是线程安全的,当有2个并发的调用可带来错乱的输出或异常,补救方式是在其外进行锁定,可能锁定它的类型自己—— lock(typeof(DateTime))来圈住调用DateTime.Now,这会工做的,但只有全部的程序员赞成这样作的时候。然而这并靠不住,锁定一个类型被认为是一件很是很差的事情。因为这些理由,DateTime上的静态成员是保证线程安全的,这是一个遍布.NET framework一个广泛模式——静态成员是线程安全的,而一个实例成员则不是。从这个模式也能在写自定义类型时获得一些体会,不要建立一个不能线程安全的难题!
当写公用组件的时候,好的习惯是不要忘记了线程安全,这意味着要单独当心处理那些在其中或公共的静态成员。
一个被阻止的线程能够经过两种方式被提早释放:
这必须经过另外活动的线程实现,等待的线程是没有能力对它的被阻止状态作任何事情的。
在一个被阻止的线程上调用Interrupt 方法,将强迫释放它,抛出ThreadInterruptedException异常,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
Program {
static
void
Main() {
Thread t =
new
Thread (
delegate
() {
try
{
Thread.Sleep (Timeout.Infinite);
}
catch
(ThreadInterruptedException) {
Console.Write (
"Forcibly "
);
}
Console.WriteLine (
"Woken!"
);
});
t.Start();
t.Interrupt();
}
}
Forcibly Woken!
|
中断一个线程仅仅释放它的当前的(或下一个)等待状态:它并不结束这个线程(固然,除非未处理
ThreadInterruptedException异常)。
若是Interrupt被一个未阻止的线程调用,那么线程将继续执行直到下一次被阻止时,它抛出
ThreadInterruptedException异常。用下面的测试避免这个问题:
if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
worker.Interrupt();
这不是一个线程安全的方式,由于可能被抢占了在if语句和worker.Interrupt间。
随意中断线程是危险的,由于任何框架或第三方方法在调用堆栈时可能会意外地在已订阅的代码上收到中断。这一切将被认为是线程被暂时阻止在一个锁中或同步资源中,而且全部挂起的中断将被踢开。若是这个方法没有被设计成能够被中断(没有适当处理finally块)的对象可能剩下无用的状态,或资源不彻底地被释放。
中断一个线程是安全的,当你知道它确切的在哪的时候。稍后咱们讨论 信号系统,它提供这样的一种方式。
被阻止的线程也能够经过Abort方法被强制释放,这与调用Interrupt类似,除了用ThreadAbortException异常代替了
ThreadInterruptedException异常,此外,异常将被从新抛出在catch里(在试图以有好方式处理异常的时候),直到Thread.ResetAbort在catch中被调用;在这期间线程的ThreadState为AbortRequested。
在Interrupt 与 Abort 之间最大不一样在于它们调用一个非阻止线程所发生的事情。Interrupt继续工做直到下一次阻止发生,Abort在线程当前所执行的位置(可能甚至不在你的代码中)抛出异常。终止一个非阻止的线程会带来严重的后果,这在后面的 “终止线程”章节中将详细讨论。
图1: 线程状态关系图
你能够经过ThreadState属性获取线程的执行状态。图1将ThreadState列举为“层”。ThreadState被设计的很恐怖,它以按位计算的方式组合三种状态“层”,每种状态层的成员它们间都是互斥的,下面是全部的三种状态“层”:
总的来讲,ThreadState是按位组合零或每一个状态层的成员!一个简单的ThreadState例子:
Unstarted
Running
WaitSleepJoin
Background, Unstarted
SuspendRequested, Background, WaitSleepJoin
(所枚举的成员有两个历来没被用过,至少是当前CLR实现上:StopRequested 和 Aborted。)
还有更加复杂的,ThreadState.Running潜在的值为0 ,所以下面的测试不工做:
if ((t.ThreadState & ThreadState.Running) > 0) ...
你必须用按位与非操做符来代替,或者使用线程的IsAlive属性。可是IsAlive可能不是你想要的,它在被阻止或挂起的时候返回true(只有在线程未开始或已结束时它才为true)。
假设你避开不推荐使用的Suspend 和 Resume方法,你能够写一个helper方法除去全部除了第一种状态层的成员,容许简单测试计算完成。线程的后台状态能够经过IsBackground 独立地得到,因此实际上只有第一种状态层拥有有用的信息。
1
2
3
4
5
6
7
|
public
static
ThreadState SimpleThreadState (ThreadState ts)
{
return
ts & (ThreadState.Aborted | ThreadState.AbortRequested |
ThreadState.Stopped | ThreadState.Unstarted |
ThreadState.WaitSleepJoin);
}
|
ThreadState对调试或程序概要分析是无价之宝,与之不相称的是多线程的协同工做,由于没有一个机制存在:经过判断ThreadState来执行信息,而不考虑ThreadState期间的变化。
lock语句(也称为Monitor.Enter / Monitor.Exit)是线程同步结构的一个例子。当lock对一段代码或资源实施排他访问时, 但有些同步任务是至关笨拙的或难以实现的,好比说须要传输信号给等待的工做线程使其开始任务执行。
Win32 API拥有丰富的同步系统,这在.NET framework以EventWaitHandle, Mutex 和 Semaphore类展露出来。而一些比有些更有用:例如Mutex类,在EventWaitHandle提供惟一的信号功能时,大多会成倍提升lock的效率。
这三个类都依赖于WaitHandle类,尽管从功能上讲, 它们至关的不一样。但它们作的事情都有一个共同点,那就是,被“点名”,这容许它们绕过操做系统进程工做,而不是只能在当前进程里绕过线程。
EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。这两个类都派生自它们的基类:它们仅有的不一样是它们用不一样的参数调用基类的构造函数。性能方面,使用Wait Handles系统开销会花费在微秒间,不会在它们使用的上下文中产生什么后果。
AutoResetEvent在WaitHandle中是最有用的的类,它连同lock 语句是一个主要的同步结构。
AutoResetEvent就像一个用票经过的旋转门:插入一张票,让正确的人经过。类名字里的“auto”实际上就是旋转门自动关闭或“从新安排”后来的人让其经过。一个线程等待或阻止经过在门上调用WaitOne方法(直到等到这个“one”,门才开) ,票的插入则由调用Set方法。若是由许多线程调用WaitOne,在门前便造成了队列,一张票可能来自任意某个线程——换言之,任何(非阻止)线程要经过AutoResetEvent对象调用Set方法来释放一个被阻止的的线程。
也就是调用WaitOne方法的全部线程会阻塞到一个等待队列,其余非阻塞线程经过调用Set方法来释放一个阻塞。而后AutoResetEvent继续阻塞后面的线程。
若是Set调用时没有任何线程处于等待状态,那么句柄保持打开直到某个线程调用了WaitOne 。这个行为避免了在线程起身去旋转门和线程插入票(哦,插入票是很是短的微秒间的事,真倒霉,你将必须不肯定地等下去了!)间的竞争。可是在没人等的时候重复地在门上调用Set方法不会容许在一队人都经过,在他们到达的时候:仅有下一我的能够经过,多余的票都被“浪费了"。
WaitOne 接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的同步内容,为了不过多的阻止发生。
Reset做用是关闭旋转门,也就是不管此时是否已经set过,都将阻塞下一次WaitOne——它应该是开着的。
AutoResetEvent能够经过2种方式建立,第一种是经过构造函数:
1
|
EventWaitHandle wh =
new
AutoResetEvent (
false
);
|
若是布尔参数为真,Set方法在构造后马上被自动的调用,也就是说第一个WaitOne会被放行,不会被阻塞,另外一个方法是经过它的基类EventWaitHandle:
1
|
EventWaitHandle wh =
new
EventWaitHandle (
false
, EventResetMode.Auto);
|
EventWaitHandle的构造器也容许建立ManualResetEvent(用EventResetMode.Manual定义).
在Wait Handle不在须要时候,你应当调用Close方法来释放操做系统资源。可是,若是一个Wait Handle将被用于程序(就像这一节的大多例子同样)的生命周期中,你能够发点懒省略这个步骤,它将在程序域销毁时自动的被销毁。
接下来这个例子,一个线程开始等待直到另外一个线程发出信号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
BasicWaitHandle {
static
EventWaitHandle wh =
new
AutoResetEvent (
false
);
static
void
Main() {
new
Thread (Waiter).Start();
Thread.Sleep (1000);
// 等一会...
wh.Set();
// OK ——唤醒它
}
static
void
Waiter() {
Console.WriteLine (
"Waiting..."
);
wh.WaitOne();
// 等待通知
Console.WriteLine (
"Notified"
);
}
}
Waiting... (pause) Notified.
|
EventWaitHandle的构造器容许以“命名”的方式进行建立,它有能力跨多个进程。名称是个简单的字符串,可能会无心地与别的冲突!若是名字使用了,你将引用相同潜在的EventWaitHandle,除非操做系统建立一个新的,看这个例子:
1
2
|
EventWaitHandle wh =
new
EventWaitHandle (
false
, EventResetMode.Auto,
"MyCompany.MyApp.SomeName"
);
|
若是有两个程序都运行这段代码,他们将彼此能够发送信号,等待句柄能够跨这两个进程中的全部线程。
设想咱们但愿在后台完成任务,但又不在每次咱们获得任务时再建立一个新的线程。咱们能够经过一个轮询的线程来完成:等待一个任务,执行它,而后等待下一个任务。这是一个广泛的多线程方案。也就是在建立线程上切份内务操做,任务执行被序列化,在多个工做线程和过多的资源消耗间排除潜在的不想要的操做。 咱们必须决定要作什么,可是,若是当新的任务来到的时候,工做线程已经在忙以前的任务了,设想这种情形下咱们需选择阻止调用者直到以前的任务被完成。像这样的系统能够用两个AutoResetEvent对象实现:一个“ready”AutoResetEvent,当准备好的时候,它被工做线程调用Set方法;和“go”AutoResetEvent,当有新任务的时候,它被调用线程调用Set方法。在下面的例子中,一个简单的string字段被用于决定任务(使用了volatile 关键字声明,来确保两个线程均可以看到相同版本):
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
|
class
AcknowledgedWaitHandle {
static
EventWaitHandle ready =
new
AutoResetEvent (
false
);
static
EventWaitHandle go =
new
AutoResetEvent (
false
);
static
volatile
string
task;
static
void
Main() {
new
Thread (Work).Start();
// Signal the worker 5 times
for
(
int
i = 1; i <= 5; i++) {
ready.WaitOne();
// First wait until worker is ready
task =
"a"
.PadRight (i,
'h'
);
// Assign a task
go.Set();
// Tell worker to go!
}
// Tell the worker to end using a null-task
ready.WaitOne(); task =
null
; go.Set();
}
static
void
Work() {
while
(
true
) {
ready.Set();
// Indicate that we're ready
go.WaitOne();
// Wait to be kicked off...
if
(task ==
null
)
return
;
// Gracefully exit
Console.WriteLine (task);
}
}
}
ah
ahh
ahhh
ahhhh
|
注意咱们要给task赋null来告诉工做线程退出。在工做线程上调用Interrupt 或Abort 效果是同样的,假若咱们先调用ready.WaitOne的话。由于在调用ready.WaitOne后咱们就知道工做线程的确切位置,不是在就是刚刚在go.WaitOne语句以前,所以避免了中断任意代码的复杂性。调用 Interrupt 或 Abort须要咱们在工做线程中捕捉异常。
另外一个广泛的线程方案是在后台工做进程从队列中分配任务。这叫作生产者/消费者队列:在工做线程中生产者入列任务,消费者出列任务。这和上个例子很像,除了当工做线程正忙于一个任务时调用者没有被阻止以外。
生产者/消费者队列是可缩放的,由于多个消费者可能被建立——每一个都服务于相同的队列,但开启了一个分离的线程。这是一个很好的方式利用多处理器的系统来限制工做线程的数量一直避免了极大的并发线程的缺陷(过多的内容切换和资源链接)。
在下面例子里,一个单独的AutoResetEvent被用于通知工做线程,它只有在用完任务时(队列为空)等待。一个通用的集合类被用于队列,必须经过锁
控制它的访问以确保线程安全。工做线程在队列为null任务时结束:
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
51
52
53
54
55
56
|
using
System;
using
System.Threading;
using
System.Collections.Generic;
class
ProducerConsumerQueue : IDisposable {
EventWaitHandle wh =
new
AutoResetEvent (
false
);
Thread worker;
object
locker =
new
object
();
Queue<
string
> tasks =
new
Queue<
string
>();
public
ProducerConsumerQueue() {
worker =
new
Thread (Work);
worker.Start();
}
public
void
EnqueueTask (
string
task) {
lock
(locker) tasks.Enqueue (task);
wh.Set();
}
public
void
Dispose() {
EnqueueTask (
null
);
// Signal the consumer to exit.
worker.Join();
// Wait for the consumer's thread to finish.
wh.Close();
// Release any OS resources.
}
void
Work() {
while
(
true
) {
string
task =
null
;
lock
(locker)
if
(tasks.Count > 0) {
task = tasks.Dequeue();
if
(task ==
null
)
return
;
}
if
(task !=
null
) {
Console.WriteLine (
"Performing task: "
+ task);
Thread.Sleep (1000);
// simulate work...
}
else
wh.WaitOne();
// No more tasks - wait for a signal
}
}
}
Here's a main method to test the queue:
class
Test {
static
void
Main() {
using
(ProducerConsumerQueue q =
new
ProducerConsumerQueue()) {
q.EnqueueTask (
"Hello"
);
for
(
int
i = 0; i < 10; i++) q.EnqueueTask (
"Say "
+ i);
q.EnqueueTask (
"Goodbye!"
);
}
// Exiting the using statement calls q's Dispose method, which
// enqueues a null task and waits until the consumer finishes.
}
}
|
Performing task: Hello
Performing task: Say 1
Performing task: Say 2
Performing task: Say 3
...
...
Performing task: Say 9
Goodbye!
注意咱们明确的关闭了Wait Handle在ProducerConsumerQueue被销毁的时候,由于在程序的生命周期中咱们可能潜在地建立和销毁许多这个类的实例。
5.4 ManualResetEvent
ManualResetEvent是AutoResetEvent变化的一种形式,它的不一样之处在于:在线程被WaitOne的调用而经过的时候,它不会自动地reset,这个过程就像大门同样——调用Set打开门,容许任何数量的已执行WaitOne的线程经过;调用Reset关闭大门,可能会引发一系列的“等待者”直到下次门打开。
你能够用一个布尔字段"gateOpen" (用 volatile 关键字来声明)与"spin-sleeping" – 方式结合——重复地检查标志,而后让线程休眠一段时间的方式,来模拟这个过程。
ManualResetEvent有时被用于给一个完成的操做发送信号,又或者一个已初始化正准备执行工做的线程。
Mutex提供了与C#的lock语句一样的功能,这使它大多时候变得的冗余了。它的优点在于它能够跨进程工做——提供了一计算机范围的锁而胜于程序范围的锁。
Mutex是至关快的,而lock 又要比它快上数百倍,获取Mutex须要花费几微秒,获取lock需花费数十纳秒(假定没有阻止)。
对于一个Mutex类,WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex以后被释放,就像C#的lock语句同样,Mutex只
能从获取互斥锁的这个线程上被释放。
Mutex在跨进程的广泛用处是确保在同一时刻只有一个程序的的实例在运行,下面演示如何使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class
OneAtATimePlease {
// Use a name unique to the application (eg include your company URL)
static
Mutex mutex =
new
Mutex (
false
,
"oreilly.com OneAtATimeDemo"
);
static
void
Main() {
// Wait 5 seconds if contended – in case another instance
// of the program is in the process of shutting down.
if
(!mutex.WaitOne (TimeSpan.FromSeconds (5),
false
)) {
Console.WriteLine (
"Another instance of the app is running. Bye!"
);
return
;
}
try
{
Console.WriteLine (
"Running - press Enter to exit"
);
Console.ReadLine();
}
finally
{ mutex.ReleaseMutex(); }
}
}
|
Mutex有个好的特性是,若是程序结束时而互斥锁没经过ReleaseMutex首先被释放,CLR将自动地释放Mutex。
Semaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人能够再进入这个夜总会,而且在其外会造成一个队列。而后,当人一我的离开时,队列头的人即可以进入了。构造器须要至少两个参数——夜总会的活动的空间,和夜总会的容量。
Semaphore 的特性与Mutex 和 lock有点相似,除了Semaphore没有“全部者”——它是不可知线程的,任何在Semaphore内的线程均可以调用Release,而Mutex 和 lock仅有那些获取了资源的线程才能够释放它。
在下面的例子中,10个线程执行一个循环,在中间使用Sleep语句。Semaphore确保每次只有不超过3个线程能够执行Sleep语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
SemaphoreTest {
static
Semaphore s =
new
Semaphore (3, 3);
// Available=3; Capacity=3
static
void
Main() {
for
(
int
i = 0; i < 10; i++)
new
Thread (Go).Start();
}
static
void
Go() {
while
(
true
) {
s.WaitOne();
Thread.Sleep (100);
// Only 3 threads can get here at once
s.Release();
}
}
}
|
5.7 WaitAny, WaitAll 和 SignalAndWait
除了Set 和 WaitOne方法外,在类WaitHandle中还有一些用来建立复杂的同步过程的静态方法。
WaitAny, WaitAll 和 SignalAndWait使跨多个可能为不一样类型的等待句柄变得容易。
SignalAndWait多是最有用的了:他在某个WaitHandle上调用WaitOne,并在另外一个WaitHandle上自动地调用Set。你能够在一对EventWaitHandle上装配两个线程,而让它们在某个时间点“相遇”,这马马虎虎地合乎规范。AutoResetEvent 或 ManualResetEvent都没法使用这个技巧。第一个线程像这样:
WaitHandle.SignalAndWait (wh1, wh2);
同时第二个线程作相反的事情:
WaitHandle.SignalAndWait (wh2, wh1);
WaitHandle.WaitAny等待一组等待句柄任意一个发出信号,WaitHandle.WaitAll等待全部给定的句柄发出信号。与票据旋转门的例子相似,这些方法可能同时地等待全部的旋转门——经过在第一个打开的时候(WaitAny状况下),或者等待直到它们全部的都打开(WaitAll状况下)。
WaitAll 其实是不肯定的值,由于这与单元模式线程——从COM体系遗留下来的问题,有着奇怪的联系。WaitAll 要求调用者是一个多线程单元——刚巧是单元模式最适合——尤为是在 Windows Forms程序中,须要执行任务像与剪切板结合同样庸俗!
幸运地是,在等待句柄难使用或不适合的时候,.NET framework提供了更先进的信号结构——Monitor.Wait 和 Monitor.Pulse。
与手工的锁定相比,你能够进行说明性的锁定,用衍生自ContextBoundObject 并标以Synchronization特性的类,
它告诉CLR自动执行锁操做,看这个例子:
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.Threading;
using
System.Runtime.Remoting.Contexts;
[Synchronization]
public
class
AutoLock : ContextBoundObject {
public
void
Demo() {
Console.Write (
"Start..."
);
Thread.Sleep (1000);
// We can't be preempted here
Console.WriteLine (
"end"
);
// thanks to automatic locking!
}
}
public
class
Test {
public
static
void
Main() {
AutoLock safeInstance =
new
AutoLock();
new
Thread (safeInstance.Demo).Start();
// Call the Demo
new
Thread (safeInstance.Demo).Start();
// method 3 times
safeInstance.Demo();
// concurrently.
}
}
|
Start... end
Start... end
Start... end
CLR确保了同一时刻只有一个线程能够执行 safeInstance中的代码。它建立了一个同步对象来完成工做,并在每次调
用safeInstance的方法和属性时在其周围只可以行锁定。锁的做用域——这里是safeInstance对象,被称为同步环境。
那么,它是如何工做的呢?Synchronization特性的命名空间:System.Runtime.Remoting.Contexts是一个线索。
ContextBoundObject能够被认为是一个“远程”对象,这意味着全部方法的调用是被监听的。让这个监听称为可能,
就像咱们的例子AutoLock,CLR自动的返回了一个具备相同方法和属性的AutoLock对象的代理对象,它扮演着一个中间
者的角色。总的来讲,监听在每一个方法调用时增长了数微秒的时间。
自动同步不能用于静态类型的成员,和非继承自 ContextBoundObject(例如:Windows Form)的类。
锁在内部以相同的方式运做,你可能期待下面的例子与以前的有同样的结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
[Synchronization]
public
class
AutoLock : ContextBoundObject {
public
void
Demo() {
Console.Write (
"Start..."
);
Thread.Sleep (1000);
Console.WriteLine (
"end"
);
}
public
void
Test() {
new
Thread (Demo).Start();
new
Thread (Demo).Start();
new
Thread (Demo).Start();
Console.ReadLine();
}
public
static
void
Main() {
new
AutoLock().Test();
}
}
|
(注意咱们放入了Console.ReadLine语句。)由于在同一时刻的同一个此类的对象中只有一个线程能够执行代码,
三个新线程将保持被阻止在Demo 放中,直到Test 方法完成,须要等待ReadLine来完成。所以咱们以与以前的有相同
结果而了结,可是只有在按完Enter键以后。这是一个线程安全的手段,差很少足够能在类中排除任何有用的多线程!
此外,咱们仍未解决以前描述的一个问题:若是AutoLock是一个集合类,好比说,咱们仍然须要一个像下面同样的锁,
假设运行在另外一个类里:
if (safeInstance.Count > 0) safeInstance.RemoveAt (0);
除非使用这代码的类自己是一个同步的ContextBoundObject!
同步环境能够扩展到超过一个单独对象的区域。默认地,若是一个同步对象被实例化从在另外一段代码以内,它们拥有
共享相同的同步环境(换言之,一个大锁!)。这个行为能够由改变Synchronization特性的构造器的参数来指定。使用
SynchronizationAttribute类定义的常量之一:
常量 |
含义 |
NOT_SUPPORTED |
至关于不使用同步特性 |
SUPPORTED |
若是从另外一个同步对象被实例化,则合并已存在的同步环境,不然只剩下非同步。 |
REQUIRED |
若是从另外一个同步对象被实例化,则合并已存在的同步环境,不然建立一个新的同步环境。 |
REQUIRES_NEW |
老是建立新的同步环境 |
因此若是SynchronizedA的实例被实例化于SynchronizedB的对象中,若是SynchronizedB像下面这样声明的话,
它们将有分离的同步环境:
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)]
public class SynchronizedB : ContextBoundObject { ...
越大的同步环境越容易管理,可是减小机会对有用的并发。换个有限的角度,分离的同步环境会形成死锁,看这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[Synchronization]
public
class
Deadlock : ContextBoundObject {
public
DeadLock Other;
public
void
Demo() { Thread.Sleep (1000); Other.Hello(); }
void
Hello() { Console.WriteLine (
"hello"
); }
}
public
class
Test {
static
void
Main() {
Deadlock dead1 =
new
Deadlock();
Deadlock dead2 =
new
Deadlock();
dead1.Other = dead2;
dead2.Other = dead1;
new
Thread (dead1.Demo).Start();
dead2.Demo();
}
}
|
由于每一个Deadlock的实例在Test内建立——一个非同步类,每一个实例将有它本身的同步环境,所以,有它本身的锁。
当它们彼此调用的时候,不会花太多时间就会死锁(确切的说是一秒!)。若是Deadlock 和 Test是由不一样开发团队来
写的,这个问题特别容易发生。别期望Test知道如何产生的错误,更别期望他们来解决它了。在死锁显而易见的状况下,
这与使用明确的锁的方式造成鲜明的对比。
6.1 可重入性问题
线程安全方法有时候也被称为可重入式的,由于在它执行的时候能够被抢占部分线路,在另外的线程调用也不会带来坏效果。从某个意义上讲,术语线程安全和 可重入式的是同义的或者是贴义的。
不过在自动锁方式上,若是Synchronization的参数可重入式的 为true的话,可重入性会有潜在的问题:
[Synchronization(true)]
同步环境的锁在执行离开上下文时被临时地释放。在以前的例子里,这将能预防死锁的发生;很明显很须要这样的功能。然而一个反作用是,在这期间,任何线程均可以自由的调用在目标对象(“重进入”的同步上下文)的上任何方法,而很是复杂的多线程中试图避免不释放资源是排在首位的。这就是可重入性的问题。 由于[Synchronization(true)]做用于类级别,这特性打开了对于非上下文的方法访问,因为可重入性问题使它们混入类的调用。
虽然可重入性是危险的,但有些时候它是不错的选择。好比:设想一个在其内部实现多线程同步的类,将逻辑工做线程运行在不一样的语境中。在没有可重入性问题的状况下,工做线程在它们彼此之间或目标对象之间可能被无理地阻碍。
这凸显了自动同步的一个基本弱点:超过适用的大范围的锁定带来了其它状况没有带来的巨大麻烦。这些困难:死锁,可重入性问题和被阉割的并发,使另外一个更简单的方案——手动的锁定变得更为合适
单元模式线程是一个自动线程安全机制, 很是贴近于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命名空间 下帮助类,它管理着工做线程。它提供了如下特性:
最后两个特性是至关地有用:意味着你再也不须要将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
RecursiveUpgradeCount {
get
; }
public
int
RecursiveWriteCount {
get
; }
|
有时候,在一个原子操做里面交换读写锁是很是有用的,好比,当某个item不在list中的时候,添加此item进去。最好的状况是,最小化写如锁的时间,例如像下面这样处理:
1 得到一个读取锁
2 测试list是否包含item,若是是,则返回
3 释放读取锁
4 得到一个写入锁
5 写入item到list中,释放写入锁。
可是在步骤三、4之间,当另一个线程可能偷偷修改List(好比说添加一样一个Item),ReaderWriterLockSlim经过提供第三种锁来解决这个问题,这就是upgradeable lock。一个可升级锁和read lock 相似,只是它可以经过一个原子操做,被提高为write lock。使用方法以下:
从调用者的角度,这很是想递归(嵌套)锁。实际上第三步的时候,经过一个原子操做,释放了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
若是你的程序有不少线程,致使花费了大多时间在等待句柄的阻止上,你能够经过 线程池来削减负担。线程池经过合并不少等待句柄在不多的线程上来节省时间。
使用线程池,你须要注册一个连同将被执行的委托的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); });
|
另外一个进入线程池的方式是经过异步委托。
在第一部分咱们描述如何使用 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这个帮助类即可轻松完成。
周期性的执行某个方法最简单的方法就是使用一个计时器,好比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,在使用相同的线程池时提供了额外的便利——相同的底层引擎。下面是增长的特性的摘要:
例子:
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事件被主线程调用,若是它有停顿, 将使用户接口变的没有响应。
每一个线程与其它线程数据存储是隔离的,这对于“不相干的区域”的存储是有益的,它支持执行路径的基础结构,如通讯,事务和安全令牌。 经过方法参数传递这些数据是十分笨拙的。存储这些数据到静态域意味着这些数据能够被全部线程共享。
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对象。