概念
多个线程同时使用共享对象会造成很多问题,同步这些线程使得对共享对象的操作能够以正确的顺序执行非常重要,导致竞争条件的问题的原因就是线程没有正确的同步。当一个线程在使用共享资源的时候,其他线程应该依次等待,这种常见问题通常被称为线程同步。
更多内容
首先应尽量避免在多个线程间使用单一对象,从而去掉复杂的同步构造。如果实在无法避免,则只能使用原子操作。这个操作一旦开始,就一直运行到结束,不会被线程调度机制打断。这就避免了使用锁,也避免了死锁。
如果原子操作不可行,且程序逻辑很复杂,我们只能使用不同的方式来协调线程。我们可以将等待的线程置于阻塞状态,阻塞状态会尽量少地占用CPU资源,然而这以为着至少出现一次上下文切换(context switch)。上下文切换是指,操作系统的线程调度器会保存等待线程的状态,并切换到另一个线程,最后依次恢复等待的线程的状态,这相当消耗资源。如果线程被挂起很久,这是值得的,这被称为内核模式(kernel-mode),因为只有系统的内核才能阻止线程使用CPU时间。
但如果线程只是需要等待很少的一段时间,最好只是简单的等待而不是置于阻塞状态。虽然线程等待浪费CPU时间,但避免了上下文切换耗费的CPU时间,该方式成为用户模式(user-mode)。该方式非常轻量,速度很快,但如果线程需要等待很长时间则会浪费大量CPU时间。
混合模式(hybrid)很好的利用了这两种模式。混合模式先尝试使用用户模式等待,如果线程等待足够长时间,则会切换到阻塞状态以节省CPU时间。
基本原子操作
借助于Interlocked类,该类提供了Increment、Decrement、Add等基本数学操作的原子方法,我们无需锁定对象机可获取正确的结果。
Interlocked.Increment(counter);
Interlocked.Decrement(counter);
Mutex类
这是一种原始的同步方式,其只对一个线程授予对共享资源的独占访问。
程序启动,定义了一个指定名称的互斥量:MutexName,设置initialOwner标志位false。无法获取互斥量,则m.WaitOne(TimeSpan.FromSeconds(15), false)为true,表示正在阻塞该线程,m.ReleaseMutex会释放互斥量;如果获取到了互斥量,m.WaitOne(TimeSpan.FromSeconds(15), false)为false,执行相应code。因此务必要正确关闭互斥量,最好使用using代码块来包裹互斥量对象。

using System; using System.Threading; namespace MutexTest { class Program { static void Main(string[] args) { const string MutexName = "csthreadingcookbook"; using (var m=new Mutex(false,MutexName)) { if (!m.WaitOne(TimeSpan.FromSeconds(15), false)) { Console.WriteLine("second instance is running"); Console.ReadLine(); } else { Console.WriteLine("running"); Console.ReadLine(); m.ReleaseMutex(); } } } } }
SemaphoreSlim类
SemaphoreSlim作为Semaohore类的轻量级版本,该类限制访问同一资源的线程数量。
如果当前访问共享资源的线程数量未超过限制,则_semaphore.Wait不会阻塞当前线程;若已超过限制,则_semaphore.Wait会阻塞当前线程,直到有线程执行_semaphore.Release(这意味着有一个线程释放了对共享资源的访问),才会继续执行。
SemaphoreSlim不使用Windows内核信号量,而且不支持进程间同步。

