一、线程同步概述
在多线程程序中,当存在共享变量和抢占资源的情况时,需要使用线程同步机制来防止发生这些冲突,这样才能保证得到可预见的结果,也就是线程安全的。否则就会出现不可预知的结果产生线程不安全问题。特别是在访问同一个数据的时候最为明显。主要通过以下四个方式进行:
构成 |
目的 |
Sleep |
阻止给定的时间周期 |
Join |
等待另一个线程完成 |
构成 |
目的 |
是否跨进程 |
速度 |
lock |
确保只有一个线程访问某个资源或某段代码。 |
否 |
快 |
Mutex |
确保只有一个线程访问某个资源或某段代码。可被用于防止一个程序的多个实例同时运行。 |
是 |
中等 |
Semaphore |
确保不超过指定数目的线程访问某个资源或某段代码。 |
是 |
中等 |
构成 |
目的 |
跨进程? |
速度 |
EventWaitHandle |
允许线程等待直到它受到了另一个线程发出信号。 |
是 |
中等 |
Wait 和 Pulse* |
允许一个线程等待直到自定义阻止条件得到满足。 |
否 |
中等 |
构成 |
目的 |
跨进程? |
速度 |
Interlocked* |
完成简单的非阻止原子操作。 |
是(内存共享情况下) |
非常快 |
volatile* |
允许安全的非阻止在锁之外使用个别字段。 |
非常快 |
bool blocked = (Thread.ThreadState & ThreadState.WaitSleepJoin) != 0;解除阻塞发生的情况:(其中对于使用Suspend方法挂起的线程并不视为阻塞状态)
static object locker = new object(); static int val1, val2; static void Main(string[] args) { val1 = 100; val2 = 10; Thread t1 = new Thread(Go); Thread t2 = new Thread(Go); t1.Start(); t2.Start(); Console.ReadKey(); } static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine(val1 / val2); val2 = 0; } }
Monitor.Enter(locker); try { if (val2 != 0) Console.WriteLine(val1/val2); val2 = 0; } finally { Monitor.Exit(locker); }上述使用的locker同步对象, 必须是引用类型,最好是在私有类里面定义,防止外部锁定相同对象。可使用lock(this){...}来精确控制锁的范围和粒度,同时, 锁没有阻止对同步对象本身的访问。
static void Main(string[] args) { using (var mutex = new Mutex(false, "mutex")) { if (!mutex.WaitOne(TimeSpan.FromSeconds(3), false)) { Console.WriteLine("Another app instance is running. Bye!"); return; } RunProgram(); } } static void RunProgram() { Console.WriteLine("Running. Press Enter to exit"); Console.ReadLine(); }5、Semaphore
static SemaphoreSlim sem = new SemaphoreSlim(3); static void Main(string[] args) { for (int i = 1; i < 10; i++) new Thread(Enter).Start(i); Console.ReadKey(); } static void Enter(object id) { Console.WriteLine(id + "wants to enter"); sem.Wait(); Console.WriteLine(id + "enter in!"); Thread.Sleep(1000 * (int)id); Console.WriteLine(id + "is leaving"); sem.Release(); }
static SemaphoreSlim sem = new SemaphoreSlim(3); static void Main(string[] args) { Thread t = new Thread(delegate() { try { Thread.Sleep(Timeout.Infinite); } catch (ThreadInterruptedException) { Console.WriteLine("Exception!"); } Console.WriteLine("Woken"); }); t.Start(); t.Interrupt(); Console.ReadKey(); }
WaitOne 接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的同步内容,为了避免过多的阻止发生。Reset作用是关闭旋转门,也就是无论此时是否已经set过,都将阻塞下一次WaitOne——它应该是开着的。
使用构造函数创建对象或者使用基类创建:
EventWaitHandle wh = new AutoResetEvent (false); EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);
class BasicWaitHandle { static EventWaitHandle _waitHandle = new AutoResetEvent (false); static void Main() { new Thread (Waiter).Start(); Thread.Sleep (1000); // Pause for a second... _waitHandle.Set(); // Wake up the Waiter. } static void Waiter() { Console.WriteLine ("Waiting..."); _waitHandle.WaitOne(); // Wait for notification Console.WriteLine ("Notified"); } }
EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的。
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto, "MyCompany.MyApp.SomeName");如果两个程序都运行上述代码,他们就可以彼此发送信号,等待句柄可以跨越两个进程中的所有线程。
假设希望在后台完成任务,但又不在每次得到任务时再创建一个新的线程。可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。
static EventWaitHandle ready = new AutoResetEvent(false); static EventWaitHandle go = new AutoResetEvent(false); static volatile string task; static void Main(string[] args) { new Thread(work).Start(); for (int i = 1; i <= 5; i++) { ready.WaitOne(); task = "#".PadRight(i, '@'); go.Set(); } ready.WaitOne(); task = null; go.Set(); Console.ReadKey(); } static void work() { while (true) { ready.Set(); go.WaitOne(); if (task == null) return; Console.WriteLine(task); } }
还有一个普遍的线程方案是在后台工作进程从队列中分配任务,称为生产者/消费者队列:在工作线程中生产者入列任务,消费者出列任务。下面例子中,AutoResetEvent用来通知工作线程,只有在用完任务事等待。集合类队列来表示,通过锁来控制访问确保线程安全,队列为Null时结束任务。
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); } else wh.WaitOne(); } } }
static void Main(string[] args) { using (ProducerConsumerQueue q = new ProducerConsumerQueue()) { q.EnqueueTask("Hello"); for (int i = 0; i < 10; i++) q.EnqueueTask("Say " + i); q.EnqueueTask("Goodbye!"); } Console.ReadKey(); }
ManualResetEvent是AutoResetEvent变化的一种形式,它的不同之处在于:在线程被WaitOne的调用而通过的时候,它不会自动地Reset,这个过程就像大门一样——调用Set打开门,允许任何数量的已执行WaitOne的线程通过;调用Reset则关闭大门。此时会阻塞所有调用了WaitOne的线程,只到下次调用Set时,可能会引起一系列的“等待者”在打开门时全部释放。除了这个区别之外,其他都和AutoResetEvent的用法一样。他们的速度都在一个微妙级别。
ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。
在NET4.0中出现了优化的ManualResetEventSlim类,这个可以在等待的时候有更快的响应时间,可以通过CancellationToken来取消Wait,同时这个类不是WaitHandle类的子类,而是组合了WaitHandle类的一个对象作为一个属性,返回一个WaitHandle基类对象。同时优化过的这个类的速度是原来的50倍,也就是20个纳秒级别。
ManualResetEvent是让一个线程解除一系列等待的线程的阻塞。
CountdownEvent是NET4.0中新增的类,对于使用之前版本的NET的时候,可以使使用Wait和Paulse自己写一个同样功能的类。这个类是用来等待一系列线程执行完毕之后通知当前等待的这个线程执行。
CountdownEvent countdown= new CountdownEvent(3); //初始化等待线程数为3个
static CountdownEvent _countdown = new CountdownEvent (3);
static void Main()
{
new Thread (SaySomething).Start ("I am thread 1");
new Thread (SaySomething).Start ("I am thread 2");
new Thread (SaySomething).Start ("I am thread 3");
_countdown.Wait(); // Blocks until Signal has been called 3 times
Console.WriteLine ("All threads have finished speaking!");
}
static void SaySomething (object thing)
{
Thread.Sleep (1000);
Console.WriteLine (thing);
_countdown.Signal();
}
当count还没有达到0的时候,可以使用AddCount增加,可以使用Reset重置为初始化的count数目,但是如果达到了0之后,再调用AddCount则会引发异常,此时可以使用TryAddCount,当count为0的时候返回false。
参考:http://www.albahari.com/threading/part2.aspx