C#:你能检测出当前的执行上下文是否在`lock(this)`之内?

时间:2010-04-06 00:44:56

标签: c# .net synchronization multithreading locking

如果我有一个对象,我想强行从锁中访问,如下所示:

var obj = new MyObject();

lock (obj)
{
    obj.Date = DateTime.Now;
    obj.Name = "My Name";
}

是否可以在AddOneRemoveOne函数中检测当前执行上下文是否在锁定内?

类似的东西:

Monitor.AreWeCurrentlyEnteredInto(this)

修改(以澄清意图)

这里的目的是能够拒绝在锁之外进行的任何修改,以便对象本身的所有更改都是事务性和线程安全的。锁定对象本身内的互斥锁并不能确保编辑具有事务性质。


我知道可以这样做:

var obj = new MyObject();

obj.MonitorEnterThis();

try
{
    obj.Date = DateTime.Now;
    obj.Name = "My Name";
}
finally
{
    obj.MonitorExitThis();
}

但这样就可以让任何其他线程在没有先调用Enter的情况下调用Add / Remove函数,从而绕过保护。


编辑2:

以下是我目前正在做的事情:

var obj = new MyObject();

using (var mylock = obj.Lock())
{
    obj.SetDate(DateTime.Now, mylock);
    obj.SetName("New Name", mylock);
}

这很简单,但它有两个问题:

  1. 我正在实施IDisposable mylock对象,这是一点点 滥用IDisposable 接口

  2. 我想将SetDateSetName函数更改为 属性,为清楚起见。

7 个答案:

答案 0 :(得分:4)

如果没有自己跟踪状态(例如使用某种信号量),我认为这是不可能的。但即便如此,这也是对封装的严重违反。您的方法通常不应该关心它们是否在特定的锁定环境中执行。

答案 1 :(得分:2)

在运行时没有记录的检查这种情况的方法,如果有的话,我会怀疑任何使用它的代码,因为任何改变基于调用堆栈的行为的代码都会非常困难调试。

真正的ACID语义实现并不容易,我个人不会尝试;这就是我们拥有的数据库,如果您需要快速/可移植的代码,您可以使用内存数据库。如果你只是想要强制单线程语义,这是一个更容易驯服的野兽,虽然作为一个免责声明我应该提到,从长远来看,你最好只是提供原子操作,而不是试图阻止多线程访问。

让我们假设您有充分的理由想要这样做。这是您可以使用的概念验证课程:

public interface ILock : IDisposable
{
}

public class ThreadGuard
{
    private static readonly object SlotMarker = new Object();

    [ThreadStatic]
    private static Dictionary<Guid, object> locks;

    private Guid lockID;
    private object sync = new Object();

    public void BeginGuardedOperation()
    {
        lock (sync)
        {
            if (lockID == Guid.Empty)
                throw new InvalidOperationException("Guarded operation " +
                    "was blocked because no lock has been obtained.");
            object currentLock;
            Locks.TryGetValue(lockID, out currentLock);
            if (currentLock != SlotMarker)
            {
                throw new InvalidOperationException("Guarded operation " +
                    "was blocked because the lock was obtained on a " +
                    "different thread from the calling thread.");
            }
        }
    }

    public ILock GetLock()
    {
        lock (sync)
        {
            if (lockID != Guid.Empty)
                throw new InvalidOperationException("This instance is " +
                    "already locked.");
            lockID = Guid.NewGuid();
            Locks.Add(lockID, SlotMarker);
            return new ThreadGuardLock(this);
        }
    }

    private void ReleaseLock()
    {
        lock (sync)
        {
            if (lockID == Guid.Empty)
                throw new InvalidOperationException("This instance cannot " +
                    "be unlocked because no lock currently exists.");
            object currentLock;
            Locks.TryGetValue(lockID, out currentLock);
            if (currentLock == SlotMarker)
            {
                Locks.Remove(lockID);
                lockID = Guid.Empty;
            }
            else
                throw new InvalidOperationException("Unlock must be invoked " +
                    "from same thread that invoked Lock.");
        }
    }