class Program { static void Main(string[] args) { for (int i = 0; i < 10; i++) { string threadname = "Thread" + i; int secondWait = 2 + 2 * i; var t = new Thread(() => AccessDataBase(threadname, secondWait)); t.Start(); } Console.ReadLine(); } static SemaphoreSlim _semaphore = new SemaphoreSlim(4); static void AccessDataBase(string name, int seconds) { Console.WriteLine($"{name} waits to a access a database"); _semaphore.Wait(); Console.WriteLine($"{name} was granted an access to a database"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} is completed"); _semaphore.Release(); } }
AutoResetEvent类
借助AutoResetEvent类从一个线程向另外一个线程发送通知。调用_mainEvent.WaitOne该线程会被阻塞,直到调用_mainEvent.Set。

class Program { private static AutoResetEvent _workEvent = new AutoResetEvent(false); private static AutoResetEvent _mainEvent = new AutoResetEvent(false); static void Main(string[] args) { Thread t = new Thread(() => Process()); t.Start(); Console.WriteLine("wait for another thread to complete work");//2 _workEvent.WaitOne(); Console.WriteLine("first operation is completed");//4 Console.WriteLine("start an opeation on main thread");//5 Thread.Sleep(TimeSpan.FromSeconds(10)); _mainEvent.Set(); Console.WriteLine("second operation on second thread");//7 _workEvent.WaitOne(); Console.WriteLine("second operation is completed");//10 Console.ReadLine(); } static void Process() { Console.WriteLine("do something……");//1 Thread.Sleep(TimeSpan.FromSeconds(10)); Console.WriteLine("work done");//3 _workEvent.Set(); Console.WriteLine("wait for main thread to complete work");//6 _mainEvent.WaitOne(); Console.WriteLine("start second operation");//8 Thread.Sleep(TimeSpan.FromSeconds(10)); Console.WriteLine("second work is done");//9 _workEvent.Set(); } }
ManualRestEventSlim类
ManualRestEventSlim类可以在线程间以更灵活的方式传递信号。_workEvent.Wait会阻塞当前线程,_workEvent.Set会恢复当前线程,但_workEvent.Reset会再次阻塞当前线程,直到再次调用_workEvent.Set方法。

class Program { private static ManualResetEventSlim _workEvent = new ManualResetEventSlim(false); static void Main(string[] args) { Thread t1 = new Thread(() => TravelThroughGates("T1", 5)); Thread t2 = new Thread(() => TravelThroughGates("T2", 6)); Thread t3 = new Thread(() => TravelThroughGates("T3", 12)); t1.Start(); t2.Start(); t3.Start(); Sleep(TimeSpan.FromSeconds(6)); Console.WriteLine("The gates are now open 1"); _workEvent.Set(); Sleep(TimeSpan.FromSeconds(2)); _workEvent.Reset(); Console.WriteLine("The gates have been closed 1"); Sleep(TimeSpan.FromSeconds(10)); Console.WriteLine("The gate are open for the second time"); _workEvent.Set(); Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("The gate have been closed 2"); _workEvent.Reset(); Console.ReadLine(); } static void TravelThroughGates(string threadName, int seconds) { Console.WriteLine($"{threadName} falls to sleep"); Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{threadName} waits for the gates open"); _workEvent.Wait(); Console.WriteLine($"{threadName} enters the gates"); } }
CountDownEvent类
主线程中_countdown.Wait会阻塞当前线程,直到调用_countdown.Signal的线程数达到指定次数;每个线程执行相应code之后,调用_countdown.Signal。达到次数后,主线程继续执行。

static CountDownEvent _countdown=new CountDownEvent(2); { var t1=new Thread(()=>FunName(para1,para2)); var t2=new Thread(()=>FunName(para1,para2)); t1.Start(); t2.Start(); _countdown.Wait(); _countdown.Dispose(); } FunName(……) { //Code _countdown.Signal(); }
Barrier类
其提供一个回调函数,每次线程调用SignalAndWait方法后该回调函数会被执行(完成指定线程数)。

class Program { static Barrier _barrier = new Barrier(2, b => Console.WriteLine($"end of phase {b.CurrentPhaseNumber + 1}")); static void Main(string[] args) { Thread t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", 5)); Thread t2 = new Thread(() => PlayMusic("the singer", "sing his song", 2)); t1.Start(); t2.Start(); Console.ReadLine(); } static void PlayMusic(string name, string message, int seconds) { for (int i = 0; i < 3; i++) { Console.WriteLine("----------------"); Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} starts to {message}"); Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} finishes to {message}"); _barrier.SignalAndWait(); } } }
ReadWriterLockSlim类
在多线程中对一个集合进行读写操作。ReadWriterLockSlim代表了一个管理资源访问的锁,允许多个线程同时读取,以及独占写。

class Program { static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim(); static Dictionary<int, int> items = new Dictionary<int, int>(); static void Main(string[] args) { new Thread(Read) { IsBackground = true }.Start(); new Thread(Read) { IsBackground = true }.Start(); new Thread(Read) { IsBackground = true }.Start(); new Thread(() => Write("T1")) { IsBackground = true }.Start(); new Thread(() => Write("T2")) { IsBackground = true }.Start(); Sleep(1000); Console.ReadLine(); } static void Read() { Console.WriteLine(CurrentThread.Name + " Reading contents of a dictionary"); while (true) { try { _rw.EnterReadLock(); foreach (var key in items.Keys) { Sleep(100); Console.WriteLine(items[key]); } } catch (Exception) { throw; } finally { _rw.ExitReadLock(); } } } static void Write(string threadName) { while (true) { try { int newKey = new Random().Next(250); _rw.EnterUpgradeableReadLock(); if (!items.ContainsKey(newKey)) { try { _rw.EnterWriteLock(); items[newKey] = newKey; Console.WriteLine($"new key {newKey} is added to dic by {threadName}"); } catch (Exception) { throw; } finally { _rw.ExitWriteLock(); } Sleep(100); } } catch (Exception) { throw; } finally { _rw.ExitUpgradeableReadLock(); } } } }
SpinWait类
采用混合模式。volatile关键字指出一个字段可能会被多个线程修改,确保该字段总是最新值。刚开始SpinWait尝试使用用户模式,在N个迭代后,开始切换线程为阻塞状态。

class Program { static volatile bool isCompleted = false; static void Main(string[] args) { Thread t1 = new Thread(UserMode); Thread t2 = new Thread(HybridSpin); Console.WriteLine("Running user mode waiting"); t1.Start(); Sleep(200); isCompleted = true; Sleep(1000); isCompleted = false; Console.WriteLine("Running hybridspin waiting"); t2.Start(); Sleep(50); isCompleted = true; Console.ReadLine(); } static void UserMode() { while (!isCompleted) { Console.WriteLine("."); } Console.WriteLine(); Console.WriteLine("waiting is complete"); } static void HybridSpin() { var w = new SpinWait(); while (!isCompleted) { w.SpinOnce(); Console.WriteLine(w.NextSpinWillYield); } Console.WriteLine("waiting is complete 1"); } }
注:本文是在阅读《C#多线程编程实战》后所写,部分内容引用该书内容,这是一本不错的书,感谢!