【问题标题】:Detecting that a method is called without a lock检测方法在没有锁的情况下被调用
【发布时间】:2017-03-03 08:48:03
【问题描述】:

有没有什么方法可以检测到我的代码中的某个方法被调用而不在调用堆栈中的以下任何方法中使用任何锁?
目标是调试有故障的应用程序并找出某些代码片段是否不是线程安全的。

【问题讨论】:

  • ReSharper 发现成员变量有时在锁内有时在锁外使用的情况。
  • @Bernhard Hiller,Resharper 在这方面的能力非常非常有限。它基于静态分析,仅捕获有限的案例子集。
  • 您希望能够在不进行任何修改的情况下做到这一点,只需使用常规的“锁定(对象)”语句吗?我可以想象你可以使用一些对象包装器来做到这一点,这些对象包装器在调试模式下会为你提供必要的信息。
  • @Evk,能够在不修改所有锁定代码的情况下做到这一点会很棒,是的。
  • 如果你可以访问用于锁定的对象(一些类似于ICollection.SyncRoot的属性),那么你可以使用Monitor.IsEntered方法。否则是不可能的。

标签: c# .net multithreading thread-safety clr


【解决方案1】:

这似乎是 AOP(面向方面​​编程)的一个不错的用例。 AOP 的一个非常基本的总结是,它是一种处理横切关注点以使代码干燥和模块化的方法。这个想法是,如果您对对象的每个方法调用都做一些事情(例如记录每个调用)而不是在每个方法的开头和结尾添加日志,那么您可以继承该对象并在类之外执行此操作以免混淆其目的。

这可以通过几种方式完成,我将举两个例子。首先是手动(这不是很好,但对于小案例可以很容易地完成)。

假设你有一个类,Doer,有两个方法 Do 和 Other。您可以从中继承并制作

public class Doer
{
    public virtual void Do()
    {
        //do stuff.
    }

    public virtual void Other()
    {
        //do stuff.
    }
}

public class AspectDoer : Doer
{
    public override void Do()
    {
        LogCall("Do");
        base.Do();
    }

    public override void Other()
    {
        LogCall("Other");
        base.Other();
    }

    private void LogCall(string method)
    {
       //Record call 
    }
}

如果你只关心一个类,这很好,但如果你必须为许多类做这件事,很快就会变得不可行。对于这些情况,我建议使用 CastleProxy 库之类的东西。这是一个动态创建代理来包装您想要的任何类的库。结合 IOC,您可以轻松地将每项服务封装在应用程序中。

这是一个使用 CastleProxy 的快速示例,要点是使用 ProxyGenerator.GenerateProxy 并传入 IInterceptors 来处理方法调用:

    [Test]
    public void TestProxy()
    {
        var generator = new ProxyGenerator();
        var proxy = generator.CreateClassProxy<Doer>(new LogInterceptor());
        proxy.Do();
        Assert.True(_wasCalled);
    }

    private static bool _wasCalled = false;
    public class LogInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Log(invocation.Method.Name);
            invocation.Proceed();
        }

        private void Log(string name)
        {
            _wasCalled = true;
        }
    }

现在,记录部分。我不确定您是否真的需要无锁,短锁可能就足够了,但让我们继续认为您需要。

我不知道 C# 中有多少工具支持无锁操作,但我能看到的最简单的版本是使用 Interlocked 来增加在任何给定时间方法中有多少实例的计数器 If会看起来像这样:

        [Test]
    public void TestProxy()
    {
        var generator = new ProxyGenerator();
        var proxy = generator.CreateClassProxy<Doer>(new LogInterceptor());
        proxy.Do();
        Assert.AreEqual(1, _totalDoCount);
    }

    private static int _currentDoCount = 0;
    private static int _totalDoCount = 0;
    public class LogInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            if (invocation.Method.Name == "Do")
            {
                var result = Interlocked.Increment(ref _currentDoCount);
                Interlocked.Increment(ref _totalDoCount);
                if(result > 1) throw new Exception("thread safe violation");
            }


            invocation.Proceed();
            Interlocked.Decrement(ref _currentDoCount);
        }
    }

Interlocked 使用神奇的寄存器魔法来执行线程安全操作(我相信比较和交换,但我真的不知道)。如果您需要更多的上下文,而不仅仅是“它发生了”。您可以使用无锁的并发堆栈或并发队列(它们也使用互锁:https://msdn.microsoft.com/en-us/library/dd997305.aspx/)。不过,我会在它们上加上一个时间戳,因为我没有充分使用它们来知道它们是否承诺按照它们发生的顺序返回元素。

就像我上面所说的,您可能不需要无锁操作,但这应该。我不知道这是否适合您,因为我不知道您的确切问题,但它应该为您提供一些解决此问题的工具。

【讨论】:

  • 是的,Interlocked.Increment 使用原子比较和交换操作。
【解决方案2】:

您可以host the CLR yourself,并使用IHostSyncManager::CreateMonitorEvent 方法跟踪所占用的锁。然后,您需要将您自己的机制从您的主机公开到您的方法,称为“IsLockTaken()”。然后,您可以在实际代码中从您的方法中调用它。

我认为这是可能的,但这将需要大量的工作,而且几乎可以肯定会完全分散您要解决的问题,但无疑会很有趣!

这里有一篇关于死锁检测的有趣文章https://blogs.msdn.microsoft.com/sqlclr/2006/07/25/deadlock-detection-in-sql-clr/

【讨论】:

    猜你喜欢
    • 2020-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多