内容预告:
线程状态:可以通过ThreadState查看线程的状态。
关于线程的状态,有三层意思:
所以线程的状态可以是这三层中各占其一,比如:SuspendRequested, Background, WaitSleepJoin
等待句柄:
Win32 API有很多线程同步的结构,在.NET里是通过EventWaitHandle, Mutex 和Semaphore暴露的。这些类都是WaitHandle的派生类。通过命名线程,可以跨进程的工作,而不是只在线程内工作。
EventWaitHandle有两个子类,AutoResetEvent 和ManualResetEvent,两者的不同在于构造函数的参数不同。
AutoResetEvent 就像一个门禁,刷一次门卡进一个人。Auto的意思是当有刷卡的时候自动开门,人进去以后自动关门。可以通过调用WaitOne函数让一个线程在门口等,可以通过Set函数插入门卡。如果有很多线程在门外等,就排成一个队列。WaitOne可以接收一个超时参数,如果时间结束会返回一个false。
class BasicWaitHandle { static EventWaitHandle wh = new AutoResetEvent (false); static void Main() { new Thread (Waiter).Start(); Thread.Sleep (1000); // Wait for some time... wh.Set(); // OK - wake it up } static void Waiter() { Console.WriteLine ("Waiting..."); wh.WaitOne(); // Wait for notification Console.WriteLine ("Notified"); } }
输出:Waiting... (pause) Notified.
创建一个跨进程的EventWaitHandle
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto, "MyCompany.MyApp.SomeName");
生产者、消费者模型:
另一个常见的场景是:一个队列中有一个后台工作进程任务。这个队列叫做生产者/消费者队列,生产者将任务加入队,消费者将任务移出队。
生产者消费者队列可以扩展的,可以创建多个消费者,每个消费者都在一个单独的线程上服务于同一个队列。下面的例子中,单个AutoResetEvent用来当任务运行完成时给工作线程发信号:
using System; using System.Threading; using System.Collections.Generic; class ProducerConsumerQueue : IDisposable { EventWaitHandle wh = new AutoResetEvent (false); Thread worker; object locker = new object(); Queue<string> tasks = new Queue<string>(); public ProducerConsumerQueue() { worker = new Thread (Work); worker.Start(); } public void EnqueueTask (string task) { lock (locker) tasks.Enqueue (task); wh.Set(); } public void Dispose() { EnqueueTask (null); // Signal the consumer to exit. worker.Join(); // Wait for the consumer's thread to finish. wh.Close(); // Release any OS resources. } void Work() { while (true) { string task = null; lock (locker) if (tasks.Count > 0) { task = tasks.Dequeue(); if (task == null) return; } if (task != null) { Console.WriteLine ("Performing task: " + task); Thread.Sleep (1000); // simulate work... } else wh.WaitOne(); // No more tasks - wait for a signal } } } Here's a main method to test the queue: class Test { static void Main() { using (ProducerConsumerQueue q = new ProducerConsumerQueue()) { q.EnqueueTask ("Hello"); for (int i = 0; i < 10; i++) q.EnqueueTask ("Say " + i); q.EnqueueTask ("Goodbye!"); } // Exiting the using statement calls q's Dispose method, which // enqueues a null task and waits until the consumer finishes. } }
输出结果是:
Performing task: Hello Performing task: Say 1 Performing task: Say 2 Performing task: Say 3 ... ... Performing task: Say 9 Goodbye!
注意:我们在这个例子中当ProducerConsumerQueue析构时显式地关闭了等待句柄,因为我们可以在程序的生命周期里创建和销毁很多这个类的实例。
ManualResetEvent:是AutoResetEvent的变种,区别在于在线程在WaitOne后被允许通过门禁后不能自动重置。要手动Set开门,Reset关门。
Mutex:和lock的功能一样,只是可以跨进程。一个常用的功能是保证程序同时只有一个实例在运行:
class OneAtATimePlease { // Use a name unique to the application (eg include your company URL) static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo"); static void Main() { // Wait 5 seconds if contended – in case another instance // of the program is in the process of shutting down. if (!mutex.WaitOne (TimeSpan.FromSeconds (5), false)) { Console.WriteLine ("Another instance of the app is running. Bye!"); return; } try { Console.WriteLine ("Running - press Enter to exit"); Console.ReadLine(); } finally { mutex.ReleaseMutex(); } } }
Mutex有一个还不错的功能是程序在终止时如果没有调用ReleaseMutex,CLR会自动释放Mutex。
Semaphore(信号量):Semaphore就像一个夜总会,有一个保镖保护着,当里面满的时候,外面就得排除进去,直到有空位置出来。构造函数有两个参数,一个是夜总会当前的空位数量和容限(总位置数)。
一个带容限的Semaphore和Mutex以及lock都很像,除了Semaphore没有宿主,任何线程都可以释放一个信号量,在Mutex和lock上只有获得资源的线程才能释放信号量。下面的例子里,10个线程执行一个带Sleep的循环语句,一个信号量确保不超过3个线程可以同时执行Sleep:
class SemaphoreTest { static Semaphore s = new Semaphore (3, 3); // Available=3; Capacity=3 static void Main() { for (int i = 0; i < 10; i++) new Thread (Go).Start(); } static void Go() { while (true) { s.WaitOne(); Thread.Sleep (100); // Only 3 threads can get here at once s.Release(); } } }
WaitAny, WaitAll and SignalAndWait
除了Set和WaitOne之外,WaitHandle类还有几个静态函数来解决复杂的同步场景,WaitAny, WaitAll 和 SignalAndWait 函数有助于等待多个句柄(可以是多个类型的)。SignalAndWait也许最有用,当在另一个WaitHandle调用Set时,它在WaitHandle上调用WaitOne,这是一个原子操作。可以用在一对EventWaitHandles上来建立两个线程,AutoResetEvent和ManualResetEvent都有这个麻烦,第一个线程:
WaitHandle.SignalAndWait (wh1, wh2);
第二个线程:
WaitHandle.SignalAndWait (wh1, wh2);
WaitHandle.WaitAny等待每一个等待句柄数组中的句柄。WaitHandle.WaitAll等待所有给定的句柄。用门禁的例子来比喻的话,这些函数就像在门禁前排队。
同步上下文:
相比手动加锁,我们也可以通过代码自动加锁,可以从ContextBoundObject类派生然后添加Synchronization特性,CLR就会自动加锁:
using System; using System.Threading; using System.Runtime.Remoting.Contexts; [Synchronization] public class AutoLock : ContextBoundObject { public void Demo() { Console.Write ("Start..."); Thread.Sleep (1000); // We can't be preempted here Console.WriteLine ("end"); // thanks to automatic locking! } } public class Test { public static void Main() { AutoLock safeInstance = new AutoLock(); new Thread (safeInstance.Demo).Start(); // Call the Demo new Thread (safeInstance.Demo).Start(); // method 3 times safeInstance.Demo(); // concurrently. } }
输出:
Start... end
Start... end
Start... end
CLR保证同时只有一个线程执行safeInstance代码,它通过创建一个同步对象,锁定safeInstance的所有函数和字段,这个safeInstance对象叫做同步上下文。
它是如何运转的?一个ContextBoundObject可以认为是一个远程对象,意思是所有函数调用都被拦截,要使拦截生效,我们通过AutoLock来看,CLR实际上返回了一个代理,一个和AutoLock对象具有相同方法和属性的对象,做为一个中介,通过中介发生自动锁定。
但是自动锁定不能限制住静态对象,以及ContextBoundObject的派生类(比如一个Windows Form)。
[Synchronization] public class AutoLock : ContextBoundObject { public void Demo() { Console.Write ("Start..."); Thread.Sleep (1000); Console.WriteLine ("end"); } public void Test() { new Thread (Demo).Start(); new Thread (Demo).Start(); new Thread (Demo).Start(); Console.ReadLine(); } public static void Main() { new AutoLock().Test(); } }