Явная параллельная видимость записи примитивных массивов - программирование
Подтвердить что ты не робот

Явная параллельная видимость записи примитивных массивов

Недавно я нашел этот камень в моей базе кода:

/** This class is used to "publish" changes to a non-volatile variable.
 *
 * Access to non-volatile and volatile variables cannot be reordered,
 * so if you make changes to a non-volatile variable before calling publish,
 * they are guaranteed to be visible to a thread which calls syncChanges
 *
 */
private static class Publisher {
    //This variable may not look like it doing anything, but it really is.
    //See the documentaion for this class.
    private volatile AtomicInteger sync = new AtomicInteger(0);

    void publish() {
        sync.incrementAndGet();
    }

    /**
     *
     * @return the return value of this function has no meaning.
     * You should not make *any* assumptions about it.
     */
    int syncChanges() {
        return sync.get();
    }
}

Это используется как таковое:

Тема 1

float[][] matrix;
matrix[x][y] = n;
publisher.publish();

Тема 2

publisher.syncChanges();
myVar = matrix[x][y];

Тема 1 - поток обновления фона, который работает непрерывно. Тема 2 - рабочий поток HTTP, который не заботится о том, чтобы то, что он читает, каким-либо образом последовательным или атомарным, только то, что записи "в конечном итоге" попадают туда и не теряются в качестве предложений богам concurrency.

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

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

Согласно документации java.util.concurrent, создайте следующие отношения happens-before:

Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое приходит позже в порядке программы.

Записывается в нестабильное поле - перед каждым последующим чтением этого же поля. Записи и чтения изменчивых полей имеют аналогичные эффекты согласованности памяти как входные и выходящие мониторы, но не влекут за собой блокировку взаимного исключения.

Итак, это звучит так:

  • выполняется матрица write-before publish() (правило 1)
  • publish() происходит до syncChanges() (правило 2)
  • syncChanges() происходит до чтения матрицы (правило 1)

Таким образом, код действительно установил цепочку событий для этой матрицы.

Но я не уверен. concurrency сложно, и я не эксперт в области. Что я пропустил? Действительно ли это безопасно?

4b9b3361

Ответ 1

С точки зрения видимости, все, что вам нужно, это volatile write-read, в любом изменчивом поле. Это будет работать

final    float[][] matrix  = ...;
volatile float[][] matrixV = matrix;

Тема 1

matrix[x][y] = n;
matrixV = matrix; // volatile write

Тема 2

float[][] m = matrixV;  // volatile read
myVar = m[x][y];

or simply
myVar = matrixV[x][y];

Но это полезно только для обновления одной переменной. Если потоки записи записывают несколько переменных, и прочитанный поток читает их, читатель может видеть несогласованную картину. Обычно он обрабатывается блокировкой чтения и записи. Copy-on-write может быть подходящим для некоторых шаблонов использования.

У Doug Lea есть новый "StampedLock" http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/StampedLock.html для Java8, который представляет собой версию блокировки чтения и записи, которая намного дешевле для чтения замки. Но это гораздо труднее использовать. В основном читатель получает текущую версию, затем переходите и читайте кучу переменных, а затем снова проверяйте версию; если версия не изменилась, во время сеанса чтения не было одновременных записей.

Ответ 2

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

Однако он содержит некоторую избыточность и может быть улучшен путем создания поля sync final. Доступ к этому полю volatile является первым из двух барьеров памяти; по контракту вызов incrementAndGet() оказывает такое же влияние на память, как на запись и чтение на изменчивой переменной, а вызов get() имеет тот же эффект, что и чтение.

Таким образом, код может опираться только на синхронизацию, предоставляемую этими методами, и сделать само поле final.

Ответ 3

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

В примере кода, если вы делаете несколько записей в matrix, а затем вызываете publish(), а другой поток вызывает synch(), а затем читает матрицу, тогда другой поток может видеть некоторые, все или ничего изменений:

  • Все изменения, если они читают обновленное значение из publish()
  • Ни одно из изменений, если оно читает старое опубликованное значение, и ни одно из изменений не просочилось через
  • Некоторые изменения, если они читают ранее опубликованное значение, но некоторые из изменений просочились через

См. в этой статье

Ответ 4

Вы правильно упомянули правило № 2 о происшествии перед отношениями

Записывается в нестабильное поле - перед каждым последующим чтением этого же поля.

Однако это не гарантирует, что publish() будет вызываться до syncChanges() на абсолютной временной шкале. Немного изменим ваш пример.

Тема 1:

matrix[0][0] = 42.0f;
Thread.sleep(1000*1000); // assume the thread was preempted here
publisher.publish(); //assume initial state of sync is 0 

Тема 2:

int a = publisher.syncChanges();
float b = matrix[0][0];

Какие существуют опции для переменных a и b?

  • a равно 0, b может быть 0 или 42
  • a - 1, b - 42 из-за отношения "бытие-до"
  • a больше 1 (по какой-то причине поток 2 был медленным, и Thread 1 повезло публиковать обновления несколько раз), значение b зависит от бизнес-логики и способа обработки матрицы - зависит ли она от предыдущего состояния или нет?

Как с этим бороться? Это зависит от бизнес-логики.

  • Если Thread 2 проверяет состояние матрицы время от времени, и отлично, чтобы иметь некоторые устаревшие значения между ними, если в конце будет обработано правильное значение, а затем оставьте его как есть.
  • Если Thread 2 не заботится о пропущенных обновлениях, но он всегда хочет следить за обновленной матрицей, тогда используйте коллекции copy-on-write или используйте ReaderWriteLock, как было упомянуто выше.
  • Если Thread 2 действительно заботится об отдельных обновлениях, тогда его следует обрабатывать более разумно, вы можете рассмотреть шаблон wait()/notify() и уведомлять Thread 2 всякий раз, когда обновляется матрица.