概述与概念
C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程。这里的一个简单的例子及其输出:
除非被指定,否则所有的例子都假定以下命名空间被引用了: using System; using System.Threading; class ThreadTest { static void Main() {
Thread t = new Thread (WriteY); t.Start(); // 在新的线程中运行WriteY
while (true) Console.Write (\不停地写'x' }
static void WriteY() {
while (true) Console.Write (\不停地写'y' } }
xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
主线程创建了一个新线程“t”,它运行了一个重复打印字母\的方法,同时主线程重复但因字母“x”。CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中我们定义了一个局部变量,然后在主线程和新创建的线程上同时地调用这个方法。 static void Main() {
new Thread (Go).Start(); // 调用Go()方法在一个新线程中 Go(); // 在主线程中调用Go() }
static void Go() {
// 声明和使用一个局部变量'cycles'
for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
??????????
变量cycles的副本分别在各自的内存堆栈中创建,输出也一样,可预见,会有10个问号输出。当线程们引用了一些公用的目标实例的时候,他们会共享数据。下面是实例:
class ThreadTest { bool done;
static void Main() {
ThreadTest tt = new ThreadTest(); // 创建一个实例 new Thread (tt.Go).Start(); tt.Go(); }
// 注意Go现在是一个实例方法 void Go() {
if (!done) { done = true; Console.WriteLine (\} }
因为在相同的ThreadTest实例中,两个线程都调用了Go(),它们共享了done字段,这个结果输出的是一个\,而不是两个。 Done
静态字段提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子:
class ThreadTest {
static bool done; // 静态方法被所有 线程一块使用 static void Main() { new Thread (Go).Start(); Go(); }
static void Go() {
if (!done) { done = true; Console.WriteLine (\} }
上述两个例子足以说明, 另一个关键概念, 那就是线程安全(或反之,它的不足之
处! ) 输出实际上是不确定的:它可能(虽然不大可能) , \,可以被打印两次。然而,如果我们在Go方法里调换指令的顺序, \被打印两次的机会会大幅地上升: static void Go() {
if (!done) { Console.WriteLine (\}
Done Done (usually!)
问题就是一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句——在它将done设置为true之前。
补救措施是当读写公共字段的时候,提供一个排他锁;C#提供了lock语句来达到这个目的:
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 (\} } }
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以\只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全。
临时暂停,或阻止是多线程的协同工作,同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停或Sleep一段时间: Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻止30秒 一个线程也可以使用它的Join方法来等待另一个线程结束:
Thread t = new Thread (Go); // 假设Go是某个静态方法 t.Start();
t.Join(); // 等待(阻止)直到线程t结束
一个线程,一旦被阻止,它就不再消耗CPU的资源了。 线程是如何工作的
线程被一个线程协调程序管理着——一个CLR委托给操作系统的函数。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间) 在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。
线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。 线程 vs. 进程
属于一个单一的应用程序的所有的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操作系统单元。
线程于进程有某些相似的地方:比如说进程通常以时间片方式与其它在电脑中运行的进程的方式与一个C#程序线程运行的方式大致相同。二者的关键区别在于进程彼此是完全隔绝的。线程与运行在相同程序其它线程共享(堆heap)内存,这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。 何时使用多线程
多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中...”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”

