如果我有一个对象,我想强行从锁中访问,如下所示:
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
的情况下调用Add / Remove函数,从而绕过保护。
编辑2:
以下是我目前正在做的事情:
var obj = new MyObject();
using (var mylock = obj.Lock())
{
obj.SetDate(DateTime.Now, mylock);
obj.SetName("New Name", mylock);
}
这很简单,但它有两个问题:
我正在实施IDisposable mylock对象,这是一点点 滥用IDisposable 接口
我想将SetDate
和SetName
函数更改为
属性,为清楚起见。
答案 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
验证锁存在当前线程(因为记住,locks
是ThreadStatic
)并在满足条件时将其删除,否则会引发异常。
最后一个操作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)
您可以覆盖AddOne
和RemoveOne
以获取布尔标志,如果从锁定中调用该标志,则设置为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。