为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

线程和同步

2017-12-03 50页 doc 168KB 24阅读

用户头像

is_668482

暂无简介

举报
线程和同步线程和同步 第 章 18 线程和同步 0 在应用程序中进行网络调用需要一定的时间。用户不希望在安装用户界面时只是等待,直到服务器返回一个响应为止。用户可以在这个过程中执行其他一些操作,甚至取消发送给服务器的请求。这些都可以使用线程来实现。 使用线程有几个原因。不让用户等待是其中一个原因。对于所有需要等待的操作,例如文件、数据库或网络访问的启动都需要一定的时间,此时就可以启动一个新线程,完成其他任务。即使是处理密集型的任务,线程也是有帮助的。一个进程的多个线程可以同时运行在不同的CPU上,或多个核心CPU的不同核心上。...
线程和同步
线程和同步 第 章 18 线程和同步 0 在应用程序中进行网络调用需要一定的时间。用户不希望在安装用户界面时只是等待,直到服务器返回一个响应为止。用户可以在这个过程中执行其他一些操作,甚至取消发送给服务器的请求。这些都可以使用线程来实现。 使用线程有几个原因。不让用户等待是其中一个原因。对于所有需要等待的操作,例如文件、数据库或网络访问的启动都需要一定的时间,此时就可以启动一个新线程,完成其他任务。即使是处理密集型的任务,线程也是有帮助的。一个进程的多个线程可以同时运行在不同的CPU上,或多个核心CPU的不同核心上。 还必须注意运行多个线程的一些问题。它们可以同时运行,但如果线程访问相同的数据,就很容易出问题。必须实现同步机制。 本章介绍用多个线程编写应用程序所需了解的知识,包括: ? 线程概述 ? 使用委托的轻型线程 ? 线程类 ? 线程池 ? 线程问题 ? 同步技术 ? COM空间 ? BackgroundWorker 18.1 概述 线程是程序中独立的指令流。使用C#编写任何程序时,都有一个入口:Main()。程序从Main()方法的第一条语句开始执行,直到这个方法返回为止。 这个程序结构非常适合于有一个可识别的任务序列的程序,但程序常常需要同时完成多个任务。线程对客户端和服务器端应用程序都非常重要。在Visual Studio编辑器中输入C#代码时,Dynamic Help窗口会立即显示与所输入代码相关的主题。后台线程会搜索帮助。 第?部分 基 类 库 Microsoft Word的拼写检查器也会做相同的事。一个线程等待用户输入,另一个线程进行后台搜索。第三个线程将写入的数据存储在临时文件中,第四个线程从Internet上下载其他数据。 运行在服务器上的应用程序中,一个线程等待客户的请求,称为监听器线程。只要接收到请求,就把它传送给另一个工作线程,之后继续与客户通信。监听器线程会立即返回,接收下一个客户发送的下一个请求。 使用Windows任务管理器,可以从菜单View | Select Columns中打开Threads列,查看进程和每个进程的线程号。在图18-1中,只有cmd.exe运行在一个线程中,其他应用程序都使用多个线程。Internet Explorer运行了51个线程。 图 18-1 操作系统会调度线程。线程有一个优先级、正在处理的程序的位置计数器、一个存储其本地变量的堆栈。每个线程都有自己的堆栈,但程序代码的内存和堆由一个进程的所有线程共享。这使一个进程中的所有线程之间的通信非常快――该进程的所有线程都寻址相同的虚拟内存。但是,这也使处理比较困难,因为多个线程可以修改同一个内存位置。 进程管理的资源包括虚拟内存和Windows句柄,其中至少包含一个线程。线程是运行程序所必需的。 在.NET中,托管的线程由Thread类定义。托管的线程不一定映射为一个操作系统线程。尽管这种情况可能出现,但应由.NET运行库负责将托管的线程映射到操作系统的物理线程上。在这方面,SQL Server 2005的运行主机与Windows应用程序的运行主机完全不同。使用ProcessThread类可以获得内部线程的信息,但在托管的应用程序中,通常最好使用托管的线程。 502 第18章 线程和同步 18.2 异步委托 创建线程的一种简单方式是定义一个委托,异步调用它。第7章提到,委托是方法的类型安全的引用。Delegate类还支持异步调用方法。在后台,Delegate类会创建一个执行任务的线程。 提示: 委托使用线程池来完成异步任务。线程池详见本章后面的内容。 为了演示委托的异步特性,启动一个方法,它需要一定的时间才能执行完毕。方法TakesAWhile至少需要作为变元传送过来的毫秒数才能执行完,因为它调用了Thread.Sleep()方法: static int TakesAWhile(int data, int ms) { Console.WriteLine("TakesAWhile started"); Thread.Sleep(ms); Console.WriteLine("TakesAWhile completed"); return ++data; } 要在委托中调用这个方法,必须定义一个有相同参数和返回类型的委托,如下面的TakesAWhileDelegate所示: public delegate int TakesAWhileDelegate(int data, int ms); 现在可以使用不同的技术异步调用委托,返回结果。 18.2.1 投票 一种技术是投票,检查委托是否完成了任务。所创建的Delegate类提供了方法BeginInvoke(),在该方法中,可以传送用委托类型定义的输入参数。BeginInvoke()方法总是有两个AsyncCallback和Object类型的额外参数(稍后讨论)。现在重要的是BeginInvoke()方法的返回类型:IAsyncResult。在IAsyncResult中,可以获得委托的信息,并验证委托是否完成了任务,这是IsCompleted属性的功劳。只要委托没有完成其任务,程序的主线程就继续执行while循环。 static void Main() { // synchronous // TakesAWhile(1, 3000); // asynchronous TakesAWhileDelegate d1 = TakesAWhile; IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null); 503 第?部分 基 类 库 while (!ar.IsCompleted) { // doing something else in the main thread Console.Write("."); Thread.Sleep(50); } int result = d1.EndInvoke(ar); Console.WriteLine("result: {0}", result); } 运行应用程序,可以看到主线程和委托线程同时运行,在委托线程执行完毕后,主线程就停止循环。 .TakesAWhile started ...................................................TakesAWhile completed result: 2 除了检查委托是否完成之外,还可以在完成了由主线程执行的工作后,调用委托类型的EndInvoke()方法。EndInvoke()方法会一直等待,直到委托完成其任务为止。 警告: 如果不等待委托完成其任务就结束主线程~委托线程就会停止。 18.2.2 等待句柄 等待异步委托的结果的另一种方式是使用与IAsyncResult相关的等待句柄。使用AsyncWaitHandle属性可以访问等待句柄。这个属性返回一个WaitHandle类型的对象,它可以等待委托线程完成其任务。方法WaitOne()将一个超时时间作为可选的第一个参数,在其中可以定义要等待的最大时间。这里设置为50毫秒。如果发生超时,WaitOne()就返回false,while循环会继续执行。如果等待操作成功,就用一个中断退出while循环,用委托的EndInvoke()方法接收结果。 static void Main() { TakesAWhileDelegate d1 = TakesAWhile; IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null); while (true) { Console.Write("."); if (ar.AsyncWaitHandle.WaitOne(50, false)) { Console.WriteLine("Can get the result now"); break; } } int result = d1.EndInvoke(ar); Console.WriteLine("result: {0}", result); } 504 第18章 线程和同步 提示: 等待句柄的内容详见本章后面的“同步”一节。 18.2.3 异步回调 等待委托的结果的第三种方式是使用异步回调。在BeginInvoke()方法的第三个参数中,可以传送一个满足AsyncCallback委托的需求的方法。AsyncCallback委托定义了一个IAsyncResult类型的参数,其返回类型是void。这里,把方法TakesAWhileCompleted的地址赋予第三个参数,以满足AsyncCallback委托的需求。对于最后一个参数,可以传送任意对象,以便从回调方法中访问它。传送委托实例是可行的,这样回调方法就可以使用它获得异步方法的结果。 现在,只要委托TakesAWhileDelegate完成了其任务,就调用TakesAWhileCompleted()方法。不需要在主线程中等待结果。但是在委托线程的任务未完成之前,不能停止主线程,除非刚刚停止的委托线程没有出问题。 static void Main() { TakesAWhileDelegate d1 = TakesAWhile; d1.BeginInvoke(1, 3000, TakesAWhileCompleted, d1); for (int i = 0; i < 100; i++) { Console.Write("."); Thread.Sleep(50); } } 方法TakesAWhileCompleted()用AsyncCallback委托指定的参数和返回类型来定义。用BeginInvoke()方法传送的最后一个参数可以使用ar. AsyncState读取。在TakesAWhileDelegate委托中,可以调用EndInvoke()方法获得结果。 static void TakesAWhileCompleted(IAsyncResult ar) { if (ar == null) throw new ArgumentNullException("ar"); TakesAWhileDelegate d1 = ar.AsyncState as TakesAWhileDelegate; Trace.Assert(d1 != null, "Invalid object type"); int result = d1.EndInvoke(ar); Console.WriteLine("result: {0}", result); } 警告: 使用回调方法~必须注意这个方法在委托线程中调用~而不是在主线程中调用。 除了定义一个单独的方法,给它传送BeginInvoke()方法之外,匿名方法也非常适合这种情况。使用委托关键字,可以定义一个匿名方法,其参数是IAsyncResult类型。现在不需要 505 第?部分 基 类 库 把一个值赋予BeginInvoke()方法的最后一个参数,因为匿名方法可以直接访问该方法外部的变量d1。但是,匿名方法仍是在委托线程中调用,以这种方式定义方法时,这不是很明显。 static void Main() { TakesAWhileDelegate d1 = TakesAWhile; d1.BeginInvoke(1, 3000, delegate(IAsyncResult ar) { int result = d1.EndInvoke(ar); Console.WriteLine("result: {0}", result); }, null); for (int i = 0; i < 100; i++) { Console.Write("."); Thread.Sleep(50); } } 提示: 只有代码不太多~且实现代码不需要用于不同的地方时~才应使用匿名方法。在这种情况下~定义一个单独的方法比较好。匿名方法详见第7章。 编程模型和所有这些利用异步委托的选项—— 投票、等待句柄和异步调用—— 不仅能用于委托,编程模型在.NET Framework的各个地方都能见到。例如,可以用HttpWebRequest类的BeginGetResponse()方法异步发送HTTP Web请求,使用SqlCommand类的BeginExecute- Reader()方法给数据库发送异步请求。其参数类似于委托的BeginInvoke()方法,也可以使用相同的方式获得结果。 提示: HttpWebRequest参见第35章~SqlCommand参见第25章。 18.3 Thread类 使用Thread类可以创建和控制线程。下面的代码是创建和启动一个新线程的简单例子。Thread类的构造接受ThreadStart和ParameterizedThreadStart类型的委托参数。ThreadStart委托定义了一个返回类型为void的无参数方法。在创建了Thread对象后,就可以用Start()方法启动线程了: using System; using System.Threading; namespace Wrox.ProCSharp.Threading { 506 第18章 线程和同步 class Program { static void Main() { Thread t1 = new Thread(ThreadMain); t1.Start(); Console.WriteLine("This is the main thread."); } static void ThreadMain() { Console.WriteLine("Running in a thread."); } } } 运行这个程序,得到两个线程的输出: This is the main thread. Running in a thread. 不能保证哪个结果先输出。线程由操作系统调度,每次哪个线程在前面都是不同的。 前面探讨了匿名方法如何与异步委托一起使用。异步委托还可以与Thread类一起使 用,将线程方法的实现代码传送给Thread构造函数的变元: using System; using System.Threading; namespace Wrox.ProCSharp.Threading { class Program { static void Main() { Thread t1 = new Thread( delegate() { Console.WriteLine("running in a thread"); }); t1.Start(); Console.WriteLine("This is the main thread."); } } } 在创建好线程后,如果不需要用引用线程的变量来控制线程,还可以用更简洁的方式 编写代码。用构造函数创建一个新的Thread对象,将匿名方法传送给构造函数,用返回的 Thread对象直接调用Start()方法: using System.Threading; namespace Wrox.ProCSharp.Threading 507 第?部分 基 类 库 { class Program { static void Main() { new Thread( delegate() { Console.WriteLine("running in a thread"); }).Start(); Console.WriteLine("This is the main thread."); } } } 但是,采用一个引用Thread对象的变量是有原因的。例如,为了更好地控制线程,可以在启动线程前,设置Name属性,给线程指定名称。为了获得当前线程的名称,可以使用静态属性Thread.CurrentThread,获取当前线程的Thread实例,访问Name属性,进行读取访问。线程也有一个托管的线程ID,可以用ManagedThreadId属性读取它。 static void Main() { Thread t1 = new Thread(ThreadMain); t1.Name = "MyNewThread1"; t1.Start(); Console.WriteLine("This is the main thread."); } static void ThreadMain() { Console.WriteLine("Running in the thread {0}, id: {1}.", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId); } 在应用程序的输出中,现在还可以看到线程名和ID: This is the main thread. Running in the thread MyNewThread1, id: 3. 警告: 给线程指定名称~非常有助于调试线程。在Visual Studio的调试会话中~可以打开Debug Location工具栏~查看线程的名称。 18.3.1 给线程传送数据 如果需要给线程传送一些数据,可以采用两种方式。一种方式是使用带ParameterizedThreadStart委托参数的Thread构造函数,另一种方式是创建一个定制类,把线程的方法定义为实例方法,这样就可以初始化实例的数据,之后启动线程。 508 第18章 线程和同步 要给线程传送数据,需要某个存储数据的类或结构。这里定义了包含字符串的结构 Data,也可以传送任意对象。 public struct Data { public string Message; } 如果使用了ParameterizedThreadStart委托,线程的入口点必须有一个object类型的参 数,返回类型为void。对象可以转换为数据,这里是把信息写入控制台。 static void ThreadMainWithParameters(object o) { Data d = (Data)o; Console.WriteLine("Running in a thread, received {0}", d.Message); } 在Thread类的构造函数中,可以将新的入口点赋予ThreadMainWithParameters,传送 变量d,调用Start()方法。 static void Main() { Data d = new Data(); d.Message = "Info"; Thread t2 = new Thread(ThreadMainWithParameters); t2.Start(d); } 给新线程传送数据的另一种方式是定义一个类(参见类MyThread),在其中定义需要 的字段,将线程的主方法定义为类的一个实例方法: public class MyThread { private string data; public MyThread(string data) { this.data = data; } public void ThreadMain() { Console.WriteLine("Running in a thread, data: {0}", data); } } 这样,就可以创建MyThread的一个对象,给Thread类的构造函数传送对象和Thread Main()方法。线程可以访问数据。 MyThread obj = new MyThread("info"); Thread t3 = new Thread(obj.ThreadMain); 509 第?部分 基 类 库 t3.Start(); 18.3.2 后台线程 只要有一个前台线程在运行,应用程序的进程就在运行。如果多个前台线程在运行,而Main方法结束了,应用程序的进程就是激活的,直到所有前台线程完成其任务为止。 在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。 在用Thread类创建线程时,可以设置属性IsBackground,以确定该线程是前台线程还是后台线程。Main()方法将线程t1的IsBackground属性设置为false(默认值)。在启动新线程后,主线程就把结束信息写入控制台。新线程会写入启动和结束信息,在这个过程中它要睡眠3秒。在这3秒中,新线程会完成其工作,主线程才结束。 class Program { static void Main() { Thread t1 = new Thread(ThreadMain); t1.Name = "MyNewThread1"; t1.IsBackground = false; t1.Start(); Console.WriteLine("Main thread ending now..."); } static void ThreadMain() { Console.WriteLine("Thread {0} started", Thread.CurrentThread.Name); Thread.Sleep(3000); Console.WriteLine("Thread {0} completed", Thread.CurrentThread.Name); } } 在启动应用程序时,会看到写入控制台的完成信息,尽管主线程会早一步完成其工作。原因是新线程也是一个前台线程。 Main thread ending now... Thread MyNewThread1 started Thread MyNewThread1 completed 如果将启动新线程的IsBackground属性改为true,显示在控制台上的结果就会不同。在一个系统上,可以看到新线程的启动信息,但没有结束信息。如果线程没有正常结束,还有可能看不到启动信息。 Main thread ending now... Thread MyNewThread1 started 后台线程非常适合于完成后台任务。例如,如果关闭Word应用程序,拼写检查器继续运行其进程就没有意义了。在应用程序结束时,拼写检查器线程就可以关闭了。但是,组织Outlook信息库的线程应一直是激活的,直到Outlook结束,它才结束。 510 第18章 线程和同步 18.3.3 线程的优先级 前面提到,操作系统是在调度线程。给线程指定优先级,就可以影响这个调度。 在改变优先级之前,必须理解线程调度器。操作系统根据优先级来调度线程。优先级最高的线程在CPU上运行。线程如果在等待资源,就会停止运行,释放CPU。线程必须等待有几个原因,例如响应睡眠指令、等待磁盘I/O的完成,等待网络包的到达等。如果线程不是主动释放CPU,线程调度器就会抢先安排该线程。如果线程有一个时间量,就可以继续使用CPU。如果优先级相同的多个线程等待使用CPU,线程调度器就会使用一个循环调度规则,将CPU逐个交给线程使用。如果线程是被其他线程抢先了,它就会排在队列的最后。 只有优先级相同的多个线程在运行,才用得上时间量和循环规则。优先级是动态的。如果线程是CPU密集型的(一直需要CPU,且不等待资源),其优先级就低于用该线程定义的基本优先级。如果线程在等待资源,就会推动优先级向上移动,它的优先级就会增加。由于有这个推动,线程才有可能在下次等待结束时获得CPU。 在Thread类中,可以设置Priority属性,以影响线程的基本优先级。Priority属性需要一个ThreadPriority枚举定义的值。该值定义的级别有Highest、AboveNormal、BelowNormal和Lowest。在给线程指定较高的优先级时要小心,因为这可能降低其他线程的运行几率。如果需要,可以改变优先级一段较短的时间。 18.3.4 控制线程 调用Thread对象的Start()方法,可以创建线程。但是,在调用Start()方法后,新线程仍不在Running状态,而是在Unstarted状态。操作系统的线程调度器选择了要运行的线程后,线程就会改为Running状态。读取Thread.ThreadState属性,就可以获得线程的当前状态。 使用Thread.Sleep()方法,会使线程处于WaitSleepJoin状态,在用Sleep()方法定义的时间过后,线程就会再次被调用。 要停止另一个线程,可以调用Thread.Abort()方法。调用这个方法时,会在接到中止命令的线程中抛出ThreadAbortException类型的异常。用一个处理程序捕获这个异常,线程可以在结束前完成一些清理工作。线程还可以在接收到调用Thread.ResetAbort()方法的结果ThreadAbortException后继续运行。如果线程没有重置中止,接收到中止请求的线程状态将从AbortRequested改为Aborted。 如果需要等待线程的结束,就可以调用Thread.Join()方法。Thread.Join()方法会停止当前线程,把它设置为WaitSleepJoin状态,直到加入的线程完成为止。 .NET 1.0也支持Thread.Suspend()和Thread.Resume()方法,它们分别用于暂停和继续一个线程。但是,线程在得到Suspend请求时,我们并不知道线程在做什么,它可能处于有锁的同步段中。这很容易导致死锁。这就是这些方法现在被废弃的原因。另外,还可以使用同步对象给线程发信号,这样线程就可以挂起了。于是,线程就知道进入等待状态的最佳时机。 511 第?部分 基 类 库 18.4 线程池 创建线程是需要时间的。如果有不同的小任务要完成,就可以事先创建许多线程,在应完成这些任务时发出请求。这个线程数应在需要更多的线程时增加,在需要释放资源时减少。 不需要自己创建这样一个列。该列表由ThreadPool类管理。这个类会在需要时增减池中线程的个数,直到最大的线程数。池中的最大线程数是可以配置的。在双核CPU中,默认设置为50个工作线程和1000个I/O线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。如果有更多的工作要处理,线程池中线程的使用也到了极限,最新的工作就要排队,必须等待线程完成其任务。 下面的示例程序首先要读取工作线程和I/O线程的最大线程数,把这个信息写入控制台。接着在for循环中,调用ThreadPool.QueueUserWorkItem()方法,传送一个WaitCallback类型的委托,把方法JobForAThread()赋予线程池中的线程。线程池收到这个请求后,就会从池中选择一个线程,来调用该方法。如果线程池还没有运行,就会创建一个线程池,启动第一个线程。如果线程池已经在运行,且有一个自由线程,就把工作传送给这个线程。 using System; using System.Threading; namespace Wrox.ProCSharp.Threading { class Program { static void Main() { int nWorkerThreads; int nCompletionPortThreads; ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletion PortThreads); Console.WriteLine("Max worker threads: {0}, I/O completion threads: {1}", nWorkerThreads, nCompletionPortThreads); for (int i = 0; i < 5; i++) { ThreadPool.QueueUserWorkItem(JobForAThread); } Thread.Sleep(3000); } static void JobForAThread(object state) { for (int i = 0; i < 3; i++) { Console.WriteLine("loop {0}, running inside pooled thread {1}", i, Thread.CurrentThread.ManagedThreadId); 512 第18章 线程和同步 Thread.Sleep(50); } } } } 运行应用程序,可以看到50个工作线程的当前设置。5个任务只由两个线程池中的线 程处理,读者运行该程序的结果可能与此不同,也可以改变任务的睡眠时间和要处理的任 务数,得到完全不同的结果。 Max worker threads: 50, I/O completion threads: 1000 loop 0, running inside pooled thread 4 loop 0, running inside pooled thread 3 loop 1, running inside pooled thread 4 loop 1, running inside pooled thread 3 loop 2, running inside pooled thread 4 loop 2, running inside pooled thread 3 loop 0, running inside pooled thread 4 loop 0, running inside pooled thread 3 loop 1, running inside pooled thread 4 loop 1, running inside pooled thread 3 loop 2, running inside pooled thread 4 loop 2, running inside pooled thread 3 loop 0, running inside pooled thread 4 loop 1, running inside pooled thread 4 loop 2, running inside pooled thread 4 线程池使用起来很简单,但它有一些限制: ? 线程池中的所有线程都是后台线程。如果进程中的所有前台线程都结束了,所有 的后台线程就会停止。不能把线程池中的线程改为前台线程。 ? 不能给线程池中的线程设置优先级或名称。 ? 对于COM对象,线程池中的所有线程都是多线程单元(multithreaded apartment,MTA) 线程。许多COM对象都需要单线程单元(single-threaded apartment,MTA) 线程。 ? 线程池中的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写 检查器线程),就应使用Thread类创建一个线程。 18.5 线程问题 用多个线程编程并不容易。在启动访问相同数据的多个线程时,会遇到难以发现的问 题。为了避免这些问题,必须特别注意同步问题和多个线程可能发生的其他问题。下面探 讨与线程相关的问题,如竞态条件和死锁。 18.5.1 竞态条件 如果两个或多个线程访问相同的对象,或者访问不同步的共享状态,就会出现竞态条件。 513 第?部分 基 类 库 为了演示竞态条件,定义一个StateObject类,它包含一个int字段和一个方法Change State。在ChangeState方法的实现代码中,验证state变量是否包含5。如果是,就递增其值。下一个语句是Trace.Assert,它验证state现在是否包含6。在给包含5的变量递增了1后,该变量的值就应是6。但事实不一定是这样。例如,如果一个线程刚刚执行完if(state ==5) 语句,它就被其他线程抢先,调度器去运行另一个线程了。第二个线程现在进入if体,由于state的值仍是5,所以将它递增为6。第一个线程现在再次被安排执行,在下一个语句中,state被递增为7。这时就发生了竞态条件,显示断言信息。 public class StateObject { private int state = 5; public void ChangeState(int loop) { if (state == 5) { state++; Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops"); } state = 5; } } 下面定义一个线程方法来验证这一点。SampleThread类的方法RaceCondition()将一个StateObject对象作为其参数。在一个无限while循环中,调用方法ChangeState()。变量i仅用于显示断言信息中的循环数。 public class SampleThread { public void RaceCondition(object o) { Trace.Assert(o is StateObject, "o must be of type StateObject"); StateObject state = o as StateObject; int i = 0; while (true) { state.ChangeState(i++); } } } 在程序的Main方法中,创建了一个新的StateObject对象,它由所有的线程共享。在Thread类的构造函数中,给RaceCondition的地址传送一个SampleThread类型的对象,以创建Thread对象。接着传送state对象,使用Start()方法启动这个线程。 static void Main() { 514 第18章 线程和同步 StateObject state = new StateObject(); for (int i = 0; i < 20; i++) { new Thread(new SampleThread().RaceCondition).Start(state); } } 启动程序,就会出现竞态条件。在竞态条件第一次出现后,还需要多长时间才能第二次出现竞态条件,取决于系统以及将程序建立为发布版本还是调试版本。如果建立为发布版本,该问题的出现次数会比较多,因为代码被优化了。如果系统中有多个CPU或使用双核CPU,其中多个线程可以同时运行,该问题也会比单核CPU的出现次数多。在单核CPU中,若线程调度是抢先式的,也会出现该问题,只是没有那么频繁。 图18-2显示在3816个循环后,发生竞态条件的程序断言。多启动应用程序几次,总是会得到不同的结果。 图 18-2 要避免该问题,可以锁定共享的对象。这可以在线程中完成:用下面的lock语句锁定在线程中共享的变量state。只有一个线程能在锁定块中处理共享的state对象。由于这个对象由所有的线程共享,因此如果一个线程锁定了state,另一个线程就必须等待该锁定的解除。一旦进行了锁定,线程就拥有该锁定,直到该锁定块的末尾才解除锁定。如果每个改变state变量引用的对象的线程都使用一个锁定,竞态条件就不会出现。 public class SampleThread { public void RaceCondition(object o) { Trace.Assert(o is StateObject, "o must be of type StateObject"); StateObject state = o as StateObject; int i = 0; while (true) { lock (state) // no race condition with this lock { 515 第?部分 基 类 库 state.ChangeState(i++); } } } } 在使用共享对象时,除了进行锁定之外,还可以将共享对象设置为线程安全的对象。其中ChangeState()方法包含一个lock语句。由于不能锁定state变量本身(只有引用类型才能用于锁定),因此定义一个object类型的变量sync,将它用于lock语句。如果每次state值都使用同一个同步对象来修改锁定,竞态条件就不会出现。 public class StateObject { private int state = 5; private object sync = new object(); public void ChangeState(int loop) { lock (sync) { if (state == 5) { state++; Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops"); } state = 5; } } } 18.5.2 死锁 过多的锁定也会有麻烦。在死锁中,至少有两个线程被挂起,等待对方解除锁定。由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去。 为了演示死锁,下面实例化两个StateObject类型的对象,并传送给SampleThread类的构造函数。创建两个线程,其中一个线程运行方法Deadlock1(),另一个线程运行方法Deadlock2(): StateObject state1 = new StateObject(); StateObject state2 = new StateObject(); new Thread(new SampleThread(state1, state2).Deadlock1).Start(); new Thread(new SampleThread(state1, state2).Deadlock2).Start(); 方法Deadlock1()和Deadlock2()现在改变两个对象s1和s2的状态。这就进行了两个锁定。方法Deadlock1()先锁定s1,接着锁定s2。方法Deadlock2()先锁定s2,再锁定s1。现在,有可能方法Deadlock1()中s1的锁定会被解除。接着出现一次线程切换,Deadlock2()开始运行,并锁定s2。第二个线程现在等待s1锁定的解除。因为它需要等待,所以线程 516 第18章 线程和同步 调度器再次调度第一个线程,但第一个线程在等待s2锁定的解除。这两个线程现在都在等 待,只要锁定块没有结束,就不会解除锁定。这是一个典型的死锁。 public class SampleThread { public SampleThread(StateObject s1, StateObject s2) { this.s1 = s1; this.s2 = s2; } private StateObject s1; private StateObject s2; public void Deadlock1() { int i = 0; while (true) { lock (s1) { lock (s2) { s1.ChangeState(i); s2.ChangeState(i++); Console.WriteLine("still running, {0}", i); } } } } public void Deadlock2() { int i = 0; while (true) { lock (s2) { lock (s1) { s1.ChangeState(i); s2.ChangeState(i++); Console.WriteLine("still running, {0}", i); } } } } } 结果是,程序运行了许多循环,不久就没有响应了。“仍在运行”的信息仅在控制台 上写入几次。死锁问题的发生频率也取决于系统配置,每次运行的结果都不同。 517 第?部分 基 类 库 死锁问题并不总是很明显。一个线程锁定了s1,接着锁定s2,另一个线程锁定了s2,接着锁定s1。只需改变锁定顺序,这两个线程就会以相同的顺序进行锁定。但是,锁定可能隐藏在方法的深处。为了避免这个问题,可以在应用程序的体系架构中,从一开始就好锁定顺序,也可以为锁定定义超时时间。如何定义超时时间详见下一节的内容。 18.6 同步 要避免同步问题,最好不要在线程之间共享数据。当然,这并不总是可行的。如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。注意,同步问题与竞态条件和死锁有关。如果不注意这些问题,就很难在应用程序中找到问题的原因,因为线程问题是不定期发生的。 本节讨论可以用于多个线程的同步技术: ? lock语句 ? Interlocked类 ? Monitor类 ? 等待句柄 ? Mutex类 ? Semaphore类 ? Event类 lock语句、Interlocked类和Monitor类可用于进程内部的同步。Mutex类、Semaphore类和Event类提供了多个进程中的线程同步。 18.6.1 lock语句和线程安全 C#为多个线程的同步提供了自己的关键字:lock语句。lock语句是设置锁定和解除锁定的一种简单方式。 在添加lock语句之前,先进入另一个竞态条件。类SharedState演示了如何使用线程共享的状态,并保存一个整数值。 public class SharedState { private int state = 0; public int State { get { return state; } set { state = value; } } } 类Task包含方法DoTheTask(),该方法是新线程的入口点。在其实现代码中,将SharedState的State递增50000次。变量sharedState在这个类的构造函数中初始化: 518 第18章 线程和同步 public class Task { SharedState sharedState; public Task(SharedState sharedState) { this.sharedState = sharedState; } public void DoTheTask() { for (int i = 0; i < 50000; i++) { sharedState.State += 1; } } } 在Main()方法中,创建一个SharedState对象,并传送给20个Thread对象的构造函 数。在启动所有的线程后,Main()方法进入另一个循环,使20个线程处于等待状态,直 到所有的线程都执行完毕为止。线程执行完毕后,把共享状态的合计值写入控制台。因 为执行了50000个循环,有20个线程,所以写入控制台的值应是1000000。但是,事实 常常并非如此。 class Program { static void Main() { int numThreads = 20; SharedState state = new SharedState(); Thread[] threads = new Thread[numThreads]; for (int i = 0; i < numThreads; i++) { threads[i] = new Thread(new Task(state).DoTheTask); threads[i].Start(); } for (int i = 0; i < numThreads; i++) { threads[i].Join(); } Console.WriteLine("summarized {0}", state.State); } } } 多次运行应用程序的结果如下所示: summarized 939270 summarized 993799 summarized 998304 summarized 937630 519 第?部分 基 类 库 每次运行的结果都不同,但没有一个结果是正确的。调试版本和发布版本的区别很大。所使用的CPU类型不同,结果也不一样。如果将循环次数改为比较小的值,就会多次得到正确的值,但不是每次。这个应用程序非常小,很容易看出问题,但该问题的原因在大型应用程序中就很难确定。 必须在这个程序中添加同步功能,这可以用lock关键字实现。 用lock语句定义的对象表示,要等待指定对象的锁定解除。只能传送引用类型。锁定值类型只是锁定了一个副本,这是没有什么意义的。编译器会提供一个锁定值类型的错误。进行了锁定后—— 只有一个线程得到了锁定块,就可以运行lock语句块。在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。 lock (obj) { // synchronized region } 要锁定静态成员,可以把锁定放在object类型上: lock (typeof(StaticClass)) { } 使用lock关键字可以将类的实例成员设置为线程安全。这样,一次只有一个线程能访问该实例的DoThis()和DoThat()方法。 public class Demo { public void DoThis() { lock (this) { // only one thread a time can access the DoThis and DoThat methods } } public void DoThat() { lock (this) { } } } 但是,因为实例的对象也可以用于外部的同步访问,我们不能在类中控制这种访问,所以应采用SyncRoot模式。在SyncRoot模式中,创建了一个私有对象syncRoot,将这个对象用于lock语句。 public class Demo { private object syncRoot = new object(); 520 第18章 线程和同步 public void DoThis() { lock (syncRoot) { // only one thread a time can access the DoThis and DoThat methods } } public void DoThat() { lock (syncRoot) { } } } 使用锁定是需要时间的,且并不总是必需的。可以创建类的两个版本,一个同步版本, 一个异步版本。这里用修改类Demo来演示。类Demo本身并不是同步的,这可以在DoThis() 和DoThat()方法中看出。该类还定义了IsSynchronized属性,客户可以从该属性中获得类 的同步选项信息。为了获得该类的同步版本,可以使用静态方法Synchronized()传送一个非 同步对象,这个方法会返回SynchronizedDemo类型的对象。SynchronizedDemo实现为派 生自基类Demo的一个内部类,并重写了基类中的虚成员。重写的成员使用了SyncRoot 模式。 public class Demo { private class SynchronizedDemo : Demo { private object syncRoot = new object(); private Demo d; public SynchronizedDemo(Demo d) { this.d = d; } public override bool IsSynchronized { get { return true; } } public override void DoThis() { lock (syncRoot) { d.DoThis(); } } public override void DoThat() { lock (syncRoot) 521 第?部分 基 类 库 { d.DoThat(); } } } public virtual bool IsSynchronized { get { return false; } } public static Demo Synchronized(Demo d) { if (!d.IsSynchronized) { return new SynchronizedDemo(d); } return d; } public virtual void DoThis() { } public virtual void DoThat() { } } 必须注意,在使用SynchronizedDemo类时,只有方法是同步的,对这个类的两个成员 的调用并没有同步。 警告: SyncRoot模式可能使线程安全产生负面影响。.NET 1.0集合类实现了SyncRoot模 式,.NET 2.0的泛型集合类不再实现这个模式。 下面研究一下前面的例子。如果试图用SyncRoot模式锁定对属性的访问,使 SharedState类变成线程安全的,仍会出现前面描述的竞态条件。 public class SharedState { private int state = 0; private object syncRoot = new object(); public int State // there’s still a race condition, don’t do this! { get { lock (syncRoot) {return state; }} set { lock (syncRoot) {state = value; }} } } 522 第18章 线程和同步 调用方法DoTheTask()的线程访问SharedState类的get存取器,以获得state的当前值, 接着get存取器给state设置新值。在调用对象的get和set存取器期间,对象没有锁定,另 一个线程可以获得临时值。 public void DoTheTask() { for (int i = 0; i < 50000; i++) { sharedState.State += 1; } } 所以,最好不改变SharedState类,让它没有线程安全性。 public class SharedState { private int state = 0; public int State { get { return state; } set { state = value; } } } 然后在方法DoTheTask()中,将lock语句添加到合适的地方: public void DoTheTask() { for (int i = 0; i < 50000; i++) { lock (sharedState) { sharedState.State += 1; } } } 这样,应用程序的结果就总是正确的: summarized 1000000 警告: 在一个地方使用lock语句并不意味着~访问对象的其他线程都在等待。必须对每个访 问共享状态的线程显式使用同步功能。 当然,还必须修改SharedState类的设计,将递增提供为一个原子操作。这是一个设计 问题—— 什么是类的原子功能, 523 第?部分 基 类 库 public class SharedState { private int state = 0; private object syncRoot = new object(); public int State { get { return state; } } public int IncrementState() { lock (syncRoot) { return ++state; } } } 提示: 上面锁定状态递增的最后一个例子~有一个使用Interlocked类的版本更快~如下所示。 18.6.2 Interlocked Interlocked类用于使变量的简单语句原子化。i++不是线程安全的,它的操作包括从内 存中获取一个值,给该值递增1,再将它存储回内存。这些操作都可能会被线程调度器打 断。Interlocked类提供了以线程安全的方式递增、递减和交换值的方法。 Interlocked类提供的方法如表18-1所示。 表 18-1 Interlocked类的成员 说 明 Increment()方法递增一个变量,把结果存储到一个原子操作中 Increment() Decrement()递减一个变量,并存储结果 Decrement() Exchange()将一个变量设置为指定的值,并返回变量的初始值 Exchange() CompareExchange()对两个变量进行相等比较,如果它们相等,就设置指CompareExchange() 定的值,返回初始值 Add()对两个值执行相加操作,用结果替代第一个变量 Add() Read()方法用于在一个原子操作中从内存中读取64位值。在32位系统Read() 中,读取64位不是原子化的,而需要从两个内存地址中读取 在64位系统中,不需要Read()方法,因为访问64位是一个原子操作 与其他同步技术相比,使用Interlocked类会快得多。但是,它只能用于简单的同 步问题。 例如,这里不使用lock语句锁定对someState变量的访问,把它设置为一个新值,以 524 第18章 线程和同步 防它是空的,而可以使用Interlocked类,它比较快: lock (this) { if (someState == null) { someState = newState; } } 这个功能相同、但比较快的版本使用了Interlocked.CompareExchange方法: Interlocked.CompareExchange(ref someState, newState, null); 不在lock语句中执行递增操作: public int State { get { lock (this) { return ++state; } } } 而使用较快的Interlocked.Increment(): public int State { get { return Interlocked.Increment(ref state); } } 18.6.3 Monitor类 C#的lock语句由编译器解析为使用Monitor类。下面的lock语句: lock (obj) { // synchronized region for obj } 解析为调用Enter()方法,该方法会一直等待,直到线程获得对象的锁定为止。一次只 有一个线程能成为对象锁定的拥有者。只要解除了锁定,线程就可以进入同步段。Monitor 类的Exit()方法解除了锁定。无论在什么情况下解除该锁定(包括抛出异常的情况),Exit() 方法都放在try块的finally处理程序中。 525 第?部分 基 类 库 提示: try/finally详见第13章。 Monitor.Enter(obj); try { // synchronized region for obj } finally { Monitor.Exit(obj); } 与C#的lock语句相比,Monitor类的主要优点是:可以添加一个等待获得锁定的超时值。这样就不会无限期地等待获得锁定,而可以使用TryEnter方法,给它传送一个超时值,确定等待获得锁定的最长时间。如果得到了obj的锁定,TryEnter方法就返回true,访问由对象obj锁定的状态。如果另一个线程锁定obj的时间超过了500毫秒,TryEnter方法就返回false,线程不再等待,而是执行其他操作。也许在以后,该线程会尝试再次获得该锁定。 if (Monitor.TryEnter(obj, 500)) { try { // acquired the lock // synchronized region for obj } finally { Monitor.Exit(obj); } } else { // didn’t get the lock, do something else } 18.6.4 等待句柄 WaitHandle是一个抽象基类,用于等待一个信号的设置。可以等待不同的信号,因为WaitHandle是一个基类,可以从中派生一些类。 在本章前面使用异步委托时,已经使用了WaitHandle。异步委托的方法BeginInvoke()返回一个实现了IAsycResult接口的对象。使用IAsycResult接口,可以用属性AsycWaitHandle访问WaitHandle。在调用WaitOne()方法时,线程会等待接收一个与等待句柄相关的信号。 static void Main() { 526 第18章 线程和同步 TakesAWhileDelegate d1 = TakesAWhile; IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null); while (true) { Console.Write("."); if (ar.AsyncWaitHandle.WaitOne(50, false)) { Console.WriteLine("Can get the result now"); break; } } int result = d1.EndInvoke(ar); Console.WriteLine("result: {0}", result); } WaitHandle类定义的、执行等待的方法如表18-2所示。 表 18-2 WaitHandle类的成员 说 明 WaitOne()是一个实例方法,利用它可以等待一个信号的发生。也可以为WaitOne() 最大等待时间指定一个超时值 WaitAll()是一个静态方法,用于传送WaitHandle对象的数组,并等待所WaitAll() 有的句柄发出信号 WaitAny()是一个静态方法,用于传送WaitHandle对象的数组,并等待WaitAny() 其中一个句柄发出信号。这个方法返回发出信号的等待句柄对象的索 引,以便确定可以在程序中继续执行什么功能。如果在句柄发出信号之 前超时,WaitAny()就返回WaitTimeout 使用SafeWaitHandle属性,还可以将一个内置句柄赋予一个操作系统资源,并等待该句柄。例如,可以指定一个SafeWaitHandle等待文件I/O操作的完成,或者指定定制的SafeTransactionHandle,参见第21章。 类Mutex、Event和Semaphore派生自基类WaitHandle,所以可以在等待时使用它们。 18.6.5 Mutex类 Mutex(mutual exclusion,互斥)是.NET Framework中提供同步访问多个进程的一个类。它非常类似于Monitor类,因为它们都只有一个线程能拥有锁定。只有一个线程能获得互斥锁定,访问受互斥锁定保护的同步代码区域。 在Mutex类的构造函数中,可以指定互斥锁定是否最初应由调用线程拥有,定义互斥锁定的名称,获得互斥锁定是否已存在的信息。在下面的示例代码中,第三个参数定义为输出参数,接收一个表示互斥锁定是否为新创建的布尔值。如果返回的值是false,就表示互斥锁定已经定义。互斥锁定可以在另一个进程中定义,因为操作系统知道有名称的互斥 527 第?部分 基 类 库 锁定,它由不同的进程共享。如果没有给互斥锁定指定名称,互斥锁定就是未命名的,不 在不同的进程之间共享。 bool createdNew; Mutex mutex = new Mutex(false, "ProCSharpMutex", out createdNew); 要打开已有的互斥锁定,还可以使用方法Mutex.OpenExisting(),它不需要用构造函数 创建互斥锁定时需要的.NET权限。 Mutex类派生自基类WaitHandle,因此可以利用WaitOne()方法获得互斥锁定,在该过 程中成为该互斥锁定的拥有者。调用ReleaseMutex()方法,即可释放互斥锁定。 if (mutex.WaitOne()) { try { // synchronized region } finally { mutex.ReleaseMutex(); } } else { // some problem happened while waiting } 由于系统知道有名称的互斥锁定,因此可以使用它禁止应用程序启动两次。在下面的 Windows窗体应用程序中,调用了Mutex对象的构造函数。接着验证名称为 SingletonWinAppMutex的互斥锁定是否存在。如果存在,应用程序就退出。 static class Program { [STAThread] static void Main() { bool createdNew; Mutex mutex = new Mutex(false, "SingletonWinAppMutex", out createdNew); if (!createdNew) { MessageBox.Show("You can only start one instance of the application"); Application.Exit(); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } 528 第18章 线程和同步 18.6.6 Semaphore类 旗语(Semaphore)锁定非常类似于互斥锁定,其区别是,旗语锁定可以同时由多个线程使用。旗语锁定是一种计数的互斥锁定。使用旗语锁定,可以定义允许同时访问受旗语锁定保护的资源的线程个数。如果有许多资源,且只允许一定数量的线程访问该资源,就可以使用旗语锁定。例如,要访问系统上的物理I/O端口,且有三个端口可用,就允许三个线程同时访问I/O端口,但第四个线程需要等待前三个线程中的一个释放资源。 在下面的示例程序中,Main()方法创建了6个线程和一个计数为4的旗语锁定。在Semaphore类的构造函数中,定义了锁定数的计数,它可以用旗语锁定(第二个参数)来获得,还定义了最初自由的锁定数(第一个参数)。如果第一个参数的值小于第二个参数,它们的差就是已经赋予线程的旗语锁定数。与互斥锁定一样,也可以给旗语锁定指定名称,使之在不同的进程之间共享。这里定义旗语锁定时没有指定名称,所以它只能在这个进程中使用。在创建了Semaphore对象之后,启动六个线程,它们都获得了相同的旗语锁定。 using System; using System.Threading; using System.Diagnostics; namespace Wrox.ProCSharp.Threading { class Program { static void Main() { int threadCount = 6; int semaphoreCount = 4; Semaphore semaphore = new Semaphore(semaphoreCount, semaphoreCount); Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { threads[i] = new Thread(ThreadMain); threads[i].Start(semaphore); } for (int i = 0; i < threadCount; i++) { threads[i].Join(); } Console.WriteLine("All threads finished"); } 在线程的主方法ThreadMain()中,线程利用WaitOne()锁定了旗语。旗语锁定的计数是4,所以有四个线程可以获得锁定。线程5必须等待,这里还定义了最大等待时间为500毫秒。如果在该等待时间过后未能获得锁定,线程就把一个信息写入控制台,在循环中继续等待。只要获得了锁定,线程就把一个信息写入控制台,睡眠一段时间,然后解除锁定。在解除锁定时,一定要解除资源的锁定。这就是在finally处理程序中调用Semaphore类的 529 第?部分 基 类 库 Release()方法的原因。 static void ThreadMain(object o) { Semaphore semaphore = o as Semaphore; Trace.Assert(semaphore != null, "o must be a Semaphore type"); bool isCompleted = false; while (!isCompleted) { if (semaphore.WaitOne(600, false)) { try { Console.WriteLine("Thread {0} locks the sempahore", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(2000); } finally { semaphore.Release(); Console.WriteLine("Thread {0} releases the semaphore", Thread.CurrentThread.ManagedThreadId); isCompleted = true; } } else { Console.WriteLine("Timeout for thread {0}; wait again", Thread.CurrentThread.ManagedThreadId); } } } } } 运行应用程序,可以看到有四个线程获得了锁定。ID为7和8的线程需要等待。该等 待会重复进行,直到四个获得锁定的线程之一解除了旗语锁定。 Thread 3 locks the sempahore Thread 4 locks the sempahore Thread 5 locks the sempahore Thread 6 locks the sempahore Timeout for thread 8; wait again Timeout for thread 7; wait again Timeout for thread 8; wait again Timeout for thread 7; wait again Timeout for thread 7; wait again Timeout for thread 8; wait again Thread 3 releases the semaphore Thread 8 locks the sempahore 530 第18章 线程和同步 Thread 4 releases the semaphore Thread 7 locks the sempahore Thread 5 releases the semaphore Thread 6 releases the semaphore Thread 8 releases the semaphore Thread 7 releases the semaphore All threads finished 18.6.7 Events类 事件是另一个系统级的资源同步方法。为了在托管代码中使用系统事件,.NET Framework在System.Threading命名空间中提供了ManualResetEvent和AutoResetEvent类。 提示: 第7章介绍了C#中的event关键字~它与System.Threading命名空间中的Event类没有关系。event关键字基于委托~而上述两个Event类是.NET封装器~用于系统级的内置事件资源的同步。 可以使用事件通知其他线程:这里有一些数据,完成了一些操作等。事件可以发信号,也可以不发信号。使用前面介绍的WaitHandle类,线程可以等待处于发信号状态的事件。 调用Set()方法,即可使ManualResetEvent发信号。调用Reset()方法,可以使之返回不发信号的状态。如果多个线程等待一个事件发出信号,并调用了Set()方法,就释放所有等待的线程。另外,如果一个线程刚刚调用了WaitOne()方法,但事件已经发出了信号,等待的线程就可以继续等待。 AutoResetEvent也是通过Set()方法发信号。也可以使用Reset()方法使之返回不发信号的状态。但是,如果一个线程在等待自动重置的事件发信号,当第一个线程的等待结束时,该事件会自动变为不发信号的状态。这样,如果多个线程在等待事件发信号,就只有一个线程结束其等待状态,它不是等待时间最长的线程,而是优先级最高的线程。 为了演示AutoResetEvent类的事件,下面的ThreadTask类定义了Calculation()方法,这是线程的入口点。在这个方法中,线程接收用于计算的输入数据(由结构InputData定义),将结果写入变量result,它可以通过Result属性来访问。只要完成了计算(在随机的一段时间过后),就调用AutoResetEvent的Set()方法,向事件发信号。 public struct InputData { public int X; public int Y; public InputData(int x, int y) { this.X = x; this.Y = y; } } public class ThreadTask 531 第?部分 基 类 库 { private AutoResetEvent autoEvent; private int result; public int Result { get { return result; } } public ThreadTask(AutoResetEvent ev) { this.autoEvent = ev; } public void Calculation(object obj) { InputData data = (InputData)obj; Console.WriteLine("Thread {0} starts calculation", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(new Random().Next(3000)); result = data.X + data.Y; // signal the event - completed! Console.WriteLine("Thread {0} is ready", Thread.CurrentThread.ManagedThreadId); autoEvent.Set(); } } 程序的Main()方法定义了包含四个AutoResetEvent对象的数组和包含四个ThreadTask 对象的数组。每个ThreadTask在构造函数中用一个AutoResetEvent对象初始化,这样每个线 程在完成时都有自己的事件对象来发信号。现在使用ThreadPool类调用QueueUserWorkItem() 方法,让后台线程执行计算任务。 class Program { static void Main() { int taskCount = 4; AutoResetEvent[] autoEvents = new AutoResetEvent[taskCount]; ThreadTask[] tasks = new ThreadTask[taskCount]; for (int i = 0; i < taskCount; i++) { autoEvents[i] = new AutoResetEvent(false); tasks[i] = new ThreadTask(mevents[i]); ThreadPool.QueueUserWorkItem(tasks[i].Calculation, new InputData(i + 1, i + 3)); } //... 532 第18章 线程和同步 WaitHandle类现在用于等待数组中的任意一个事件。WaitAny()等待向任意一个事件发信号。从WaitAny()返回的index匹配数组中传送给WaitAny()的事件,以提供向哪个事件发信号的信息,并从这个事件中读取结果。 for (int i = 0; i < taskCount; i++) { int index = WaitHandle.WaitAny(autoEvents); if (index == WaitHandle.WaitTimeout) { Console.WriteLine("Timeout!!"); } else { Console.WriteLine("finished task for {0}, result: {1}", index, tasks[index].Result); } } } } 启动应用程序,可以看到线程在进行计算,设置事件,通知主线程,它可以读取结果了。由于随机时间、是调试版本还是发布版本、以及硬件的不同,会看到不同的顺序,线程池中有不同数量的线程在执行任务。这里重用了线程池中的线程4,完成了两个任务,因为它比较快,能第一个完成计算。 Thread 3 starts calculation Thread 4 starts calculation Thread 5 starts calculation Thread 4 is ready finished task for 1, result: 6 Thread 4 starts calculation Thread 3 is ready finished task for 0, result: 4 Thread 4 is ready finished task for 3, result: 10 Thread 5 is ready finished task for 2, result: 8 18.7 COM单元 线程总是一个与COM对象相关的重要主题。COM定义了单元模型。在单线程单元(STA)中,COM运行库会执行同步。多线程单元(MTA)的性能比较好,但没有COM运行库的同步功能。 COM组件在注册表中设置了一个配置值,从而定义了它需要的单元模型。以线程安全的方式开发的COM组件支持MTA。多个线程可以同时访问这个组件,该组件必须自己实现同步。不能处理多个线程的COM组件需要使用STA。在STA中,只有一个线程(总是 533 第?部分 基 类 库 这样)访问组件。另一个线程只有使用代理,给连接到COM对象上的线程发送一个Windows信息,才能访问组件。STA使用该Windows信息进行同步。 VB6组件仅支持STA模型。用both选项配置的COM组件支持STA和MTA。 COM组件定义了对单元的要求,而实例化COM对象的线程定义了运行它的单元。这个单元应该就是COM需要的单元。 .NET线程默认运行在MTA上。在Windows应用程序的Main()方法中,有时可以看到特性[STAThread]。这个特性指定,主线程加入STA。Windows窗体应用程序需要一个STA线程。 [STAThread] static void Main() { //... 在创建新线程时,可以将[STAThread]或[MTAThread]特性应用于线程的入口点方法,或调用Thread类的SetApartmentTherad()方法,来定义单元模型,之后启动线程: Thread t1 = new Thread(DoSomeWork); t1.SetApartmentState(ApartmentState.STA); t1.Start(); 使用GetApartmentThread()方法可以获得线程的单元。 提示: 第23章介绍了.NET与COM组件的交互操作和COM单元模型的详细内容。 18.8 BackgroundWorker组件 如果没有编写过Windows应用程序,就可以跳过这一节,继续阅读后面的内容。注意,在Windows应用程序中使用线程会增加复杂性,应在阅读了Windows窗体的章节(第28,30章)或WPF(第31章)后,再阅读本节。无论如何,从Windows窗体的角度来看,这里演示的Windows窗体应用程序都是非常简单的。 Windows窗体和WPF控件绑定到一个线程上。对于每个控件,都只能从创建该控件的线程中调用方法。也就是说,如果有一个后台线程,就不能直接在这个线程中访问UI控件。 在Windows窗体控件中,唯一可以从非创建线程中调用的是方法Invoke()、BeginInvoke()、EndInvoke()和属性InvokeRequired。BeginInvoke()和EndInvoke()是Invoke()的异步版本。这些方法会切换到创建控件的线程上,调用赋予一个委托参数的方法,该委托参数可以传送给这些方法。这些方法的使用并不简单,这就是.NET 2.0新组件BackgroundWorker和新异步模式一起开发的原因。 类BackgroundWorker定义了如表18-3所示的方法、属性和事件。 534 第18章 线程和同步 表 18-3 BackgroundWorker类的成员 说明 在激活异步任务时,属性IsBusy返回true IsBusy 在调用CancelAsync()方法后,属性CancellationPending返回true。CancellationPending 如果这个属性设置为true,异步任务就应停止其工作 方法RunWorkerAsync()引发DoWork事件,在一个单独的线程中启RunWorkerAsync() 动异步任务 DoWork 如果启用了取消功能(将WorkerSupportCancellation属性设置为CancelAsync() true),就可以用CancelAsync()方法取消异步任务 WorkerSupportCancellation 如果WorkerReportsProgress属性设置为true,BackgroundWorkerReportProgress() 就可以给出异步任务进度的临时反馈信息。调用ReportProgress()ProgressChanged 方法,异步任务可以提供了已完成的工作百分数反馈信息,之后这WorkerReportsProgress 个方法会引发ProgressChanged事件 无论取消与否,只要完成了异步任务,就引发RunWorkerCompletedRunWorkerCompleted 事件 下面的示例程序演示了BackgroundWorker控件在Windows窗体应用程序中的用法,它执行一个需要一定时间的任务。创建一个新的Windows窗体应用程序,在窗体上添加三个标签控件、三个文本框控件、两个按钮控件、一个进度条控件和一个BackgroundWorker 控件,如图18-3所示。 图 18-3 按表18-4配置控件的属性。 表 18-4 控 件 属性和事件 值 标签 Text X: 文本框 Name textbox 535 第?部分 基 类 库 (续表) 控 件 属性和事件 值 标签 Text Y: 文本框 Name textBoxY 标签 Text Result: 文本框 Name textBoxResult 按钮 Name buttonCalculate Text Calculate Click OnCalculate 按钮 Name buttonCancel Text Cancel Enabled False Click OnCancel 进度条 Name ProgressBar BackgroundWorker Name backgroundWorker DoWork OnDoWork RunWorkerCompleted OnWorkCompleted 在项目中添加结构CalcInput。这个结构用于包含文本框控件中的输入数据。 public struct CalcInput { public CalcInput(int x, int y) { this.x = x; this.y = y; } public int x; public int y; } 方法OnCalculate()是按钮控件buttonCalculate的Click事件处理程序。在执行过程中,按钮buttonCalculate被禁用,所以在计算完成之前,用户不能再次单击该按钮。要启动BackgroundWorker,可调用方法RunWorkerAsync()。BackgroundWorker使用线程池中的一个线程来计算。RunWorkerAsync()需要将输入参数传送给DoWork事件的处理程序。 private void OnCalculate(object sender, EventArgs e) { this.buttonCalculate.Enabled = false; this.textBoxResult.Text = String.Empty; this.buttonCancel.Enabled = true; this.progressBar.Value = 0; 536 第18章 线程和同步 backgroundWorker.RunWorkerAsync(new CalcInput( int.Parse(this.textBoxX.Text), int.Parse(this.textBoxY.Text))); } 方法OnDoWork()连接到BackgroundWorker控件的DoWork事件上。在DoWorkEventArgs中,通过属性Argument接收输入参数。其执行代码模拟的功能需要一定的执行时间和5秒的睡眠时间。在睡眠时间过后,将计算的结果写入DoEventArgs的Result属性。如果将计算和睡眠操作添加到OnCalculate()方法中,在用户输入时,Windows应用程序就不能获得用户输入。但是,这里使用一个单独的线程,用户界面仍是激活的。 private void OnDoWork(object sender, DoWorkEventArgs e) { CalcInput input = (CalcInput)e.Argument; Thread.Sleep(5000); e.Result = input.x + input.y; } 方法OnDoWork()完成后,BackgroundWorker控件就引发RunWorkerCompleted事件。方法OnWorkCompleted()与这个事件相关。这里从RunWorkerCompletedEventArgs参数的Result属性中接收结果,该结果写入文本框控件result中。在引发该事件时,BackgroundWorker控件会把控制权交给创建它的线程,所以不需要使用Windows窗体控件的Invoke方法,而可以直接调用Windows窗体控件的属性和方法。 private void OnWorkCompleted(object sender, RunWorkerCompletedEventArgs e) { this.textBoxResult.Text = e.Result.ToString(); this.buttonCalculate.Enabled = true; this.buttonCancel.Enabled = false; this.progressBar.Value = 100; } 现在可以测试应用程序,看看计算过程是否独立于UI线程,UI仍是激活的,窗体可以四处移动。但是,取消和进度条功能仍需要实现。 18.8.1 激活取消功能 要激活取消功能,以停止线程的运行,必须把BackgroundWorker控件的属性WorkerSupportsCancellation设置为true。接着,实现与按钮buttonCancel的Click事件相关的OnCancel处理程序。BackgroundWorker控件的CancelAsync()方法可以取消正在进行的异步任务。 private void OnCancel(object sender, EventArgs e) { backgroundWorker.CancelAsync(); } 异步任务不会自动取消。在执行异步任务的OnDoWork()处理程序中,必须修改其实 537 第?部分 基 类 库 现代码,检查BackgroundWorker控件的属性CancellationPending。这个属性在调用CancelAsync()方法时设置。如果要执行取消操作,就把DoWorkEventArgs的Cancel属性设置为true,退出处理程序。 private void OnDoWork(object sender, DoWorkEventArgs e) { CalcInput input = (CalcInput)e.Argument; for (int i = 0; i < 10; i++) { Thread.Sleep(500); if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } } e.Result = input.x + input.y; } 如果异步方法成功完成或被取消,就调用完成处理程序OnWorkCompleted()。如果取消了该方法,就不能访问Result属性,因为这会抛出一个InvalidOperationException异常,并显示操作被取消的信息。所以必须检查RunWorkerCompletedEventArgs的Cancelled属性,并根据不同的情况执行不同的操作: private void OnWorkCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) { this.textBoxResult.Text = "Cancelled"; } else { this.textBoxResult.Text = e.Result.ToString(); } this.buttonCalculate.Enabled = true; this.buttonCancel.Enabled = false; } 再次运行应用程序,就可以在用户界面上取消异步进程了。 18.8.2 激活进度功能 为了获得用户界面的进度信息,必须将BackgroundWorker控件的WorkerReports- Progress属性设置为true。 在OnDoWork方法中,可以用ReportProgress()方法报告BackgroundWorker控件的进度。 538 第18章 线程和同步 private void OnDoWork(object sender, DoWorkEventArgs e) { CalcInput input = (CalcInput)e.Argument; for (int i = 0; i < 10; i++) { Thread.Sleep(500); backgroundWorker.ReportProgress(i * 10); if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } } e.Result = input.x + input.y; } 方法ReportProgress()引发BackgroundWorker控件的ProgressChanged事件,这个事件 会将控件改为UI线程。 在ProgressChanged事件中添加方法OnProgressChanged(),在其实现代码中,给进度 条控件设置一个从ProgressChangedEventArgs的ProgressPercentage属性中接收的新值。 private void OnProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar.Value = e.ProgressPercentage; } 在OnWorkCompleted()事件处理程序中,进度条最终设置为100%。 private void OnWorkCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) { this.textBoxResult.Text = "Cancelled"; } else { this.textBoxResult.Text = e.Result.ToString(); } this.buttonCalculate.Enabled = true; this.buttonCancel.Enabled = false; this.progressBar.Value = 100; } 图18-4是正在计算的应用程序。 539 第?部分 基 类 库 图 18-4 18.9 小结 本章介绍了如何通过System.Threading命名空间编写多线程应用程序。在应用程序中使用多线程要仔细规划。太多的线程会导致资源问题,线程不足又会使应用程序执行缓慢,执行效果也不好。 .NET Framework中的System.Threading命名空间允许处理线程,但.NET Framework并没有完成多线程中所有困难的任务。我们必须考虑线程的优先级和同步问题。本章讨论了这些问题,介绍了如何在C#应用程序中为它们编码。还论述了与死锁和竞态条件相关的问题。 如果要在C#应用程序中使用多线程功能,就必须仔细规划。 下面是关于线程的一些规则: ? 尝试使同步要求降到最少。同步是非常复杂的,会阻碍线程的运行。如果尝试避 免共享状态,就可以避免同步。当然,这并不总是可行。 ? 类的静态成员应是线程安全的。.NET Framework中的类通常是这样。 ? 实例状态不需要是线程安全的。要获得最佳的性能,最好根据需要在类的外部使 用同步功能,且不对类的每个成员使用该功能。.NET Framework类的实例成员通 常不是线程安全的。Framework中每个类的这方面信息可以在Thread Safety部分 找到。 540 第18章 线程和同步 541
/
本文档为【线程和同步】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
热门搜索

历史搜索

    清空历史搜索