文章结构:
- 锁定
- 监视器
- 共享资源的同步访问
- 同步事件和等待句柄
- 多线程使用准则「MSDN」
锁定
无论是程序还是数据库,只要是涉及到并发的问题,都难免会有「锁」的概念。
在C#中,使用lock关键字来对某个对象实施加锁的操作。
lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
lock调用 public static string lockTest = string.Empty; // 用于测试线程锁 。。。。。。 /* 关于lock关键字: * lock必须有一个参数对象,且这个参数对象必须是引用类型的,也就是说,不可以是int等; * 参数对象不可以是用来定义锁的范围的,这个范围主要取决于参数对象的可访问限制,private/internal/public等; * 这个提供的对象是用来唯一的标识由多个线程共享的资源,也通常表示需要进行线程同步的资源; * 也就是说,当资源对象「R1」被线程「T1」锁住后,假如线程「T2」想要使用「R1」,就必须要等「T1」执行完才可以; * 尽量不是锁定「public」类型的对象,特别是字条串,因为这样可能会造成不可预知的问题; * 也就是说,如果我定义了一个资源「R1」,我在「T1」中要使用「R1」,所以我在「T1」中执行lock(「R1」), * 然后,「T2」也想要访问「R1」,但是因为此时「R1」已经被「T1」锁定,所以「T2」的申请是失败的, * 所以「T2」会开始等待,等待「T1」执行完并释放「R1」; * */ lock (lockTest) { lockTest = Thread.CurrentThread.Name; Console.WriteLine("\n\n" + Thread.CurrentThread.Name + ":\tLock Block starting..."); for (int i = 0; i < 10; i++) Console.WriteLine(lockTest + ":\t"+i+"th"); Console.WriteLine(Thread.CurrentThread.Name + ":\tLock Block end." + "\n\n"); }
监视器
当多个线程公用一个对象的时候,应该使用监视器类「System.Threading.Monitor」,而不是锁「lock」。
首先,Monitor的作用与lock关键字类似,都是防止多个线程同时执行代码块。
object objLock = null; lock (objLock) { // To do something... } // 等效于 object objMonitor = null; System.Threading.Monitor.Enter(objMonitor); try { // To do something... } finally { System.Threading.Monitor.Exit(objMonitor); }
其次,在对小型的代码块进行操作的时候,显然使用lock关键字更加的简洁。
但是,如果要实现一个很复杂的逻辑,或者要根据线程的远行情况来对当前锁定的对象进行控制的话,显然Monitor就更加的方便了。
而且,就MSDN上的说明,lock关键字的内部实现也是使用的Monitor,只不过其只是调用了「Enter」「Exit」这两个方法,并且加代码块中的代码放于「Try…Finally…」中。
在使用的时候,一定要注意的就是Monitor只是针对引用类型对象的操作,而不是对值类型的操作。
如果使用了值类型,就会引发「从不同步的代码块中调用了对象同步方法」。
这主要是因为:
Monitor 将锁定对象(即引用类型)而非值类型。 在您将值类型传递给 Enter 和 Exit 时,它会针对每个调用分别装箱。 由于每个调用都创建一个单独的对象,所以 Enter 从不拦截,并且其旨在保护的代码并未真正同步。 此外,传递给 Exit 的对象不同于传递给 Enter 的对象,所以 Monitor 将引发 SynchronizationLockException,并显示消息“从不同步的代码块中调用了对象同步方法”。下面的示例演示这些问题。
除了使用Monitor与lock以外,还可以使用「Mutex」提供对资源的独占访问。Mutex 类比 Monitor 类使用更多系统资源,但是它可以跨应用程序域边界进行封送处理,可用于多个等待,并且可用于同步不同进程中的线程。「MSDN」
名称 说明 Enter(Object) 在指定对象上获取排他锁。 Enter(Object, Boolean) 获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。 Exit 释放指定对象上的排他锁。 Pulse 通知等待队列中的线程锁定对象状态的更改。 PulseAll 通知所有的等待线程对象状态的更改。 TryEnter(Object) 尝试获取指定对象的排他锁。 TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。 TryEnter(Object, Int32) 在指定的毫秒数内尝试获取指定对象上的排他锁。 TryEnter(Object, TimeSpan) 在指定的时间量内尝试获取指定对象上的排他锁。 TryEnter(Object, Int32, Boolean) 在指定的毫秒数中,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。 TryEnter(Object, TimeSpan, Boolean) 在指定的一段时间内,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。 Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 Wait(Object, Int32) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 Wait(Object, TimeSpan) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 Wait(Object, Int32, Boolean) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 此方法还指定是否在等待之前退出上下文的同步域(如果处于同步上下文中的话)然后重新获取该同步域。 Wait(Object, TimeSpan, Boolean) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 可以在等待之前退出同步上下文的同步域,随后重新获取该域。 「MSDN」中关于「Monitor」的例子using System; using System.Threading; // Note: The class whose internal public member is the synchronizing // method is not public; none of the client code takes a lock on the // Resource object.The member of the nonpublic class takes the lock on // itself. Written this way, malicious code cannot take a lock on // a public object. class SyncResource { public void Access(Int32 threadNum) { // Uses Monitor class to enforce synchronization. lock (this) { // Synchronized: Despite the next conditional, each thread // waits on its predecessor. if (threadNum % 2 == 0) { Thread.Sleep(2000); } Console.WriteLine("Start Synched Resource access (Thread={0})", threadNum); Thread.Sleep(200); Console.WriteLine("Stop Synched Resource access (Thread={0})", threadNum); } } } // Without the lock, the method is called in the order in which threads reach it. class UnSyncResource { public void Access(Int32 threadNum) { // Does not use Monitor class to enforce synchronization. // The next call throws the thread order. if (threadNum % 2 == 0) { Thread.Sleep(2000); } Console.WriteLine("Start UnSynched Resource access (Thread={0})", threadNum); Thread.Sleep(200); Console.WriteLine("Stop UnSynched Resource access (Thread={0})", threadNum); } } public class App { static Int32 numAsyncOps = 5; static AutoResetEvent asyncOpsAreDone = new AutoResetEvent(false); static SyncResource SyncRes = new SyncResource(); static UnSyncResource UnSyncRes = new UnSyncResource(); public static void Main() { for (Int32 threadNum = 0; threadNum < 5; threadNum++) { ThreadPool.QueueUserWorkItem(new WaitCallback(SyncUpdateResource), threadNum); } // Wait until this WaitHandle is signaled. asyncOpsAreDone.WaitOne(); Console.WriteLine("\t\nAll synchronized operations have completed.\t\n"); // Reset the thread count for unsynchronized calls. numAsyncOps = 5; for (Int32 threadNum = 0; threadNum < 5; threadNum++) { ThreadPool.QueueUserWorkItem(new WaitCallback(UnSyncUpdateResource), threadNum); } // Wait until this WaitHandle is signaled. asyncOpsAreDone.WaitOne(); Console.WriteLine("\t\nAll unsynchronized thread operations have completed."); } // The callback method's signature MUST match that of a // System.Threading.TimerCallback delegate (it takes an Object // parameter and returns void). static void SyncUpdateResource(Object state) { // This calls the internal synchronized method, passing // a thread number. SyncRes.Access((Int32) state); // Count down the number of methods that the threads have called. // This must be synchronized, however; you cannot know which thread // will access the value **before** another thread's incremented // value has been stored into the variable. if (Interlocked.Decrement(ref numAsyncOps) == 0) { // Announce to Main that in fact all thread calls are done. asyncOpsAreDone.Set(); } } // The callback method's signature MUST match that of a // System.Threading.TimerCallback delegate (it takes an Object // parameter and returns void). static void UnSyncUpdateResource(Object state) { // This calls the unsynchronized method, passing a thread number. UnSyncRes.Access((Int32) state); // Count down the number of methods that the threads have called. // This must be synchronized, however; you cannot know which thread // will access the value **before** another thread's incremented // value has been stored into the variable. if (Interlocked.Decrement(ref numAsyncOps) == 0) { // Announce to Main that in fact all thread calls are done. asyncOpsAreDone.Set(); } } }try { if (Monitor.TryEnter(workerA)) { Monitor.PulseAll(workerA); monitorTest = "Monitor\t" + Thread.CurrentThread.Name; Console.WriteLine("\n\n" + Thread.CurrentThread.Name + ":\tLock Block starting..."); for (int i = 0; i < 10; i++) Console.WriteLine(workerA.WorkerName + ":\t" + i + "th"); Console.WriteLine(Thread.CurrentThread.Name + ":\tLock Block end." + "\n\n"); Monitor.PulseAll(workerA); } } finally { Monitor.Exit(workerA); }
共享资源的同步访问
在多线程环境中保护模块中的全局数据:
- 应该程序中所有的线程都可以访问方法中的公用字段。
- 要同步对公用字段的访问,可以使用属性替代字段,并使用ReaderWriterLock对象控制访问。
- ReaderWriterLock:定义支持单个写线程和多个读线程的锁。
- 要注意ReaderWriterLock的ReleaseLock、ReleaseReaderLock、ReleaseWriterLock方法。
- 「MSDN」建议使用 ReaderWriterLockSlim 而不是 ReaderWriterLock。
- 长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。
// This example shows a ReaderWriterLock protecting a shared // resource that is read concurrently and written exclusively // by multiple threads. // The complete code is located in the ReaderWriterLock // class topic. using System; using System.Threading; public class Test { // Declaring the ReaderWriterLock at the class level // makes it visible to all threads. static ReaderWriterLock rwl = new ReaderWriterLock(); // For this example, the shared resource protected by the // ReaderWriterLock is just an integer. static int resource = 0; const int numThreads = 26; static bool running = true; static Random rnd = new Random(); // Statistics. static int readerTimeouts = 0; static int writerTimeouts = 0; static int reads = 0; static int writes = 0; public static void Main(string[] args) { // Start a series of threads. Each thread randomly // performs reads and writes on the shared resource. Thread[] t = new Thread[numThreads]; for (int i = 0; i < numThreads; i++) { t[i] = new Thread(new ThreadStart(ThreadProc)); t[i].Name = new String(Convert.ToChar(i + 65), 1); t[i].Start(); if (i > 10) Thread.Sleep(300); } // Tell the threads to shut down, then wait until they all // finish. running = false; for (int i = 0; i < numThreads; i++) { t[i].Join(); } // Display statistics. Console.WriteLine("\r\n{0} reads, {1} writes, {2} reader time-outs, {3} writer time-outs.", reads, writes, readerTimeouts, writerTimeouts); Console.WriteLine("Press ENTER to exit."); Console.ReadLine(); } static void ThreadProc() { // As long as a thread runs, it randomly selects // various ways to read and write from the shared // resource. Each of the methods demonstrates one // or more features of ReaderWriterLock. while (running) { double action = rnd.NextDouble(); if (action < .8) ReadFromResource(10); else if (action < .81) ReleaseRestore(50); else if (action < .90) UpgradeDowngrade(100); else WriteToResource(100); } } // Shows how to request and release a reader lock, and // how to handle time-outs. static void ReadFromResource(int timeOut) { try { rwl.AcquireReaderLock(timeOut); try { // It is safe for this thread to read from // the shared resource. Display("reads resource value " + resource); Interlocked.Increment(ref reads); } finally { // Ensure that the lock is released. rwl.ReleaseReaderLock(); } } catch (ApplicationException) { // The reader lock request timed out. Interlocked.Increment(ref readerTimeouts); } } // Shows how to request and release the writer lock, and // how to handle time-outs. static void WriteToResource(int timeOut) { try { rwl.AcquireWriterLock(timeOut); try { // It is safe for this thread to read or write // from the shared resource. resource = rnd.Next(500); Display("writes resource value " + resource); Interlocked.Increment(ref writes); } finally { // Ensure that the lock is released. rwl.ReleaseWriterLock(); } } catch (ApplicationException) { // The writer lock request timed out. Interlocked.Increment(ref writerTimeouts); } } // Shows how to request a reader lock, upgrade the // reader lock to the writer lock, and downgrade to a // reader lock again. static void UpgradeDowngrade(int timeOut) { try { rwl.AcquireReaderLock(timeOut); try { // It is safe for this thread to read from // the shared resource. Display("reads resource value " + resource); Interlocked.Increment(ref reads); // If it is necessary to write to the resource, // you must either release the reader lock and // then request the writer lock, or upgrade the // reader lock. Note that upgrading the reader lock // puts the thread in the write queue, behind any // other threads that might be waiting for the // writer lock. try { LockCookie lc = rwl.UpgradeToWriterLock(timeOut); try { // It is safe for this thread to read or write // from the shared resource. resource = rnd.Next(500); Display("writes resource value " + resource); Interlocked.Increment(ref writes); } finally { // Ensure that the lock is released. rwl.DowngradeFromWriterLock(ref lc); } } catch (ApplicationException) { // The upgrade request timed out. Interlocked.Increment(ref writerTimeouts); } // When the lock has been downgraded, it is // still safe to read from the resource. Display("reads resource value " + resource); Interlocked.Increment(ref reads); } finally { // Ensure that the lock is released. rwl.ReleaseReaderLock(); } } catch (ApplicationException) { // The reader lock request timed out. Interlocked.Increment(ref readerTimeouts); } } // Shows how to release all locks and later restore // the lock state. Shows how to use sequence numbers // to determine whether another thread has obtained // a writer lock since this thread last accessed the // resource. static void ReleaseRestore(int timeOut) { int lastWriter; try { rwl.AcquireReaderLock(timeOut); try { // It is safe for this thread to read from // the shared resource. Cache the value. (You // might do this if reading the resource is // an expensive operation.) int resourceValue = resource; Display("reads resource value " + resourceValue); Interlocked.Increment(ref reads); // Save the current writer sequence number. lastWriter = rwl.WriterSeqNum; // Release the lock, and save a cookie so the // lock can be restored later. LockCookie lc = rwl.ReleaseLock(); // Wait for a random interval (up to a // quarter of a second), and then restore // the previous state of the lock. Note that // there is no time-out on the Restore method. Thread.Sleep(rnd.Next(250)); rwl.RestoreLock(ref lc); // Check whether other threads obtained the // writer lock in the interval. If not, then // the cached value of the resource is still // valid. if (rwl.AnyWritersSince(lastWriter)) { resourceValue = resource; Interlocked.Increment(ref reads); Display("resource has changed " + resourceValue); } else { Display("resource has not changed " + resourceValue); } } finally { // Ensure that the lock is released. rwl.ReleaseReaderLock(); } } catch (ApplicationException) { // The reader lock request timed out. Interlocked.Increment(ref readerTimeouts); } } // Helper method briefly displays the most recent // thread action. Comment out calls to Display to // get a better idea of throughput. static void Display(string msg) { Console.Write("Thread {0} {1}. \r", Thread.CurrentThread.Name, msg); } }
同步事件和等待句柄
同步事件:
- AutoResetEvent:只要激活线程,它的状态将自动从终止变为非终止。
- ManualResetEvent:允许它的终止状态激活任意多个线程,只有它的Reset方法被调用的时候才还原到非终止状态。
using System; using System.Threading; // Visual Studio: Replace the default class in a Console project with // the following class. class Example { private static AutoResetEvent event_1 = new AutoResetEvent(true); private static AutoResetEvent event_2 = new AutoResetEvent(false); static void Main() { Console.WriteLine("Press Enter to create three threads and start them.\r\n" + "The threads wait on AutoResetEvent #1, which was created\r\n" + "in the signaled state, so the first thread is released.\r\n" + "This puts AutoResetEvent #1 into the unsignaled state."); Console.ReadLine(); for (int i = 1; i < 4; i++) { Thread t = new Thread(ThreadProc); t.Name = "Thread_" + i; t.Start(); } Thread.Sleep(250); for (int i = 0; i < 2; i++) { Console.WriteLine("Press Enter to release another thread."); Console.ReadLine(); event_1.Set(); Thread.Sleep(250); } Console.WriteLine("\r\nAll threads are now waiting on AutoResetEvent #2."); for (int i = 0; i < 3; i++) { Console.WriteLine("Press Enter to release a thread."); Console.ReadLine(); event_2.Set(); Thread.Sleep(250); } // Visual Studio: Uncomment the following line. //Console.Readline(); } static void ThreadProc() { string name = Thread.CurrentThread.Name; Console.WriteLine("{0} waits on AutoResetEvent #1.", name); event_1.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #1.", name); Console.WriteLine("{0} waits on AutoResetEvent #2.", name); event_2.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #2.", name); Console.WriteLine("{0} ends.", name); } } /* This example produces output similar to the following: Press Enter to create three threads and start them. The threads wait on AutoResetEvent #1, which was created in the signaled state, so the first thread is released. This puts AutoResetEvent #1 into the unsignaled state. Thread_1 waits on AutoResetEvent #1. Thread_1 is released from AutoResetEvent #1. Thread_1 waits on AutoResetEvent #2. Thread_3 waits on AutoResetEvent #1. Thread_2 waits on AutoResetEvent #1. Press Enter to release another thread. Thread_3 is released from AutoResetEvent #1. Thread_3 waits on AutoResetEvent #2. Press Enter to release another thread. Thread_2 is released from AutoResetEvent #1. Thread_2 waits on AutoResetEvent #2. All threads are now waiting on AutoResetEvent #2. Press Enter to release a thread. Thread_2 is released from AutoResetEvent #2. Thread_2 ends. Press Enter to release a thread. Thread_1 is released from AutoResetEvent #2. Thread_1 ends. Press Enter to release a thread. Thread_3 is released from AutoResetEvent #2. Thread_3 ends. */
多线程使用准则「MSDN」:
不要使用 Thread.Abort 终止其他线程。 对另一个线程调用 Abort 无异于引发该线程的异常,也不知道该线程已处理到哪个位置。
不要使用 Thread.Suspend 和 Thread.Resume 同步多个线程的活动。 请使用 Mutex、ManualResetEvent、AutoResetEvent 和 Monitor。
不要从主程序中控制辅助线程的执行(如使用事件), 而应在设计程序时让辅助线程负责等待任务,执行任务,并在完成时通知程序的其他部分。 如果不阻止辅助线程,请考虑使用线程池线程。 如果阻止辅助线程,Monitor.PulseAll 会很有帮助。
不要将类型用作锁定对象。 例如,避免在 C# 中使用 lock(typeof(X)) 代码,或在 Visual Basic 中使用 SyncLock(GetType(X)) 代码,或将 Monitor.Enter 和 Type 对象一起使用。 对于给定类型,每个应用程序域只有一个 System.Type 实例。 如果您锁定的对象的类型是 public,您的代码之外的代码也可锁定它,但会导致死锁。 有关其他信息,请参见可靠性最佳做法。
锁定实例时要谨慎,例如,C# 中的 lock(this) 或 Visual Basic 中的 SyncLock(Me)。 如果您的应用程序中不属于该类型的其他代码锁定了该对象,则会发生死锁。
一定要确保已进入监视器的线程始终离开该监视器,即使当线程在监视器中时发生异常也是如此。 C# 的 lock 语句和 Visual Basic 的 SyncLock 语句可自动提供此行为,它们用一个 finally块来确保调用 Monitor.Exit。 如果无法确保调用 Exit,请考虑将您的设计更改为使用 Mutex。 Mutex 在当前拥有它的线程终止后会自动释放。
一定要针对那些需要不同资源的任务使用多线程,避免向单个资源指定多个线程。 例如,任何涉及 I/O 的任务都会从其拥有其自己的线程这一点得到好处,因为此线程在 I/O 操作期间将阻止,从而允许其他线程执行。 用户输入是另一种可从专用线程获益的资源。 在单处理器计算机上,涉及大量计算的任务可与用户输入和涉及 I/O 的任务并存,但多个计算量大的任务将相互竞争。
对于简单的状态更改,请考虑使用 Interlocked 类的方法,而不是 lock 语句(在 Visual Basic 中为 SyncLock)。 lock 语句是一个优秀的通用工具,但是 Interlocked 类为必须是原子性的更新提供了更好的性能。 如果没有争夺,它会在内部执行一个锁定前缀。 在查看代码时,请注意类似于以下示例所示的代码。 在第一个示例中,状态变量是递增的:
其他参考: