代码之家  ›  专栏  ›  技术社区  ›  John Gietzen

C:您能检测当前执行上下文是否在“lock(this)”中吗?

  •  4
  • John Gietzen  · 技术社区  · 15 年前

    如果我有一个要强制从锁中访问的对象,例如:

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

    有可能吗,从内部 AddOne RemoveOne 用于检测当前执行上下文是否在锁中的函数?

    类似于:

    Monitor.AreWeCurrentlyEnteredInto(this)
    

    编辑: (用于澄清意图)

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


    我知道这样做是可能的:

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

    但这将允许任何 其他 在不首先调用 Enter 从而规避保护。


    编辑2:

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

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

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

    1. 我正在执行IDisposable mylock对象,有点 对我的虐待 接口。

    2. 我想换一个 SetDate SetName 函数到 属性,为了清楚起见。

    7 回复  |  直到 15 年前
        1
  •  2
  •   Aaronaught    15 年前

    没有在运行时检查这种情况的文档化方法,如果有,我会怀疑任何使用它的代码,因为任何基于调用堆栈改变其行为的代码都很难调试。

    真正的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 ,它验证锁是否存在 对于当前线程 (因为记住, locks ThreadStatic )如果满足该条件,则将其移除,否则将引发异常。

    • 最后一次手术, BeginGuardedOperation ,用于拥有 线程保护器 实例。它基本上是一个排序断言,它验证当前执行的线程是否拥有分配给它的锁 线程保护器 ,如果不满足条件则抛出。

    • 还有一个 ILock 接口(除了派生自 IDisposable )和一次性内件 ThreadGuardLock 实现它,它包含对 线程保护器 创造了它并称之为 释放栓 方法。注意 释放栓 是私人的,所以 ThreadGuardLock.Dispose 只有 公共访问发布功能,这是很好的-我们只需要一个单一的入口点获取和发布。

    使用 线程保护器 ,您可以将其包含在另一个类中:

    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;
            }
        }
    }
    

    所有这些只是使用 开始赤化 方法作为断言,如前所述。请注意,我并没有试图保护读写冲突,只是保护多个写冲突。如果您想要读写器同步,那么您需要使用相同的锁(可能不太好),在 MyGuardedClass (最直接的解决方案)或者改变 线程保护器 使用 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 方法完全成功。这个 TestWithCrossThreading 方法部分成功-工作线程失败,但主线程没有问题(这也是此处所需的行为)。

    这并不是为生产准备的代码,但它应该为您提供必须做什么的基本概念,以便(a)防止跨线程调用和(b)允许任何线程拥有对象,只要没有其他线程在使用它。

        2
  •  4
  •   John Feminella    15 年前

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

        3
  •  2
  •   Muhammad Hasan Khan    15 年前

    让我们重新设计您的类,使它像事务一样实际工作。

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

    在调用commit之前,不会传播更改。 在commit中,您可以获取一个锁并设置目标对象的属性。

        4
  •  1
  •   Kiril    15 年前

    您可以覆盖 AddOne RemoveOne 如果从锁调用布尔标志,则将其设置为true。我看不出别的办法。

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

        5
  •  1
  •   Keith Nicholas    15 年前

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

        6
  •  1
  •   Michael Petito    15 年前

    如果您的要求是在addone()或removeone()方法期间必须获取锁,那么为什么不简单地在每个方法中获取锁呢?如果打电话的人已经为你取得了锁,那就不成问题了。

    但是,如果您的要求是在同时调用addone()和removeone()之前必须获取锁(因为在实例上执行的其他并发操作可能不安全),那么您可能应该考虑更改公共接口,以便可以在内部处理锁。不涉及客户端代码的细节。

    实现后者的一种可能方法是为必须在addone和removeone之前和之后调用的开始和结束更改提供方法。如果在begin-end作用域之外调用addone或removeone,则应引发异常。

        7
  •  0
  •   Dan Bryant    15 年前

    我遇到了同样的问题,创建了一个helper类,如下所示:

    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。