    public bool IsLocked
    {
        get
        {
            lock (sync)
            {
                return (lockID != Guid.Empty);
            }
        }
    }

    protected static Dictionary<Guid, object> Locks
    {
        get
        {
            if (locks == null)
                locks = new Dictionary<Guid, object>();
            return locks;
        }
    }

    #region Lock Implementation

    class ThreadGuardLock : ILock
    {
        private ThreadGuard guard;

        public ThreadGuardLock(ThreadGuard guard)
        {
            this.guard = guard;
        }

        public void Dispose()
        {
            guard.ReleaseLock();
        }
    }

    #endregion
}

这里有很多事情,但我会为你分解:

  • 当前锁(每个线程)保存在[ThreadStatic]字段中,该字段提供类型安全的线程本地存储。该字段在ThreadGuard的实例之间共享,但每个实例都使用自己的密钥(Guid)。

  • 两个主要操作是GetLock,它验证没有锁定,然后添加自己的锁,ReleaseLock验证锁存在当前线程(因为记住,locksThreadStatic)并在满足条件时将其删除,否则会引发异常。

  • 最后一个操作BeginGuardedOperation旨在由拥有ThreadGuard个实例的类使用。它基本上是一个排序断言,它验证当前执行的线程拥有分配给此ThreadGuard的锁,并在不满足条件时抛出。

  • 还有一个ILock接口(除了从IDisposable派生之外什么都不做),还有一个一次性内部ThreadGuardLock来实现它,它包含一个引用处理后创建它的ThreadGuard并调用其ReleaseLock方法。请注意,ReleaseLock是私有的,因此ThreadGuardLock.Dispose是对发布功能的唯一公共访问权限,这很好 - 我们只需要一个单一的入口点用于获取和发布

要使用ThreadGuard,您可以将其包含在另一个类中:

public class MyGuardedClass
{
    private int id;
    private string name;
    private ThreadGuard guard = new ThreadGuard();

    public MyGuardedClass()
    {
    }

    public ILock Lock()
    {
        return guard.GetLock();
    }

    public override string ToString()
    {
        return string.Format("[ID: {0}, Name: {1}]", id, name);
    }

    public int ID
    {
        get { return id; }
        set
        {
            guard.BeginGuardedOperation();
            id = value;
        }
    }

    public string Name
    {
        get { return name; }
        set
        {
            guard.BeginGuardedOperation();
            name = value;
        }
    }
}

