一、线程同步概述
在多线程程序中,当存在共享变量和抢占资源的情况时,需要使用线程同步机制来防止发生这些冲突,这样才能保证得到可预见的结果,也就是线程安全的。否则就会出现不可预知的结果产生线程不安全问题。特别是在访问同一个数据的时候最为明显。主要通过以下四个方式进行:
- 简单阻塞:让一个线程等待另一个线程执行结束或者等待一段时间而阻塞执行,使用Sleep、Join、Task.Wait这几个方式
构成 |
目的 |
Sleep |
阻止给定的时间周期 |
Join |
等待另一个线程完成 |
- 锁:排他锁是最常见的锁机制,对于共享数据在每个线程内访问前判断锁的情况,保证每次只能有一个线程访问,使得相互不会干扰结果。排他锁使用lock关键字、Mutex类和SpinLock,对于共享锁使用Semaphore、SemaphoreSlim和读写锁。
构成 |
目的 |
是否跨进程 |
速度 |
lock |
确保只有一个线程访问某个资源或某段代码。 |
否 |
快 |
Mutex |
确保只有一个线程访问某个资源或某段代码。可被用于防止一个程序的多个实例同时运行。 |
是 |
中等 |
Semaphore |
确保不超过指定数目的线程访问某个资源或某段代码。 |
是 |
中等 |
- 信号量:这种机制使得线程一直阻塞知道接收到其他线程的通知才开始执行,避免了无效的轮询需求。最常见的方法是使用event wait handles和Monitors类的Wait/Pulse方法,.NET4.0 使用的是CountdownEvent和Barrier类。
构成 |
目的 |
跨进程? |
速度 |
EventWaitHandle |
允许线程等待直到它受到了另一个线程发出信号。 |
是 |
中等 |
Wait 和 Pulse* |
允许一个线程等待直到自定义阻止条件得到满足。 |
否 |
中等 |
- 非阻塞的同步机制:这个机制使用处理器的原语操作来实现。C#提供了非阻塞的原语:Thread.MemoryBarrier、Thread.VolatileRead、Thread.VolatileWrite、volatile关键字和Interlocked类。
构成 |
目的 |
跨进程? |
速度 |
Interlocked* |
完成简单的非阻止原子操作。 |
是(内存共享情况下) |
非常快 |
volatile* |
允许安全的非阻止在锁之外使用个别字段。 |
非常快 |
bool blocked = (Thread.ThreadState & ThreadState.WaitSleepJoin) != 0;解除阻塞发生的情况:(其中对于使用Suspend方法挂起的线程并不视为阻塞状态)
- 阻塞条件满足
- 执行时间完毕
- 使用Thread.Interrupt打断
- 使用Thread.Abort丢弃
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
Semaphore在限制并发方面非常有用,可以阻止过多的线程在一段代码中一次执行,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(); }
- 一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的所有的属性、方法和字段的访问。
- 另一个方式欺骗是通过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每个请求来自它自己的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法一定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,多半是在内存里缓存常用数据和提供基础设施服务,例如认证和审核。
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);2、跨进程的EventWaitHandle
EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的。
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto, "MyCompany.MyApp.SomeName");如果两个程序都运行上述代码,他们就可以彼此发送信号,等待句柄可以跨越两个进程中的所有线程。
3、任务确认
假设希望在后台完成任务,但又不在每次得到任务时再创建一个新的线程。可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。
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); } }
4、生成者消费者模型
还有一个普遍的线程方案是在后台工作进程从队列中分配任务,称为生产者/消费者队列:在工作线程中生产者入列任务,消费者出列任务。下面例子中,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(); }
5、ManualResetEvent
ManualResetEvent是AutoResetEvent变化的一种形式,它的不同之处在于:在线程被WaitOne的调用而通过的时候,它不会自动地reset,这个过程就像大门一样——调用Set打开门,允许任何数量的已执行WaitOne的线程通过;调用Reset关闭大门,可能会引起一系列的“等待者”直到下次门打开。
ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。