Нет документированного метода проверки такого рода условий во время выполнения, и если бы он был, я бы с подозрением относился к любому коду, который его использовал, потому что любой код, который изменяет свое поведение на основе стека вызовов, был бы очень сложным отладить.
Истинная семантика 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
(самое простое решение), либо изменить 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
полностью завершается успешно. Метод TestWithCrossThreading
частично завершается успешно - рабочий поток завершается с ошибкой, но основной поток не имеет проблем (что опять-таки является желаемым поведением).
Этот код не предназначен для подготовки к работе, но он должен дать вам базовое представление о том, что нужно сделать, чтобы (а) предотвратить вызовы между потоками и (б) позволить любому потоку принять владение объектом до тех пор, пока ничто иное не использует его.