Подтвердить что ты не робот

С# ручная блокировка/разблокировка

У меня есть функция в С#, которая может быть вызвана несколько раз из нескольких потоков, и я хочу, чтобы это было сделано только один раз, поэтому я подумал об этом:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        lock(this)
            if(!done)
            {
                done = true;
                _DoSomething();
            }
    }
}

Проблема _DoSomething занимает много времени, и я не хочу, чтобы многие потоки ожили, когда они могут просто увидеть, что done является истинным.
Что-то вроде этого может быть обходным путем:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        bool doIt = false;
        lock(this)
            if(!done)
                doIt = done = true;
        if(doIt)
             _DoSomething();
    }
}

Но просто делать блокировку и разблокировку вручную будет намного лучше.
Как я могу вручную заблокировать и разблокировать, как это делает lock(object)? Мне нужно использовать тот же интерфейс, что и lock, чтобы этот ручной способ и lock блокировали друг друга (для более сложных случаев).

4b9b3361

Ответ 1

Ключевое слово lock - это просто синтаксический сахар для Monitor.Enter и Monitor.Exit:

Monitor.Enter(o);
try
{
    //put your code here
}
finally
{
    Monitor.Exit(o);
}

совпадает с

lock(o)
{
    //put your code here
}

Ответ 2

Томас предлагает двойной контроль в своем ответе. Это проблематично. Во-первых, вам не следует использовать методы с низким уровнем блокировки, если вы не продемонстрировали, что у вас есть реальная проблема с производительностью, которая решается методом низкой блокировки. Методы с низким уровнем блокировки безумно трудны для правильного выбора.

Во-вторых, это проблематично, потому что мы не знаем, что делает "_DoSomething" или какие последствия его действий мы будем полагаться.

В-третьих, как я уже указывал в комментарии выше, кажется безумным вернуться к тому, что _DoSomething "сделано" , когда другой поток на самом деле все еще находится в процессе его выполнения. Я не понимаю, почему у вас есть это требование, и я собираюсь предположить, что это ошибка. Проблемы с этим шаблоном все еще существуют, даже если мы устанавливаем "done" после того, как "_DoSomething" делает свою вещь.

Рассмотрим следующее:

class MyClass 
{
     readonly object locker = new object();
     bool done = false;     
     public void DoSomething()     
     {         
        if (!done)
        {
            lock(locker)
            {
                if(!done)
                {
                    ReallyDoSomething();
                    done = true;
                }
            }
        }
    }

    int x;
    void ReallyDoSomething()
    {
        x = 123;
    }

    void DoIt()
    {
        DoSomething();
        int y = x;
        Debug.Assert(y == 123); // Can this fire?
    }

Является ли это потокобезопасным во всех возможных реализациях С#? Я не думаю, что это так. Помните, что энергонезависимые чтения могут перемещаться во времени кэшем процессора. Язык С# гарантирует, что волатильные чтения последовательно упорядочиваются в отношении критических точек выполнения, таких как блокировки, и гарантирует, что неизменяемые чтения согласуются в одном потоке выполнения, но это не гарантирует, что неизменяемые чтения будут согласованы в любых путь через потоки выполнения.

Посмотрим на пример.

Предположим, что есть два потока: Alpha и Bravo. Оба называют DoIt в новом экземпляре MyClass. Что происходит?

В потоке Bravo кэш процессора используется для выполнения (энергонезависимой!) выборки памяти для x, которая содержит нуль. "done" происходит на другой странице памяти, которая еще не загружена в кеш.

В потоке Alpha в то же время на другом процессоре DoI вызывает DoSomething. Thread Alpha теперь запускает все. Когда поток Alpha выполняется, работа выполнена верно, а x - 123 на процессоре Alpha. Thread Alpha-процессор очищает эти факты от основной памяти.

В настоящее время программа dragvo запускает DoSomething. Он считывает страницу основной памяти, содержащую "сделанную" в кэш процессора, и видит, что это правда.

Итак, теперь "сделано" верно, но "x" по-прежнему равен нулю в кэше процессора для потока Bravo. Thread Bravo не требуется для того, чтобы аннулировать часть кеша, содержащего "x", равную нулю, потому что в потоке Bravo ни чтение "сделано" , ни "x" не было изменчивым.

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

Как правильно сделать эту версию шаблона, нужно сделать, по крайней мере, первое чтение "сделано" в изменчивом чтении. Тогда чтение "x" не будет разрешено перемещать "впереди" волатильного чтения в "сделано" .

Ответ 3

Вы можете проверить значение done до и после блокировки:

    if (!done)
    {
        lock(this)
        {
            if(!done)
            {
                done = true;
                _DoSomething();
            }
        }
    }

Таким образом, вы не войдете в блокировку, если done - true. Вторая проверка внутри замка заключается в том, чтобы справиться с условиями гонки, если два потока одновременно входят в первый if.

BTW, вы не должны блокировать this, потому что это может вызвать взаимоблокировки. Вместо этого заблокируйте личное поле (например, private readonly object _syncLock = new object())

Ответ 4

Ключевое слово lock - это просто синтаксический сахар для класса Monitor. Также вы можете вызвать Monitor.Enter(), Monitor.Exit().

Но сам класс Monitor имеет также функции TryEnter() и Wait(), которые могут помочь в вашей ситуации.

Ответ 5

Я знаю, что этот ответ наступает несколько лет назад, но ни один из текущих ответов, похоже, не затрагивает ваш фактический сценарий, который стал очевидным только после comment:

В других потоках не нужно использовать какую-либо информацию, созданную ReallyDoSomething.

Если другим потокам не нужно ждать завершения операции, второй фрагмент кода в вашем вопросе будет работать нормально. Вы можете оптимизировать его дальше, полностью устранив блокировку и используя атомную операцию:

private int done = 0;
public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)   // only evaluates to true ONCE
        _DoSomething();
}

Кроме того, если ваш _DoSomething() является операцией "огонь и забухание", вам может не понадобиться даже первый поток, чтобы дождаться его, позволяя ему асинхронно запускать задачу в пуле потоков:

int done = 0;

public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)
        Task.Factory.StartNew(_DoSomething);
}