文章结构:
- 锁定
- 监视器
- 共享资源的同步访问
- 同步事件和等待句柄
- 多线程使用准则「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 类为必须是原子性的更新提供了更好的性能。 如果没有争夺,它会在内部执行一个锁定前缀。 在查看代码时,请注意类似于以下示例所示的代码。 在第一个示例中,状态变量是递增的:
其他参考:
- http://msdn.microsoft.com/zh-cn/library/ms173179.aspx
- http://msdn.microsoft.com/zh-cn/library/z8chs7ft.aspx
- http://msdn.microsoft.com/zh-cn/library/dd997305.aspx
- http://msdn.microsoft.com/zh-cn/magazine/cc163352.aspx 「死锁监控」
- http://msdn.microsoft.com/zh-cn/library/3e8s7xdd.aspx 「托管线程」