Несколько условий против нескольких блокировок

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

Условия параллелизма сложны и перечислены ниже:

  1. Центральный массив байтов должен быть защищен исключительно (т.е. один поток одновременно).
  2. Два метода доступа (foo и bar) должны работать одновременно (внутренняя блокировка, если они попытаются получить доступ к центральному массиву байтов).
  3. Вызовы любого метода (foo и bar) должны быть эксклюзивными (т. е. множественные вызовы foo из разных потоков приведут к блокировке одного потока).

В моей первоначальной реализации я решил реализовать две вложенные блокировки, как показано ниже:

ReentrantLock lockFoo = new ReentrantLock(true);
ReentrantLock lockCentral = new ReentrantLock(true);

Condition centralCondition = lockCentral.newCondition();

public void foo(){
    // thread-safe processing code here

    lockFoo.lock();        
    lockCentral.lock();

    try{
        // accessing code here

        try{
            // waits upon some condition for access
            while(someCondition){
                centralCondition.await();
            }
        }catch(InterruptedException ex){
            // handling code here
        }

        // more processing
    }finally{
        lockCentral.unlock();
        lockFoo.unlock();
    }
}

структура аналогична методу bar, просто с другим объектом блокировки lockBar. Кроме того, код для простоты сократил мои более сложные ожидания и сигналы с несколькими условиями до одного условия.

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

Вместо этого я попытался реорганизовать внешний замок (lockFoo и lockBar) как условие lockCentral, как показано ниже:

ReentrantLock lockCentral = new ReentrantLock(true);

Condition fooCondition = lockCentral.newCondition();
Condition centralCondition = lockCentral.newCondition();

boolean isInFoo = false;

public void foo(){
    // thread-safe processing code here

    lockCentral.lock();

    try{
        // implement method exclusiveness via fooCondition

        try{
            while(isInFoo){
                fooCondition.await();
            }

            isInFoo = true;
        }catch(InterruptedException ex){
            return;
        }

        // accessing code here

        try{
            // waits upon some condition for access
            while(someCondition){
                centralCondition.await();
            }
        }catch(InterruptedException ex){
            // handling code here
        }

        // more processing
    }finally{
        isInFoo = false;
        fooCondition.signal();

        lockCentral.unlock();
    }
}

после некоторого осмотра я не могу решить, лучше ли первое или второе (особенно с включением этого случайного логического значения). Идея упрощения кода, по-видимому, привела к созданию более длинного кода, что в данном случае очень нелогично.

Есть ли какое-то соглашение или веская причина, чтобы аргументировать:

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

  2. Использование одной блокировки для каждого ресурса блокировки (последний код, где защищаемая центральная структура использует одну блокировку, а все остальное реализовано как условия для доступа к указанной структуре).


person initramfs    schedule 28.04.2015    source источник


Ответы (2)


Последний код отличается от предыдущего только ручной реализацией блокировки lockFoo с fooCondition (то же верно и для части, связанной с bar).

Поскольку такая реализация блокировки учитывает, что критическая секция foo почти такая же, как центральная, она гарантированно будет быстрее в случае, когда нет конкуренции на foo()< /strong> (в этом случае ожидание fooCondition никогда не выполняется).

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

person Tsyvarev    schedule 28.04.2015
comment
Хм... Я понимаю вашу точку зрения. Я не думал о приросте производительности от последнего, может ли попытка блокировки ReentrantLock привести к значительному снижению производительности (в споре)? Особенно с накладными расходами на доступ к глобальному lockCentral (поскольку fooCondition должен каким-то образом распознавать центральный мьютекс). Тесты не дают мне убедительных результатов на моем компьютере, и это действительно очень важный для производительности сегмент кода. - person initramfs; 28.04.2015

В исходном примере блокировки lockFoo и lockBar полностью избыточны, поскольку ни foo(), ни bar() не могут выполнять никакой работы без блокировки блокировки lockCentral. Если вы не измените дизайн программы, lockCentral будет единственным замком, который вам нужен.


Вы сказали, что считаете первый пример "слишком сложным", но второй пример намного сложнее. Похоже, вы просто пытаетесь заменить lockFoo и lockBar кодом блокировки собственной разработки. Но какой в ​​этом смысл? Он не будет делать ничего отличного от того, что делает ваш первый пример.


