1、Monitor.Enter和Monitor.Exit
Monitor 类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。
注意:使用 Monitor 锁定对象(即引用类型)而不是值类型。
Monitor 具有以下功能:
它根据需要与某个对象相关联。
它是未绑定的,也就是说可以直接从任何上下文调用它。
不能创建 Monitor 类的实例。
将为每个同步对象来维护以下信息:
对当前持有锁的线程的引用。
对就绪队列的引用,它包含准备获取锁的线程。
对等待队列的引用,它包含正在等待锁定对象状态变化通知的线程。
使用 Enter 和 Exit 方法标记临界区的开头和结尾。
如果临界区是一个连续指令集,则由 Enter 方法获取的锁将保证只有一个线程可以使用锁定对象执行所包含的代码。在这种情况下,建议您将这些指令放在 try 块中,并将 Exit 指令放在 finally 块中。此功能通常用于同步对类的静态或实例方法的访问。
如果实例方法需要同步线程访问,则它将使用当前实例作为要锁定的对象调用 Enter 和对应的 Exit 方法。由于只能有一个线程持有当前实例上的锁,因此该方法一次只能由一个线程来执行。
静态方法是使用当前实例的 Type 作为锁定对象以类似的方式来保护的。Enter 和 Exit 方法提供的功能与 C# lock 语句提供的功能相同。
如果临界区跨越整个方法,则可以通过将 System.Runtime.CompilerServices.MethodImplAttribute 放置在方法上并在 MethodImplAttribute 的构造函数中指定 Synchronized 值来实现上述锁定功能。使用该属性后就不需要 Enter 和 Exit 语句了。请注意,该属性将使当前线程持有锁,直到方法返回;如果可以更早释放锁,则使用 Monitor 类或 C# lock 语句而不是该属性。
尽管锁定和释放给定对象的 Enter 和 Exit 语句可以跨越成员或类的边界或同时跨越两者的边界,但并不推荐这样做。
当选择要同步的对象时,应只锁定私有或内部对象。锁定外部对象可能导致死锁,这是因为不相关的代码可能会出于不同的目的而选择锁定相同的对象。
//实例方法同步线程访问
public class Account
{
int val;
public void Deposit(int x)
{
Monitor.Enter(this);
try
{
val += x;
}
finally
{
Monitor.Exit(this);
}
}
public void WithDraw(int x)
{
Monitor.Exiter(this);
try
{
val -= x;
}
finally
{
Monitor.Exit(this);
}
}
}
//静态方法同步线程访问
public class DemoStatic
{
public static int count =0;
}
public class DemoStaticLock
{
public static void Demo()
{
try
{
Monitor.Enter(typeof(DemoStatic));
DemoStatic.count++;
}
catch(Exception e)
{
Console.WriteLine("捕获异常{0}",e.ToString());
}
finally
{
Monitor.Exit(typeof(DemoStatic));
}
}
}
2、Lock/SyncLock语句
Lock/SyncLock关键字将某个语句标志为临界区。
//实例方法同步线程访问
public class Account
{
int val;
public void Deposit(int x)
{
lock(this)
{
val += x;
}
}
public void WithDraw(int x)
{
lock(this)
{
val -= x;
}
}
}
3、ReaderWriterLock
ReaderWriterLock 用于同步对资源的访问。在任一特定时刻,它允许多个线程同时进行读访问,或者允许单个线程进行写访问。在资源不经常发生更改的情况下,ReaderWriterLock 所提供的吞吐量比简单的一次只允许一个线程的锁(如 Monitor)更高。
在多数访问为读访问,而写访问频率较低、持续时间也比较短的情况下,ReaderWriterLock 的性能最好。多个读线程与单个写线程交替进行操作,所以读线程和写线程都不会长时间阻塞。
注意:长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。
一个线程可以持有读线程锁或写线程锁,但是不能同时持有两者。若要获取写线程锁,请使用 UpgradeToWriterLock 和 DowngradeFromWriterLock,而不要通过释放读线程锁的方式获取。
递归锁请求会增加锁上的锁计数。
读线程和写线程将分别排入各自的队列。当线程释放写线程锁时,此刻读线程队列中的所有等待线程都将被授予读线程锁;当已释放所有读线程锁时,写线程队列中处于等待状态的下一个线程(如果存在)将被授予写线程锁,依此类推。换句话说,ReaderWriterLock 在一组读线程和一个写线程之间交替进行操作。
当写线程队列中有一个线程在等待活动读线程锁被释放时,请求新的读线程锁的线程会排入读线程队列。即使它们能和现有的读线程锁持有者同时一起访问,也不会授予它们的请求;这可以防止写线程被读线程无限期阻塞。
大多数在 ReaderWriterLock 上获取锁的方法都采用超时值。使用超时可以避免应用程序中出现死锁。例如,某个线程可能获取了一个资源上的写线程锁,然后请求第二个资源上的读线程锁;同时,另一个线程获取了第二个资源上的写线程锁,并请求第一个资源上的读线程锁。如果不使用超时,这两个线程将出现死锁。
如果超时间隔过期并且没有授予锁请求,则此方法通过引发 ApplicationException 将控制返回给调用线程。线程可以捕捉此异常并确定下一步要进行的操作。
超时用毫秒表示。如果使用 System.TimeSpan 指定超时,则所用的值是 TimeSpan 所表示的毫秒整数的总和。
下面显示用毫秒表示的有效超时值。
值 说明
-1 Infinite.
0 无超时。
> 0 要等待的毫秒数。
除了 -1 以外,不允许使用负的超时值。如果要使用 -1 以外的负整数来指定超时,系统将使用零(无超时)。如果指定的 TimeSpan 表示的是 -1 以外的负毫秒数,将引发 ArgumentOutOfRangeException。
使用ReaderWriterLock实现互斥:首先实例化ReaderWriterLock;然后在读取临界资源前调用AcquireReaderLock方法,读过程结束后,调用ReleaseReaderLock释放读锁定;在修改临界资源之前,调用AcquireWriterLock方法请求写锁定,在写过程结束后,调用ReleaseWriterLock方法释放写锁定。
//例子
public class Account
{
int val ;
ReaderWriterLock rwl = new ReaderWriterLock();
public int Read()
{
rwl.AcquireReaderLock(Timeout.Infinite);
int iRet = 0;
try
{
iRet = val;
}
finally
{
rwl.ReleaseReaderLock();
}
return iRet;
}
public void Deposit(int x)
{
rwl.AcquireWriterLock(Timeout.Infinite);
try
{
val += x;
}
finally
{
rwl.ReleaseWriterLock();
}
}
}
4、互斥体Mutex
当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex 是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。
可以使用 WaitHandle.WaitOne 请求互斥体的所属权。拥有互斥体的线程可以在对 Wait 的重复调用中请求相同的互斥体而不会阻塞其执行。但线程必须调用 ReleaseMutex 方法同样多的次数以释放互斥体的所属权。如果线程在拥有互斥体期间正常终止,则互斥体状态设置为终止,并且下一个等待线程获得所属权。如果没有线程拥有互斥体,则互斥体状态为终止。
public class Account
{
int val = 100;
Mutex m = new Mutex();
public void Deposite(int x)
{
m.WaitOne() ; //请求获得互斥对象
try
{
val += x;
}
finally
{
m.ReleaseMutex(); //释放互斥对象
}
}
public void Withdraw(int x)
{
m.WaitOne() ;
try
{
val -= x;
}
finally
{
m.ReleaseMutex();
}
}
}
可以使用Mutex对象在线程之间跨进程进行同步。虽然Mutex不具备Monitor类的所有等待和脉冲功能,但它的确提供了创建可在进程之间使用的命名的互斥的功能。如下代码:
using System;
using System.Threading;
namespace DemoSyncAcrossProc
{
public class App
{
static public void DemoMutex()
{
Mutex mutex = new Mutex(false, "Demo");
Console.WriteLine("创建名为Demo的互斥体");
if(mutex.WaitOne())
Console.WriteLine("得到互斥体");
else
Console.WriteLine("没有得到互斥体");
Console.WriteLine("按任意键退出");
Console.ReadLine();
}
static int Main()
{
DemoMutex();
return 0;
}
}
}
编译后,开启一个命令窗口,运行该程序,不按任何键,程序显示如下:
创建名为Demo的互斥体
得到互斥体
按任意键退出
再开启第2个命令窗口执行该程序,程序显示如下:
创建名为Demo的互斥体
此时,第2个实例被阻塞(因为第1个应用实例还没释放名为Demo的互斥体)。
接着切换到第1个命令行窗口,按任意键结束第1个应用实例,再切换到第2个命令行窗口,则会观测到程序继续执行输出“得到互斥体”。
5、InterLocked
此类的方法可以防止可能在下列情况发生的错误:线程正在更新可由其他线程访问的变量时,计划程序切换上下文;或者两个线程在不同的处理器上同时执行。此类的成员不引发异常。
Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
(1).将实例变量中的值加载到寄存器中。
(2).增加或减少该值。
(3).在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤。当第一个线程重新开始执行时,它改写实例变量中的值,造成第二个线程执行增减操作的结果丢失。
Exchange 方法自动交换指定变量的值。CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。比较和交换操作按原子操作执行。
有InterLocked公开的Exchange和CompareExchange方法采用可以存储引用的Object类型的参数。但是,类型安全要求将所有参数严格类型化为Object;不能对其中一个方法的调用中简单地将对象强制转换为Object,换言之,必须创建Object类型变量,将自定义对象赋给该变量,然后传递该变量。例如:
public class DemoInterLocked
{
Object _x;
public Object X
{
set
{
Object ovalue = value;
InterLocked.CompareExchange(ref _x , ovalue , null);
}
get
{
return _x;
}
}
}
//下面用Interlocked实现互斥的例子
using System ;
using System.Threading;
namespace DemoSyncResource
{
class Resource
{
ReaderWriterLock rwl = new ReaderWriterLock();
public void Read(Int32 threadNum)
{
rwl.AcquireReaderLock(Timeout.Infinite);
try
{
Console.WriteLine("开始读资源(Thread={0})", threadNum);
Thread.Sleep(250);
Console.WriteLine("读取资源结束(Thread={0})", threadNum);
}
finally
{
rwl.ReleaseReaderLock();
}
}
public void Write(Int32 threadNum)
{
rwl.AcquireWriterLock(Timeout.Infinite);
try
{
Console.WriteLine("开始写资源 (Thread={0})", threadNum);
Thread.Sleep(750);
Console.WriteLine("写资源结束 (Thread={0})", threadNum);
}
finally
{
rwl.ReleaseWriterLock();
}
}
}
class App
{
//临界资源
static Int32 numAsyncOps = 4;
//同步对象
static AutoResetEvent asyncAreDone = new AutoResetEvent(false);
//临界资源
static Resource res = new Resource();
public static void Main()
{
for (Int32 threadNum = 0 ;threadNum < 4 ; threadNum++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(UpdateResource),threadNum);
}
asyncAreDone.WaitOne();
Console.WriteLine("所有操作都结束");
}
static void UpdateResource(object state)
{
Int32 threadNum = (Int32)state;
if ((threadNum % 2)!=0)
res.Read(threadNum);
else
res.Write(threadNum);
//利用Interlocked.Decrement互斥的修改临界资源
//每执行一个线程都将numAsyncOps减1
//如果numAsyncOps变为0,说明4个线程都执行完了
if(Interlocked.Decrement(ref numAsyncOps) == 0)
asyncAreDone.Set();
}
}
}
这5种实现互斥方法的比较:
A.Monitor.Enter/Monitor.Exit和Lock(obj)/SyncLock都是基于引用对象琐技术。锁跟临界资源捆绑在一起。这2种方法的粒度较粗。
B.Mutex是基于自身的锁。通过将一个临界资源跟一个Mutex实例相关,要求所有的请求该临界资源的线程首先获得跟它相关的Mutex锁。这种方式的锁定粒度可以自由控制,可以是一个对象、一段代码、甚至整个过程。
C.Interlocked提供了基于粒度最细的锁,它不依赖于锁定,而基于原子操作的不可分割性,它使增、减、交换、比较等动作成为一个不可分割的原子操作实现互斥。