所有这一切都是使用BeginGuardedOperation方法作为断言,如前所述。请注意,我并没有尝试保护读写冲突,只是多次写入冲突。如果你想要读写器同步,那么你需要要求相同的锁读取(可能不太好),在MyGuardedClass中使用额外的锁(最直接的解决方案)或改变{{1}使用ThreadGuard类公开并获取真正的“锁定”(小心)。

这是一个可以玩的测试程序:

Monitor

正如代码(希望)暗示的那样,只有class Program { static void Main(string[] args) { MyGuardedClass c = new MyGuardedClass(); RunTest(c, TestNoLock); RunTest(c, TestWithLock); RunTest(c, TestWithDisposedLock); RunTest(c, TestWithCrossThreading); Console.ReadLine(); } static void RunTest(MyGuardedClass c, Action<MyGuardedClass> testAction) { try { testAction(c); Console.WriteLine("SUCCESS: Result = {0}", c); } catch (Exception ex) { Console.WriteLine("FAIL: {0}", ex.Message); } } static void TestNoLock(MyGuardedClass c) { c.ID = 1; c.Name = "Test1"; } static void TestWithLock(MyGuardedClass c) { using (c.Lock()) { c.ID = 2; c.Name = "Test2"; } } static void TestWithDisposedLock(MyGuardedClass c) { using (c.Lock()) { c.ID = 3; } c.Name = "Test3"; } static void TestWithCrossThreading(MyGuardedClass c) { using (c.Lock()) { c.ID = 4; c.Name = "Test4"; ThreadPool.QueueUserWorkItem(s => RunTest(c, cc => cc.ID = 5)); Thread.Sleep(2000); } } } 方法才能完全成功。 TestWithLock方法部分成功 - 工作线程失败,但主线程没有问题(这也是此处所需的行为)。

这不是生产就绪的代码,但它应该为您提供基本的想法,以便(a)防止跨线程调用和(b)允许任何线程采取必须执行的操作只要没有其他东西正在使用它,对象的所有权。

答案 2 :(得分:2)

让我们重新设计你的课程,使其真正像交易一样工作。

using (var transaction = account.BeginTransaction())
{
       transaction.Name = "blah";
       transaction.Date = DateTime.Now;
       transaction.Comit();
}

在调用commit之前,不会传播更改。 在提交中,您可以锁定并在目标对象上设置属性。

答案 3 :(得分:1)

您可以覆盖AddOneRemoveOne以获取布尔标志,如果从锁定中调用该标志,则设置为true。我没有看到任何其他方式。

如果您想了解当前的执行上下文,也可以使用ExecutionContext class。您可以通过调用ExecutionContext.Capture()来获取当前上下文。

答案 4 :(得分:1)

使用线程本地存储,您可以存储锁的进入和退出。

答案 5 :(得分:1)

如果您的要求是必须在AddOne()或RemoveOne()方法的持续时间内获取锁,那么为什么不简单地获取每个方法中的锁?如果呼叫者已经为您获得了锁定,那应该不是问题。

但是,如果您的要求是必须在一起调用AddOne()和RemoveOne()之前获取锁(因为在实例上执行的其他并发操作可能不安全),那么您可能应该考虑更改公共接口以便可以在内部处理锁定,而无需考虑具有详细信息的客户端代码。

完成后者的一种可能方法是提供必须在AddOne和RemoveOne之前和之后调用的Begin和End-Changes方法。如果在Begin-End范围之外调用AddOne或RemoveOne,则应引发异常。

答案 6 :(得分:0)

我遇到了同样的问题并创建了一个如下所示的辅助类:

public class BusyLock : IDisposable
{
    private readonly Object _lockObject = new Object();
    private int _lockCount;

    public bool IsBusy
    {
        get { return _lockCount > 0; }
    }

    public IDisposable Enter()
    {
        if (!Monitor.TryEnter(_lockObject, TimeSpan.FromSeconds(1.0)))
            throw new InvalidOperationException("Cannot begin operation as system is already busy");

        Interlocked.Increment(ref _lockCount);
        return this;
    }

    public bool TryEnter(out IDisposable busyLock)
    {
        if (Monitor.TryEnter(_lockObject))
        {
            busyLock = this;
            Interlocked.Increment(ref _lockCount);
            return true;
        }

        busyLock = null;
        return false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        if (_lockCount > 0)
        {
            Monitor.Exit(_lockObject);
            Interlocked.Decrement(ref _lockCount);
        }
    }

    #endregion
}

然后,您可以创建一个包含这样的实例:

public sealed class AutomationManager
{
    private readonly BusyLock _automationLock = new BusyLock();

    public IDisposable AutomationLock
    {
        get { return _automationLock.Enter(); }
    }

    public bool IsBusy
    {
        get { return _automationLock.IsBusy; }
    }
}

并像这样使用它:

    public void DoSomething()
    {
        using (AutomationLock)
        {
            //Do important busy stuff here
        }
    }

对于我的特殊情况,我只想要一个强制执行锁(如果两个线程表现良好,它们不应该同时尝试获取锁),所以我抛出异常。您可以轻松修改它以执行更典型的锁定,并仍然可以利用IsBusy。