线程被一个线程协调程序管理着——一个CLR委托给操作系统的函数。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)
在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。
线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。
线程与进程
属于一个单一的应用程序的所有的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操作系统单元。
线程于进程有某些相似的地方:比如说进程通常以时间片方式与其它在电脑中运行的进程的方式与一个C#程序线程运行的方式大致相同。二者的关键区别在于进程彼此是完全隔绝的。线程与运行在相同程序其它线程共享(堆heap)内存,这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。
线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的
public
delegate
void
ThreadStart();
|
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()
一个线程可以通过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
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!"
); }
|
前台和后台线程
线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。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任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。
对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。