C #: Можете ли вы определить, находится ли текущий контекст выполнения в `lock (this)`? - PullRequest
4 голосов
/ 06 апреля 2010

Если у меня есть объект, к которому я хотел бы получить доступ из замка, например:

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

Но это позволило бы любому другому потоку вызывать функции Add / Remove без предварительного вызова Enter, что позволяет обойти защиту.

<Ч />

Редактировать 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. Я бы хотел изменить функции SetDate и SetName на Свойства, для наглядности.

Ответы [ 7 ]

4 голосов
/ 06 апреля 2010

Я не думаю, что это возможно без самостоятельного отслеживания состояния (например, с помощью какого-то семафора). Но даже если бы это было так, это было бы грубым нарушением инкапсуляции. Ваши методы обычно не должны заботиться о том, выполняются ли они в определенном контексте блокировки.

2 голосов
/ 06 апреля 2010

Позволяет заново создать класс, чтобы он действительно работал как транзакция.

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

Изменения не будут распространяться, пока не будет вызван коммит. В коммите вы можете взять блокировку и установить свойства целевого объекта.

2 голосов
/ 06 апреля 2010

Нет документированного метода проверки такого рода условий во время выполнения, и если бы он был, я бы с подозрением относился к любому коду, который его использовал, потому что любой код, который изменяет свое поведение на основе стека вызовов, был бы очень сложным отладить.

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

Этот код не предназначен для подготовки к работе, но он должен дать вам базовое представление о том, что нужно сделать, чтобы (а) предотвратить вызовы между потоками и (б) позволить любому потоку принять владение объектом до тех пор, пока ничто иное не использует его.

1 голос
/ 06 апреля 2010

Если ваше требование заключается в том, что блокировка должна быть получена на время любого метода AddOne () или RemoveOne (), то почему бы просто не получить блокировку внутри каждого метода? Это не должно быть проблемой, если абонент уже получил блокировку для вас.

Однако, если вы требуете, чтобы блокировка была получена до одновременного вызова AddOne () и RemoveOne () (поскольку другие параллельные операции, выполняемые над экземпляром, потенциально небезопасны), возможно, вам следует подумать об изменении открытого интерфейса, блокировка может быть обработана внутри, не касаясь кода клиента с деталями.

Один из возможных способов сделать это позже - предоставить методы Begin- и End-Changes, которые должны вызываться до и после AddOne и RemoveOne. Исключение следует вызывать, если AddOne или RemoveOne вызывается вне области Begin-End.

1 голос
/ 06 апреля 2010

используя локальное хранилище потока, вы можете хранить вход и выход из замка.

1 голос
/ 06 апреля 2010

Вы можете переопределить AddOne и RemoveOne, чтобы получить логический флаг со значением true, если он вызывается из блокировки. Я не вижу другого пути.

Вы также можете поиграть с ExecutionContext class, если хотите узнать что-то о текущем контексте выполнения. Вы можете получить текущий контекст, вызвав ExecutionContext.Capture().

0 голосов
/ 06 апреля 2010

Я столкнулся с этой же проблемой и создал вспомогательный класс, который выглядит следующим образом:

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.

...