解释WCF源代码中的奇怪同步

时间:2012-08-24 18:33:47

标签: c# wcf synchronization try-catch

在查看System.ServiceModel.Channels.BufferManager的源代码时,我注意到了这个方法:

void TuneQuotas()
{
    if (areQuotasBeingTuned)
        return;

    bool lockHeld = false;
    try
    {
        try { }
        finally
        {
            lockHeld = Monitor.TryEnter(tuningLock);
        }

        // Don't bother if another thread already has the lock
        if (!lockHeld || areQuotasBeingTuned)
            return;
        areQuotasBeingTuned = true;
    }
    finally
    {
        if (lockHeld)
        {
            Monitor.Exit(tuningLock);
        }
    }
    //
    // DO WORK... (code removed for brevity)
    //
    areQuotasBeingTuned = false;
}

显然,他们只希望一个线程运行TuneQuotas(),而其他线程如果已经被另一个线程运行则不等待。我应该注意,删除的代码没有尝试保护。

我试图理解上面这个方法的优点而不仅仅是这样做

void TuneQuotas()
{
    if(!Monitor.TryEnter(tuningLock)) return;
    //
    // DO WORK...
    //
    Monitor.Exit(tuningLock);
}

为什么他们可能会为此烦恼?我怀疑他们使用finally块的方式是防止线程中止场景,但我仍然没有看到这一点,因为即使使用所有这些代码,TuneQuotas()也会被锁定好的如果由于某种原因,一个线程没有一直到最后设置areQuotasBeingTunes=false。那么我错过了这种模式的一些很酷的东西吗?

修改 作为旁注,似乎该方法存在于.NET 4.0中,我确认使用在框架4上运行的此代码(尽管我无法确认该方法的内容与我在Web上找到的内容没有变化):< / p>

var buffMgr = BufferManager.CreateBufferManager(1, 1);
var pooledBuffMgrType = buffMgr.GetType()
    .GetProperty("InternalBufferManager")
    .GetValue(buffMgr, null)
    .GetType();

Debug.WriteLine(pooledBuffMgrType.Module.FullyQualifiedName);
foreach (var methodInfo in pooledBuffMgrType
    .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic))
{
    Debug.WriteLine(methodInfo.Name);
}

输出:

C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Runtime.DurableInstancing\v4.0_4.0.0.0__3    1bf3856ad364e35\System.Runtime.DurableInstancing.dll
ChangeQuota
DecreaseQuota
FindMostExcessivePool
FindMostStarvedPool
FindPool
IncreaseQuota
TuneQuotas
Finalize
MemberwiseClone

3 个答案:

答案 0 :(得分:1)

我会添加一些评论:

void TuneQuotas()
{
    if (areQuotasBeingTuned)
        return; //fast-path, does not require locking

    bool lockHeld = false;
    try
    {
        try { }
        finally
        {
            //finally-blocks cannot be aborted by Thread.Abort
            //The thread could be aborted after getting the lock and before setting lockHeld
            lockHeld = Monitor.TryEnter(tuningLock);
        }

        // Don't bother if another thread already has the lock
        if (!lockHeld || areQuotasBeingTuned)
            return; //areQuotasBeingTuned could have switched to true in the mean-time
        areQuotasBeingTuned = true; //prevent others from needlessly trying to lock (trigger fast-path)
    }
    finally //ensure the lock being released
    {
        if (lockHeld)
        {
            Monitor.Exit(tuningLock);
        }
    }
    //
    // DO WORK... (code removed for brevity)
    //
    //this might be a bug. There should be a call to Thread.MemoryBarrier,
    //or areQuotasBeingTuned should be volatile
    //if not, the write might never reach other processor cores
    //maybe this doesn't matter for x86
    areQuotasBeingTuned = false;
}

您提供的简单版本无法防范某些问题。至少它不是异常安全的(锁定不会被释放)。有趣的是,“复杂”版本也没有。

此方法已从.NET 4中删除。

答案 1 :(得分:1)

在.NET 4.0之前,代码中存在一个由lock声明生成的错误。它会产生类似于以下内容的东西:

Monitor.Enter(lockObject)
// see next paragraph
try
{
    // code that was in the lock block
}
finally
{
   Monitor.Exit(lockObject);
}

这意味着如果Entertry之间发生异常,则永远不会调用Exit。正如我们所提到的,这可能是由Thread.Abort引起的。

你的例子:

if(!Monitor.TryEnter(tuningLock)) return;
//
// DO WORK...
//
Monitor.Exit(tuningLock);

遇到这个问题等等。此代码被中断且Exit未被调用的窗口基本上是整个代码块 - 除了Thread.Abort之外的任何异常(<{1}})。

我不知道为什么大多数代码是用.NET编写的。但是,我猜测这段代码的编写是为了避免Entertry之间出现异常的问题。我们来看一些细节:

try{}
finally
{
  lockHeld = Monitor.TryEnter(tuningLock);
}

Finally块基本上在IL中生成约束执行区域。约束的执行区域不能被任何东西打断。因此,将TryEnter放在上面的finally块中会确保lockHeld可靠地保持lock的状态。

该代码块包含在try / finally块中,如果finally为真,则Monitor.Exit语句调用tuningLock。这意味着Entertry块之间没有任何可以中断的点。

FWIW,此方法仍在.NET 3.5中,并且在WCF 3.5源代码(不是.NET源代码)中可见。我还不知道4.0中有什么;但我想它会是一样的;即使部分结构的推动力不再存在,也没有理由改变工作代码。

有关lock 使用生成内容的详细信息,请参阅http://blogs.msdn.com/b/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx

答案 2 :(得分:0)

  

为什么他们可能会为此烦恼?

经过一些测试后,我认为看到一个原因(如果不是原因):他们可能会为此烦恼,因为它的速度要快得多!

结果Monitor.TryEnter昂贵的调用,如果对象已经锁定(如果它未锁定,TryEnter仍然非常快 - 没有问题)。因此除了第一个线程之外的所有线程都将经历缓慢。

我认为这不会那么重要;从那时起,每个线程都会尝试只使用一次锁定然后继续前进(不像他们坐在那里,尝试循环)。但是,我写了一些代码用于比较,它表明TryEnter(已经锁定)的成本很高。实际上,在我的系统上,每次调用大约需要0.3毫秒,而不需要附加调试器,这比使用简单的布尔检查慢几个数量级。

所以我怀疑,这可能出现在微软的测试结果中,所以他们通过添加快速布尔检查来优化上面的代码。但那只是我的猜测..