Какова цель блокировки вообще? Вы сказали: «Вызовы любого метода (foo и bar) должны быть эксклюзивными». Это начало не с той ноги: не используйте блокировки для защиты методов; Используйте блокировки для защиты данных.

Что это за «центральный массив байтов»? Что с ним делают нити? Почему его нужно защищать?

С какими данными работает foo()? Почему его нужно защищать? С какими данными работает bar()? Почему это нужно защищать?

Самое главное, должны ли данные foo() и данные bar() защищаться в то же время, что и центральный массив байтов?

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

SomeObject someObject;
SomeOtherObject someOtherObject;
boolean success = false;
while (! success) {

    someLock.lock();
    try {
        someObject = getLocalCopyOfSomeData();
        someOtherObject = getLocalCopyOfSomeOtherData();
    } finally {
        someLock.unlock();
    }

    doTheRealWork(someObject, someOtherObject);

    someLock.lock();
    try {
        success = updateCentralCopyOf(someObject) || updateCentralCopyOf(someOtherObject);
    } finally {
        someLock.unlock();
    }
}
person Solomon Slow    schedule 28.04.2015
comment
Во-первых, я не говорил, что foo и bar должны быть эксклюзивными, я сказал, что они должны работать одновременно. Возможно, я слишком упростил свою ситуацию. Я могу заверить вас, что код не может выжить на одном lockCentral, так как это приведет к повреждению данных. Обратите внимание, как lockCentral должен быть освобожден и повторно получен во время выполнения метода, когда какое-то условие вызывает await() (в моей реальной реализации это должно делать это несколько раз, поэтапно). - person initramfs; 28.04.2015
comment
Возьмем следующий пример: если поток A входит в метод foo, содержит lockCentral, изменяет некоторые переменные, а затем освобождает lockCentral как часть условия. Оппортунистический поток B может затем удерживать lockCentral, входить в метод foo и изменять переменные недопустимым образом. lockFoo есть/было там, чтобы остановить любой другой поток, входящий в метод foo, пока поток A не сможет завершиться (т.е. будет сигнализирован другим потоком из другого метода [например, bar]). - person initramfs; 28.04.2015
comment
@CPUTerminator Мои комментарии были основаны в основном на вашем первом примере. Код в вашем первом примере не позволит одному потоку выполнять работу в foo(), в то время как другой поток работает в bar(), потому что ни один из этих методов не работает без удержания центрального замка. Я признаю, что раньше не замечал, что может один поток выполнять wait() в foo(), в то время как другой поток работает в bar(). Но я все еще задаюсь вопросом, какой смысл допускать один поток в foo() и один в bar(), если они не могут оба работать одновременно? - person Solomon Slow; 28.04.2015
comment
@CPUTerminator, я все еще думаю, что вам следует изменить свою точку зрения: перестать говорить о блокировке методов и вместо этого говорить о блокировке данных. Методы — это всего лишь средства для работы с данными. Если вы больше думаете о данных — какие состояния могут быть в то время, когда поток A работает с ними, какие состояния вы хотите, чтобы поток B мог видеть — вы получите лучший дизайн. - person Solomon Slow; 28.04.2015
comment
Один метод предназначен для потребителей данных, а другой — для производителей данных. В этом классе много таких пар методов, я упростил до одной пары foo и bar, чтобы избежать лишней ненужной детализации. Центральный замок предназначен для защиты данных, которые производители делают для потребителей (после обработки). Этот класс можно считать очень похожим на циклический буфер (или пару PipedInputStream и PipedOutputStream в стандартной библиотеке Java) или, возможно, на промежуточную цепочку в видеодекодере. - person initramfs; 28.04.2015
comment
Я прекрасно понимаю, для чего нужны замки. Вот почему я решил изменить свою технику блокировки на один ReentrantLock при нескольких условиях (мой последний код), хотя он оказался более сложным. Блокировки предназначены для защиты данных, не методов. Тем не менее, большая часть метода должна быть заключена в блокировку, чтобы предотвратить чередующийся доступ к методу несколькими потоками. Моя формулировка центрального замка может быть неуместной, я думаю. Я имею в виду первичную большую структуру данных с множеством вспомогательных примитивов (целые, длинные, двойные) для поддержания состояния. - person initramfs; 28.04.2015