同步上下文(Synchronization Contexts)
手动使用锁的一个替代方案是去声明锁。通过派生ContextBoundObject和应用Synchronization属性,你告诉CLR自动加锁。
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class AutoLock : ContextBoundObject
{
public void Demo()
{
Console.Write ("Start...");
Thread.Sleep (); // We can't be preempted here
Console.WriteLine ("end"); // thanks to automatic locking!
}
}
public class Test
{
public static void Main()
{
AutoLock safeInstance = new AutoLock();
new Thread (safeInstance.Demo).Start(); // Call the Demo
new Thread (safeInstance.Demo).Start(); // method 3 times
safeInstance.Demo(); // concurrently.
}
}
/// Output
Start... end
Start... end
Start... end
CLR确保一次仅一个线程能够执行SafeInstance的代码。它是通过创建一个同步对象来实现的--并且围绕着每一个SafeInstance的方法和属性加锁。锁的范围--在这个例子中,是safeINstance对象--也称为同步上下文。
那么这是如何工作的呢?在Synchronization属性的命名空间中有一体线索:System.Runtime.Remoting.Contexts。
ContextBoundObject可以被认为是一个远程(Remote)对象,意味着所有方法的调用被拦截。为了使他可以被拦截,当我们实例化AutoLock时,CLR实际返回了一个代理--带有与AutoLock有相同方法和属性的对象,作为中间人。通过这个中间人自动加锁,总之,它围绕每一个函数调用增加了一个微妙。
自动同步不能被用于静态成员类型,也不能用于不是从ContextBoundObject中派生的类(如Window Form)。
在内部锁以同样的方式使用。你可以预期下面的例子与上面的有相同的结果:
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class AutoLock : ContextBoundObject
{
public void Demo()
{
Console.Write ("Start...");
Thread.Sleep (); // We can't be preempted here
Console.WriteLine ("end"); // thanks to automatic locking!
} public void Test()
{
new Thread (Demo).Start(); // Call the Demo
new Thread (Demo).Start(); // method 3 times
new Thread (Demo).Start(); // method 3 times
Console.ReadLine();
} public static void Main()
{new AutoLock().Test();}
}
/// Output
Start... end
Start... end
Start... end
(注意我们加了Console.ReadLine()语句)。因为一次只能一个线程执行,因此3个新的线程将阻塞在Demo上直到Test返回--它要求ReadLine。因此,我们有相同的结果,但是必须在按下Enter之后。这个线程安全的锤子足够大以致能妨碍类里的其它非常有用的线程。
进一步,我们并没有解决前面提到的问题:如果AutoLock是一个集合类,我们仍然要求围绕下面的语句加锁,假设它允许在另外一个类上:if(safeInstance.Count>0)safeInstance.RemoeAt(0);除非这个类本身的代码是一个同步的ContextBoundObject!
一个同步对象能被扩展到单一对象的范围之外。默认,如果同步对象是从另外一个类的代码中实例化,那么2者有相同的上下文(也就是说,一个更大的锁)。这种行为可以通过指定Synchronization属性的一个整数标记来改变,下面就是标记的常量:
Constant | Meaning |
NOT_SUPPORTED | Equivalent to not using the Synchronized attribute |
SUPPORTED | Joins the existing synchronization context if instantiated from another synchronized object, otherwise remains unsynchronized. |
REQUIRED (DEFAULT) |
Joins the xisting synchronization context if instantiated from another synchronized object, otherwise creates a new context. |
REQUIRES_NEW | Always creates a new synchronization context. |
所以,如果SynchronizedA实例化一个SynchronizedB对象,那么他们有不同的同步上下文,如果它们象下面那样声明:
[Synchronization(SynchronizationAttribute.REQUIRES_NEW)]
public class SynchronizedB : ContextBoundObject{...}
同步上下文的范围越大,越容易去管理,但是用于并发的机会也越小。话又说回来,独立的同步上下文会引起死锁。如
[Synchronization]
public class Deadlock : ContextBoundObject
{
public DeadLock Other;
public void Demo() { Thread.Sleep (); Other.Hello(); }
void Hello() { Console.WriteLine ("hello"); }
}
public class Test
{
static void Main()
{
Deadlock dead1 = new Deadlock();
Deadlock dead2 = new Deadlock();
dead1.Other = dead2;
dead2.Other = dead1;
new Thread (dead1.Demo).Start();
dead2.Demo();
}
}
因为每一个DeadLock实例在Test内部创建--一个非同步类--每一个实例有它自己的同步上下文,因此,它拥有自己的锁。当2个对象彼此调用时,不用多久就会发生死锁(最多1秒钟)。如果DeadLock和Test是不同的团队写的,那么这个很难察觉。要负责Test类的人去意识到死锁是不切实际的,更不用说让他解决这个问题。与显式锁相比,死锁更加明显。
重入(Reentrancy)
一个线程安全的方法有时称为可再入,因为它可以被抢先执行,然后再次调用其它线程而不会有副作用。在通常情况下,线程安全和可再入的术语被认为是同义词或非常接近。
然而,重入在自动化加锁机制中有更多的内涵。如果Synchronization属性带有reentrant为true的参数被使用:[Synchronization[true]]
那么这个同步上下文的锁将被临时释放,当它离开上下文时。在前面的例子中,这将避免死锁的发生:这正是我们所要的。但是,这个期间的有一个副作用,任何线程可以*在原始对象上调用任何方法(重入同步环境)并且释放
出了一开始想避免的并发的复杂性。这就是再入的问题。
因为[Synchronization(true)]应用在类级别,这个属性使得被类调用的每一个函数脱离上下文变成了*。
当重入变得危险时,有时可以使用几个选项。如,假设在一个同步类内实现了多线程,通过委托这个逻辑到工作线程运行在一个独立的上下文中。这些工作线程可能是不合理的妨碍了彼此间或者与非再入性的原始对象之间的的通讯。
这突出了自动同步一个基本弱点:大范围的应用锁可能制造很多困难,但也可能不会出现困难。这些困难如死锁,重入及被阉割的并发性,使得在一些简单的场景中手动加锁比起它更加有优势。