logo资料库

Threading in C#中文版.pdf

第1页 / 共68页
第2页 / 共68页
第3页 / 共68页
第4页 / 共68页
第5页 / 共68页
第6页 / 共68页
第7页 / 共68页
第8页 / 共68页
资料共68页,剩余部分请下载后查看
C#中的多线程1
googlepages.com
C#中的多线程 - Swanky.wu's knowledge library
-第二部分:线程同步基础
googlepages.com
C#中的多线程 -第二部分:线程同步基础 - Swanky.wu's knowledge library
第三部分:使用多线程f
googlepages.com
C#中的多线程 -第三部分:使用多线程- Swanky.wu's knowledge library
第四部分:高级话题
googlepages.com
C#中的多线程 -第四部分:高级话题- Swanky.wu's knowledge library
C#中的多线程 - Swanky.wu's knowledge library C#中的多线程 By Joseph Albahari, Translated by Swanky Wu Based on "C# 3.0 in a Nutshell" by Joseph Albahari and Ben Albahari (O'Reilly Media) http://www.albahari.com/nutshell/ 入门 概述与概念 创建和开始使用多线程 概述与概念 线程同步基础 同步要领 锁和线程安全 Interrupt 和 Abort 线程状态 等待句柄 同步环境 使用多线程 高级话题 单元模式和Windows Forms BackgroundWorker类 ReaderWriterLock类 线程池 异步委托 计时器 局部储存 非阻止同步 Wait和Pulse Suspend和Resume 终止线程 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"); // 不停地写'x' } static void WriteY() { while (true) Console.Write ("y"); // 不停地写'y' } } xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ... 主线程创建了一个新线程“t”,它运行了一个重复打印字母"y"的方法,同时主线程重复但因字母“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 ("Done"); } } } 因为在相同的ThreadTest实例中,两个线程都调用了Go(),它们共享了done字段,这个结果输出的是一个"Done",而不是 两个。 Done 静态字段提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子: class ThreadTest { http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library static bool done; // 静态方法被所有 线程一块使用 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; } } 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 ("Done"); done = true; } } } } 当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下, 就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。代码以如此方式在不确定的多线程环境中被叫 http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library 做线程安全。 临时暂停,或阻止是多线程的协同工作,同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因 是线程想要暂停或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程序来 说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中 运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中...”,以防止工作无法继续。这就避免了程序 出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续 接收事件,而实际的任务已被工作线程完成。BackgroundWorker恰好可以辅助完成这一功能。 在没有用户界面的程序里,比如说Windows Service, 多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比 如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它 的事情。 另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分 开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。 http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library 一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特 性——比如BackgroundWorker类, 线程池,threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的情况, 人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中 多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。 何时不要使用多线程 多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,这带 来了无论是否交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。因此,要么多线程的交互设 计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。 当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有 一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。 创建和开始使用多线程 线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的: public delegate void ThreadStart(); 调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创 建TheadStart委托: class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); // 在新线程中运行Go() Go(); // 同时在主线程中运行Go() } static void Go() { Console.WriteLine ("hello!"); } 在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来: hello! hello! 一个线程可以通过C#堆委托简短的语法更便利地创建出来: static void Main() { Thread t = new Thread (Go); // 没必要明确地使用ThreadStart t.Start(); ... } static void Go() { ... } 在这种情况,ThreadStart被编译器自动推断出来,另一个快捷的方式是使用匿名方法来启动线程: static void Main() { http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); }); t.Start(); } 线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。 一个线程一旦结束便不能重新开始了。 将数据传入ThreadStart中 话又说回来,在上面的例子里,我们想更好地区分开每个线程的输出结果,让其中一个线程输出大写字母。我们传入一个状态 字到Go中来完成整个任务,但我们不能使用ThreadStart委托,因为它不接受参数,所幸的是,.NET framework定义了另一 个版本的委托叫做ParameterizedThreadStart, 它可以接收一个单独的object类型参数: public delegate void ParameterizedThreadStart (object obj); 之前的例子看起来是这样的: 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!"); } hello! HELLO! 在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数,就像这样 写: Thread t = new Thread (new ParameterizedThreadStart (Go)); t.Start (true); ParameterizedThreadStart的特性是在使用之前我们必需对我们想要的类型(这里是bool)进行装箱操作,并且它只能 接收一个参数。 一个替代方案是使用一个匿名方法调用一个普通的方法如下: static void Main() { Thread t = new Thread (delegate() { WriteText ("Hello"); }); t.Start(); } static void WriteText (string text) { Console.WriteLine (text); } 优点是目标方法(这里是WriteText),可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿 http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library 名方法中,向下面的一样: 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); } After 匿名方法打开了一种怪异的现象,当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无意的互 动。有意的互动(通常通过字段)被认为是足够了!一旦线程开始运行了,外部变量最好被处理成只读的——除非 有人愿意使用适当的锁。 另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下列重写 了原来的例子: 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命名: class ThreadNaming { static void Main() { http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
C#中的多线程 - Swanky.wu's knowledge library 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); } } Hello from main Hello from worker 前台和后台线程 线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不 维持程序的存活。 改变线程从前台到后台不会以任何方式改变它在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(); } } 如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主 线程退出,但是程序保持运行,因为一个前台线程仍然活着。 另一方面如果有参数传入Main(),工作线程被赋值为后台线程,当主线程结束程序立刻退出,终止了ReadLine。 后台线程终止的这种方式,使任何最后操作都被规避了,这种方式是不太合适的。好的方式是明确等待任何后台工作线程完成 后再结束程序,可能用一个timeout(大多用Thread.Join)。如果因为某种原因某个工作线程无法完成,可以用试图终止 它的方式,如果失败了,再抛弃线程,允许它与 与进程一起消亡。(记录是一个难题,但这个场景下是有意义的) 拥有一个后台工作线程是有益的,最直接的理由是它当提到结束程序它总是可能有最后的发言权。交织以不会消亡的前台线 程,保证程序的正常退出。抛弃一个前台工作线程是尤为险恶的,尤其对Windows Forms程序,因为程序直到主线程结束时才 退出(至少对用户来说),但是它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏 找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。 http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html[2009-8-23 0:48:41]
分享到:
收藏