【转】C#中的线程 入门
Keywords:C# 线程
Source:http://www.albahari.com/threading/
Author: Joe Albahari
Translator: Swanky Wu
Published: http://www.cnblogs.com/txw1958/
Download:http://www.albahari.info/threading/threading.pdf
本系列文章可以算是一本很出色的C#线程手册,思路清晰,要点都有介绍,看了后对C#的线程及同步等有了更深入的理解。
- 入门
- 概述与概念
- 创建和开始使用多线程
- 线程同步基础
- 同步要领
- 锁和线程安全
- Interrupt 和 Abort
- 线程状态
- 等待句柄
- 同步环境
- 使用多线程
- 单元模式和Windows Forms
- BackgroundWorker类
- ReaderWriterLock类
- 线程池
- 异步委托
- 计时器
- 局部储存
- 高级话题
- 非阻止同步
- Wait和Pulse
- Suspend和Resume
- 终止线程
一、入门
1. 概述与概念
C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行(单CPU多核)。一个C#客户端程序(Console, WPF, or Windows Forms)开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并可以通过创建额外的线程 组成多线程。这里的一个简单的例子及其输出:
除非被指定,否则所有的例子都假定以下命名空间被引用了:
using System;
using System.Threading;
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”。
线程一旦开始,其 Islive属性为true,直到线程结束。当委托传递给线程的构造函数执行完毕线程就结束,一旦结束,线程不能重新开始。
CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。
在接下来的例子中,我们定义有一个局部变量的方法,然后在主线程和新创建的线程上同时地调用这个方法。
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个问号输出。。
当两个线程们引用了一些共同的对象实例的时候,他们会共享数据。下面是实例:
class ThreadTest { bool done; static void Main() { ThreadTest tt = new ThreadTest(); // Create a common instance (tt作为他们共同的一个实例对象) new Thread (tt.Go).Start(); //新线程调用 tt.Go(); //主线程调用 } // Note that Go is now an instance method(实例方法) void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }
因为两个线程在同一样的ThreadTest对象上 调用了Go(),它们<span style="color:#ff0000;">共享了done字段,这个结果输出的是一个"Done",而不是两个</span>。
静态字段提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子:
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"被打印两次的机会会大幅地上升:(实践证明是的,)
static void Go() { if (!done) { Console.WriteLine ("Done"); done = true; } }
问题就是一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句——在它将done设置为true之前。
补救措施是当读写公共字段的时候,提供一个互斥锁;C#提供了lock语句来达到这个目的:
class ThreadSafe { static bool done; static readonly 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),一个线程等待,或者说被阻止(blocks), 直到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次(之后done为true)。代码以如此方式在不确定的多线程环境中被叫做线程安全。
多线程中复杂而隐蔽错误的一个主要原因是数据共享。尽管多线程是经常必须的,但尽可能保持简单点。
A thread, while blocked, doesn't consume CPU resources.(一个线程,阻塞的时候,不消耗CPU资源。)
(让线程等一段时间再执行)
static void Main() { Thread t = new Thread (Go); t.Start(); t.Join(); Console.WriteLine ("Thread t has ended!"); } static void Go() { for (int i = 0; i < 1000; i++) Console.Write ("y"); }
结果先打印1000次“y”,再打印Thread t has ended!,Join()可设置等待的时间,时间一到,其他线程就会执行
Thread.Sleep
pauses the current thread for a specified period:(阻止当前线程一个指定的时间)
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour Thread.Sleep (500); // sleep for 500 milliseconds
线程是如何工作的
线程被一个线程协调程序管理着(一个CLR委托给操作系统的函数)。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程—比如说在互斥锁中、或在用户输入,都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)
在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。
线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。
线程 vs. 进程
属于一个单一的应用程序的所有的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操作系统单元。
线程于进程有某些相似的地方:正如进程并行运行在电脑上一样, 线程并行运行在单个进程。。进程是完全独立于彼此的。而线程只是一个有限程度的隔离。线程与运行在相同程序中的其它线程共享(堆heap)内存,这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。
线程的使用和误用
多线程有许多用途,下面是最常见的用途:
1、保持一个快速响应的用户界面
通过在一个并行的“worker”线程上运行耗时的任务,主UI线程可以自由的继续处理键盘和鼠标事件。
2、有效利用CPU的阻塞
多线程是有用的:当一个线程正在等待一个另一台计算机或硬件的响应时。当一个线程因为执行任务而被阻塞时,
其他线程可以利用 计算机的未占用资源。
3、并行编程
何时使用多线程
多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于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委托如何定义的:【也可以不用】
public
delegate
void
ThreadStart();
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托:
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#指出线程开始的方法(不用明确使用委托也可以,例如前面的例子)
Thread t =
new
Thread (Go);
// No need to explicitly use ThreadStart
在这种情况,ThreadStart被编译器自动推断出来,
三、另一个快捷方式是使用一个lambda表达式或匿名方法:
static void Main() { Thread t = new Thread ( () => Console.WriteLine ("Hello!") ); t.Start(); }
将数据传入ThreadStart中
最简单的方法传递参数到一个线程:是执行一个lambda表达式,调用该方法所需的参数
static void Main() { Thread t = new Thread ( () => Print ("Hello from t!") ); t.Start(); } static void Print (string message) { Console.WriteLine (message); }
使用这种方法,您可以将任意数量的参数传递给方法。你甚至可以把整个实现包在一个多语句λ表达式中
new Thread (() => { Console.WriteLine ("I'm running on another thread!"); Console.WriteLine ("This is so easy!"); }).Start();
使用匿名方法 同样可以
new Thread (delegate() { ... }).Start();
另一种方法是:在Thread
’s Start()方法中传参数
static void Main() { Thread t = new Thread (Print); t.Start ("Hello from t!"); } static void Print (object messageObj) { string message = (string) messageObj; // We need to cast here Console.WriteLine (message); }
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);
话又说回来,在上面的例子里,我们想更好地区分开每个线程的输出结果,让其中一个线程输出大写字母。我们传入一个状态字到Go中来完成整个任务,但我们不能使用ThreadStart委托,因为它不接受参数,所幸的是,.NET framework定义了另一个版本的委托叫做ParameterizedThreadStart, 它可以接收一个单独的object类型参数(通常需要参数转换)
Lambda表达式和捕获变量:
正如我们所见,一个lambda表达式是最强大的方式传递数据到一个线程。然而,您必须小心线程开始后变量的意外修改,因为这些变量都是共享的。例如,考虑以下:
for (int i = 0; i < 10; i++) new Thread (() => Console.Write (i)).Start();
输出是非确定的! 例如:224458891010 (后面的数不小于前面的数,在1-10之间的数,10次循环开了10个线程)
Here’s a typical result:
The problem is that the i
variable refers to the same memory location throughout the loop’s lifetime(循环周期时 两个线程捕获的是同一个内存地址区). Therefore, each thread calls Console.Write
on a variable whose value may change as it is running!
The solution is to use a temporary variable as follows:
for (int i = 0; i < 10; i++) { int temp = i; new Thread (() => Console.Write (temp)).Start(); }
We can illustrate the problem in the earlier code more simply with the following example:
string text = "t1"; Thread t1 = new Thread ( () => Console.WriteLine (text) ); text = "t2"; Thread t2 = new Thread ( () => Console.WriteLine (text) ); t1.Start(); t2.Start();
Because both lambda expressions capture the same text
variable, t2
is printed twice:
t2
t2
命名线程
线程可以通过它的Name属性进行命名,这非常有利于调试:可以用Console.WriteLine打印出线程的名字,Microsoft Visual Studio可以将线程的名字显示在调试工具栏的位置上。线程的名字可以在被任何时间设置,但只能设置一次,重命名会引发异常。
程序的主线程也可以被命名,下面例子里主线程通过CurrentThread属性命名:
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); } }
结果也有可能相反,不能保证哪个先输出。线程由操作系统来调度,每次哪个线程先运行可能不同。
前台和后台线程
创建的线程默认为前台线程(而线程池中的线程总是后台线程),这意味着任何一个前台线程在运行都会保持程序存活。然而后台线程不是。一旦所有的前台线程结束,程序也就结束了,任何后台线程也将突然停止。。
一个线程的前台/后台状态 与他的优先级或分配执行时间没有关系。。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。
线程的IsBackground属性控制它的前后台状态,如下实例:
class PriorityTest { static void Main (string[] args) { Thread worker = new Thread (delegate() { Console.ReadLine(); }); if (args.Length > 0) worker.IsBackground = true; worker.Start(); } }
1、如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户按回车来触发,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着。
2、 另一方面如果有参数传入Main(),工作线程被赋为后台线程,当主线程结束程序立刻退出,终止了ReadLine。后台线程终止的这种方式,使任何最后操作都被规避了,这种方式是不太合适的。好的方式是明确等待任何后台工作线程完成后再结束程序,有两种方法解决:
a、对创建的线程调用Join()方法,让其他等待它的结束
b、在线程池中的话,用一个事件等待来处理
在这两种情况下,你应该指定一个超时(timeout),所以可以放弃一个叛离线程(出于某种原因拒绝完成任务的线程)。
拥有一个后台工作线程是有益的,最直接的理由是它当提到结束程序它总是可能有最后的发言权。
对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。
线程优先级
线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:
enum
ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
注意:提升一个线程的优先级之前要仔细想想—它可能导致其他线程的资源缺乏等问题
设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:
using (Process p = Process.GetCurrentProcess()) p.PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 实际上是一个等级的最高优先级:Realtime(实时)。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最好的选择实时进程级别。
如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。
(虽然在写这篇文章的时候,在互联网电话程序Skype侥幸地这么做, 也许是因为它的界面相当简单吧。)
降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)
异常处理
任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系。考虑下面的程序:
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // 不会在这得到异常 Console.WriteLine ("Exception!"); } static void Go() { throw null; } }
这里
try
/
catch
语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的,
补救方法是在线程处理的方法内加入他们自己的异常处理:
public static void Main() { new Thread (Go).Start(); } static void Go() { try { ... throw null; // 这个异常在下面会被捕捉到 ... } catch (Exception ex) { 记录异常日志,并且或通知另一个线程 我们发生错误 ... }
从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃,try/catch块需要出现在每个线程进入的方法内,至少要在产品程序中应该如此。