文章系参考转载,英文原文网址请参考:http://www.albahari.com/threading/
作者 Joseph Albahari, 翻译 Swanky Wu
中文翻译作者把原文放在了"google 协作"上面,GFW屏蔽,不能访问和查看,因此我根据译文和英文原版整理转载到园子里面。
本系列文章可以算是一本很出色的C#线程手册,思路清晰,要点都有介绍,看了后对C#的线程及同步等有了更深入的理解。
一、入门
1. 概述与概念
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分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中我们定义了一个局部变量,然后在主线程和新创建的线程上同时地调用这个方法。
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操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。
2. 创建和开始使用多线程
线程用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();
|