我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。
线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。
线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
1)线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
2)只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
3)只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
4)多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
C#.Net提供了三种方法来完成对共享资源 ,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。
(1) 代码域同步:使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步。
(2) 手工同步:使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和对共享资源的等待而造成的死锁解除。
(3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。
lock语句是设置锁定和解除锁定的一种简单方式。用lock语句定义的对象表示,要等待指定对象的锁定解除。只能传送引用类型,锁定值类型只是锁定了一个副本,这是没有什么意义的。编译器会提供一个锁定值类型的错误。进行了锁定后----只有一个线程得到了锁定块,就可以运行lock语句块。在lock语句块的最后,对象的锁定解除,另一个等待锁定的线程就可以获得该锁定块。
使用lock语句的两种形式:
lock (obj) { // 同步区域 } lock (typeof(StaticClass)) { }
public class Demo { public void DoThis() { lock(this) { // ... } } }
但是实例的对象也可以用于外部的同步访问,我们不在在类中控制这种访问,所以应采用SyncRoot模式。即在类中定义一个私有的对象,将这个对象用于lock语句
public class Demo { private object SyncRoot = new object(); public void DoThis() { lock(SyncRoot) { // ... } } }
使用锁定是需要时间的,但是并总是需要的。因此可以创建类的两个版本,一个同步版本,一个异步版本。
public class Demo { public virtual bool IsSynchorized { get { return false; } } /* 为了获得类的同步版本,使用该方法传递一个非同步对象,返回一个SynchornizedDemo对象*/ public static Demo Synchornized(Demo d) { if (!d.IsSynchorized) return new SynchornizedDemo(d); return d; } public virtual void DoThis() { } // 注意:使用SynchornizedDemo类时,只有方法是同步的,对这个类的两个成员的调用并没有同步 private class SynchornizedDemo : Demo { private object SyncRoot = new object(); private Demo d; public SynchornizedDemo(Demo d) { this.d = d; } public override bool IsSynchorized { get { return true; } } public override void DoThis() { lock(SyncRoot) { // ... } } } }
警告:使用SyncRoot模式可能使线程安全产生负面影响。.Net1.0集合类实现了SyncRoot模式,.Net2.0的泛型集合类不再实现这个模式。
比如:如果使用syncRoot模式锁定对象的属性访问器,使该类变成线程安全的,但是仍会出现竞态。因为在给对象属性赋值时,在调用对象属性的get和set访问器期间,对象没有锁定,另一个线程可以获取临时值。
Interlocked类用于使变量的简单语句原子化。它提供了以线程安全的方式递增、递减和交换值的方法。比如i++就不是线程安全的。
名称 | 说明 |
---|---|
Add | 已重载。 以原子操作的形式,添加两个整数并用两者的和替换第一个整数。 |
CompareExchange | 已重载。 比较两个值是否相等,如果相等,则替换其中一个值。 |
Decrement | 已重载。 以原子操作的形式递减指定变量的值并存储结果。 |
Exchange | 已重载。 以原子操作的形式将变量设置为指定的值。 |
Increment | 已重载。 以原子操作的形式递增指定变量的值并存储结果。 |
Read | 返回一个以原子操作形式加载的 64 位值。 |
具体使用可参考MSDN
C#的lock语句就被编译器解析为使用Monitor类。
Monitor 对象通过使用 Monitor.Enter、Monitor.TryEnter 和 Monitor.Exit 方法对特定对象获取锁和释放锁来公开同步访问代码区域的能力。在对代码区域获取锁后,就可以使用 Monitor.Wait、Monitor.Pulse 和 Monitor.PulseAll 方法了。如果锁被暂挂,则 Wait 释放该锁并等待通知。当 Wait 接到通知后,它将返回并再次获取该锁。Pulse 和 PulseAll 都会发出信号以便等待队列中的下一个线程继续执行。
与lock语句相比,monitor类的主要优点是:可以添加一个等待获得锁定的超时值。这样就不会无限期地等待获得锁定,而使用TryEnter方法,给它传送一个超时值,确定等待获得锁定的最长时间。如果得到了obj的锁定,就可访问由对象obj锁定的状态;否则,线程等待超时后不再等待,而是执行其他操作。也许以后,该线程会尝试再次获得该锁定。
object obj = new object(); // 1st使用方法 System.Threading.Monitor.Enter(obj); try { // ... } finally { System.Threading.Monitor.Exit(obj); } // 2nd使用方法 if (System.Threading.Monitor.TryEnter(obj, 500)) { try { // ... } finally { System.Threading.Monitor.Exit(obj); } } else { // ... }
在您的应用程序一次只处理一个异步操作时,用于处理异步操作的回调和轮询模型十分有用。 等待模型提供了一种更灵活的方式来处理多个异步操作。 有两种等待模型,是根据用于实现它们的 WaitHandle 方法命名的: 等待(任何)模型和等待(所有)模型。
要使用上述任一等待模型,您需要使用 BeginExecuteNonQuery、BeginExecuteReader 或 BeginExecuteXmlReader 方法返回的 IAsyncResult 对象的 AsyncWaitHandle 属性。 WaitAny 和 WaitAll 方法都要求您将多个 WaitHandle 对象一起组合在一个数组中,作为一个参数发送。
这两种等待方法都监控异步操作,等待操作完成。 WaitAny 方法等待任何操作完成或超时。 一旦您知道某一特定操作完成后,就可以处理其结果,然后继续等待下一个操作完成或超时。 WaitAll 方法等待 WaitHandle 实例数组中的所有进程都完成或超时后,再继续。
名称 | 说明 |
---|---|
SignalAndWait | 已重载。 以原子操作的形式,向一个 WaitHandle 发出信号并等待另一个。 |
WaitAll | 已重载。 等待指定数组中的所有元素都收到信号。 |
WaitAny | 已重载。 等待指定数组中的任一元素收到信号。 |
WaitOne | 已重载。 阻止当前线程,直到当前 WaitHandle 收到信号。 |
类Mutex,Semaohone和Event派生自基类WaitHandle。
可以使用 Mutex 对象提供对资源的独占访问。Mutex 类比 Monitor 类使用更多系统资源,但是它可以跨应用程序域边界进行封送处理,可用于多个等待,并且可用于同步不同进程中的线程。
线程调用 mutex 的 WaitOne 方法请求所有权。该调用会一直阻塞到 mutex 可用,或直至达到可选的超时间隔。如果没有任何线程拥有它,则 Mutex 的状态为已发信号的状态。
线程通过调用其 ReleaseMutex 方法释放 mutex。mutex 具有线程关联;即 mutex 只能由拥有它的线程释放。如果线程释放不是它拥有的 mutex,则会在该线程中引发 ApplicationException。
由于 Mutex 类从 WaitHandle 派生,所以您还可以结合其他等待句柄调用 WaitHandle 的静态 WaitAll 或 WaitAny 方法请求 Mutex 的所有权。
如果某个线程拥有 Mutex,则该线程就可以在重复的等待-请求调用中指定同一个 Mutex,而不必阻止其执行;但是,它必须释放 Mutex,次数与释放所属权的次数相同。
class Resource { Mutex m = new System.Threading.Mutex(); public void Access(Int32 threadNum) { m.WaitOne(); try { Console.WriteLine("Start Resource access (Thread={0})", threadNum); System.Threading.Thread.Sleep(500); Console.WriteLine("Stop Resource access (Thread={0})", threadNum); } finally { m.ReleaseMutex(); } } }
它是一种计数的互斥锁定。与互斥锁定的区别是,它可以同时由多个线程使用。
使用Semaohone类锁定,可以定义允许同时访问受信号量锁定保护的资源的线程个数。如果有许多资源,且只有一定数量的线程访问该资源,就可以使用该锁定。
编程人员应负责确保线程释放信号量的次数不会过多。例如,假定信号量的最大计数为二,线程 A 和线程 B 都进入信号量。如果线程 B 中发生了一个编程错误,导致它调用 Release 两次,则两次调用都会成功。这样,信号量的计数就已经达到了最大值,所以,当线程 A 最终调用 Release 时,将引发 SemaphoreFullException。
public class Example { // A semaphore that simulates a limited resource pool. private static Semaphore _pool; // A padding interval to make the output more orderly. private static int _padding; public static void Main() { // 信号量计数为3,初始为0 _pool = new System.Threading.Semaphore(0, 3); // 创建并启动5个线程 for (int i = 1; i <= 5; i++) { System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(Worker)); t.Start(i); } // Wait for half a second, to allow all the // threads to start and to block on the semaphore. System.Threading.Thread.Sleep(500); // The main thread starts out holding the entire // semaphore count. Calling Release(3) brings the // semaphore count back to its maximum value, and // allows the waiting threads to enter the semaphore, // up to three at a time. Console.WriteLine("Main thread calls Release(3)."); _pool.Release(3); Console.WriteLine("Main thread exits."); } private static void Worker(object num) { // Each worker thread begins by requesting the semaphore. Console.WriteLine("Thread {0} begins and waits for the semaphore.", num); // 锁定 _pool.WaitOne(); // A padding interval to make the output more orderly. int padding = System.Threading.Interlocked.Add(ref _padding, 100); Console.WriteLine("Thread {0} enters the semaphore.", num); // The thread's "work" consists of sleeping for // about a second. Each thread "works" a little // longer, just to make the output more orderly. System.Threading.Thread.Sleep(1000 + padding); Console.WriteLine("Thread {0} releases the semaphore.", num); Console.WriteLine("Thread {0} previous semaphore count: {1}", num, _pool.Release()); } }
事件是另一个系统级的资源同步方法。
1)AutoResetEvent 类表示一个本地等待处理事件,在释放了单个等待线程以后,该事件会在终止时自动重置。该类表示它的基类(即 EventWaitHandle)的特殊情况。有关自动重置事件的使用和功能,请参见 EventWaitHandle 概念文档。
在释放了单个等待线程以后,系统会自动将一个 AutoResetEvent 对象重置为非终止。如果没有线程在等待,事件对象的状态会保持为终止。AutoResetEvent 对应于 Win32 CreateEvent 调用,从而为 bManualReset 参数指定 false。
2)ManualResetEvent 类表示一个本地等待处理事件,在已发事件信号后必须手动重置该事件。此类表示其基类 EventWaitHandle 的一种特殊情况。有关手动重置事件的用法和功能,请参见 EventWaitHandle 概念文档。
在调用 ManualResetEvent 对象的 Reset 方法之前,该对象始终保持已发信号状态。在对象保持已发信号状态期间,可以释放任意数目的等待线程或在已发事件信号后仍等待事件的线程。ManualResetEvent 对应 Win32 CreateEvent 调用,它为 bManualReset 参数指定 true。
可以使用事件通知其他线程:这里有一些数据,完成了一些操作等。事件可以发信号也可以不发信号。
class CalculateTest { static void Main() { Calculate calc = new Calculate(); Console.WriteLine("Result = {0}.", calc.Result(234).ToString()); Console.WriteLine("Result = {0}.", calc.Result(55).ToString()); } } class Calculate { double baseNumber, firstTerm, secondTerm, thirdTerm; System.Threading.AutoResetEvent[] autoEvents; System.Threading.ManualResetEvent manualEvent; // Generate random numbers to simulate the actual calculations. Random randomGenerator; public Calculate() { autoEvents = new System.Threading.AutoResetEvent[]{ new System.Threading.AutoResetEvent(false), new System.Threading.AutoResetEvent(false), new System.Threading.AutoResetEvent(false) }; manualEvent = new System.Threading.ManualResetEvent(false); } void CalculateBase(object stateInfo) { baseNumber = randomGenerator.NextDouble(); // Signal that baseNumber is ready. manualEvent.Set(); } // The following CalculateX methods all perform the same // series of steps as commented in CalculateFirstTerm. void CalculateFirstTerm(object stateInfo) { // Perform a precalculation. double preCalc = randomGenerator.NextDouble(); // Wait for baseNumber to be calculated. manualEvent.WaitOne(); // Calculate the first term from preCalc and baseNumber. firstTerm = preCalc * baseNumber * randomGenerator.NextDouble(); // Signal that the calculation is finished. autoEvents[0].Set(); } void CalculateSecondTerm(object stateInfo) { double preCalc = randomGenerator.NextDouble(); manualEvent.WaitOne(); secondTerm = preCalc * baseNumber * randomGenerator.NextDouble(); autoEvents[1].Set(); } void CalculateThirdTerm(object stateInfo) { double preCalc = randomGenerator.NextDouble(); manualEvent.WaitOne(); thirdTerm = preCalc * baseNumber * randomGenerator.NextDouble(); autoEvents[2].Set(); } public double Result(int seed) { randomGenerator = new Random(seed); // Simultaneously calculate the terms. System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateBase)); System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateFirstTerm)); System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateSecondTerm)); System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateThirdTerm)); // Wait for all of the terms to be calculated. System.Threading.WaitHandle.WaitAll(autoEvents); // Reset the wait handle for the next calculation. manualEvent.Reset(); return firstTerm + secondTerm + thirdTerm; } }
ReaderWriterLockSlim 类允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁。
可以在应用程序中使用 ReaderWriterLockSlim,以便在访问一个共享资源的线程之间提供协调同步。获得的锁是针对 ReaderWriterLockSlim 本身的。
与任何线程同步机制相同,您必须确保任何线程都不会跳过 ReaderWriterLockSlim 提供的锁定。确保做到这一点的一种方法是设计一个封装该共享资源的类。此类将提供访问专用共享资源以及使用专用 ReaderWriterLockSlim 进行同步的成员。ReaderWriterLockSlim 足够有效,可用于同步各个对象。
设计您应用程序的结构,让读取和写入操作的时间尽可能最短。因为写入锁是排他的,所以长时间的写入操作会直接影响吞吐量。长时间的读取操作会阻止处于等待状态的编写器,并且,如果至少有一个线程在等待写入访问,则请求读取访问的线程也将被阻止。
.NET Framework 有两个读取器-编写器锁,即 ReaderWriterLockSlim 和 ReaderWriterLock。建议在所有新的开发工作中使用 ReaderWriterLockSlim。ReaderWriterLockSlim 类似于 ReaderWriterLock,只是简化了递归及升级和降级锁定状态的规则。ReaderWriterLockSlim 可避免多种潜在的死锁情况。此外,ReaderWriterLockSlim 的性能明显优于 ReaderWriterLock。
名称 | 说明 |
---|---|
EnterReadLock | 尝试进入读取模式锁定状态。 |
EnterUpgradeableReadLock | 尝试进入可升级模式锁定状态。 |
EnterWriteLock | 尝试进入写入模式锁定状态。 |
ExitReadLock | 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。 |
ExitUpgradeableReadLock | 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。 |
ExitWriteLock | 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。 |
TryEnterReadLock | 已重载。 尝试进入读取模式锁定状态,可以选择超时时间。 |
TryEnterUpgradeableReadLock | 已重载。 尝试进入可升级模式锁定状态,可以选择超时时间。 |
TryEnterWriteLock | 已重载。 尝试进入写入模式锁定状态,可以选择超时时间。 |
ReaderWriterLockSlim 可以处于以下四种状态之一:未进入、读取、升级和写入。
未进入:在此状态下,没有任何线程进入锁定状态(或者所有线程都已退出锁定状态)。
读取:在此状态下,一个或多个线程已进入受保护资源的读访问锁定状态。
说明: |
---|
线程既可通过使用 EnterReadLock 或 TryEnterReadLock 方法进入读取模式的锁定,也可通过从可升级模式降级进入。 |
升级:在此状态下,一个已进入锁定状态进行读取访问的线程包含升级为写入访问(即可升级模式)的选项,同时零个或多个线程已进入读取访问锁定状态。每次只有一个线程可以进入包含升级选项的锁定,其他尝试进入可升级模式的线程都将受到阻塞。
写入:在此状态下,有一个线程已进入对受保护资源进行写入访问的锁定。该线程独占了锁定状态。出于任何原因尝试进入锁定的任意其他线程都将受到阻塞。
详见MSDN