2014-02-15 5 views
10

Хорошо, я знаю, это может показаться довольно глупым (и я боюсь, что это так), но я не вполне доволен ответом, который я дал себе, поэтому я подумал, что стоит того, чтобы его спрашивали здесь. Я имею дело с упражнением о параллельности (в Java), который идет как этотКогда требуется блокировка

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

Теперь мой вопрос: так как потоки должны выполнять «чтение», собирать информацию из диаграммы и разрабатывать их где-то в другом месте, не могли бы они работать, не беспокоясь о параллелизме? Состояние диаграммы всегда согласовано, так как не выполняется «запись», поэтому оно никогда не изменяется.

Не блокируются/синхронизированы блоки/синхронизированные методы, если и только если существует риск потери консистенции ресурсов? Другими словами, я правильно понял параллелизм?

+1

Исключает ли «фиксированное количество потоков» использование одного потока? –

+0

@JBNizet действительно – fge

+0

Совместное, изменяемое состояние должно быть защищено. – duffymo

ответ

9

Это довольно тонкий вопрос, а не глупость.

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

См. Раздел 3.5 Goetz, et. al., Java Concurrency In Practice, для дальнейшего обсуждения концепции безопасной публикации. Раздел 3.5.4 «Эффективно неизменяемые объекты» здесь применим здесь, поскольку совет становится фактически неизменным в определенный момент, потому что он никогда не записывается после того, как он достиг решенного состояния.

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

Существуют и другие способы координации видимости памяти, такие как запись/чтение в изменчивую переменную или AtomicReference. Использование конструкций параллелизма более высокого уровня, таких как защелки или барьеры, или отправка задач на ExecutorService, также обеспечит гарантии видимости памяти.

UPDATE

на основе обмена в комментариях с Donal Fellows, я хотел бы также отметить, что безопасной публикация требования также применяется при получении результатов назад от чтения потоков. То есть, как только один из потоков читателей будет иметь результат из его части вычисления, он должен опубликовать этот результат где-нибудь, чтобы он мог быть объединен с результатами других потоков читателей. Такие же методы могут быть использованы по-прежнему, например, блокировка/синхронизация по общей структуре данных, летучие вещества и т. Д. Однако это обычно не требуется, поскольку результаты могут быть получены из Future, возвращенных ExecutorService.submit или invoke. Эти конструкции автоматически обрабатывают безопасные требования к публикации, поэтому приложению не нужно иметь дело с синхронизацией.

+1

Однако различные потоки читателей также должны проголосовать за то, будет ли их фрагмент задачи способствовать общему успеху или неудаче проверки Sudoku. Что такое запись ... –

+0

Отчетность и слияние частичных результатов необязательно должны включать запись в разделяемое изменяемое состояние. Альтернативно, часто предпочтительнее, возвращать результаты в виде значения функции, например, из 'Callable'. Это доступно для задач координации потоков через «Будущее». См. «ExecutorService.invokeAll». Это связано с передачей значений между потоками, но никакой дополнительной блокировки не требуется. –

+0

Это действительно зависит от того, как реализована межпоточная передача сообщений, не так ли? Если вы собираетесь вызвать барьер памяти, идущий в одну сторону, вам понадобится что-то эквивалентное. (Имейте в виду, так как это действительно один бит - или более реалистично один байт или слово в зависимости от используемого метода - есть несколько довольно эффективных способов сделать это.) –

2

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

Если вы 100% уверены, что ни один поток не пишет, то это безопасно пропустить синхронизации и блокировку ...

EDIT: пропуск блокировка в тех случаях, тезисов является лучшей практикой! :)

+5

Собственно, это лучшая практика. Совместное использование неизменяемых данных среди потоков только для чтения - лучший способ не испортить проблемы синхронизации и обеспечивает лучшую производительность :-) –

+0

@JBNizet Спасибо, что указали это. Я редактирую последнюю строку. – Merlevede

+1

Но не забудьте, что сказал Стюарт Маркс в первом ответе: «Неизбежный объект (ы) должен быть« безопасно опубликован ». Это означает, что ни одна другая нить не может видеть неизменяемые объекты данных до тех пор, пока они (или все они) не будут полностью построены. Существуют тонкие способы (описанные в _Java Concurrency In Practice_), что многопоточная программа, работающая на многопроцессорной машине, может позволить одному потоку видеть объект, какой он был до того, как какой-либо другой поток завершил его построение. –

1

Нет необходимости в синхронизации файла, если он читается-only.Basically замок применяется к критической секции .Critical разделе, где различные потоки, обращающиеся к разделяемой памяти одновременно. Поскольку синхронизация делает программу медленной, так как при одновременном доступе нескольких потоков лучше не использовать блокировку в случае файлов только для чтения.

-2

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

+0

Даже если операция записи занимает два тактовых цикла или 1 наносекунду, при записи требуется блокировка. – Merlevede

+0

В теории это правильно. Дело в том, что такие операции записи выполняются в 99% всех случаев для носителей данных в памяти, таких как «CHM», и синхронизация записи предоставляется из коробки, поэтому вам не придется заботиться об этом самостоятельно. Для всех остальных случаев вы должны делать блокировку вручную, и я думаю, что этот стартер означал именно это. – injecteer

+0

@ injecteer: это просто неправильно. И ваше правило неприменимо в любом случае: «долгое время» не означает ничего конкретного. Я предлагаю вам прочитать Concurreny на практике, Брайан Гетц. –

1

Представьте, что у вас есть куча работы для завершения (проверьте 9 строк, 9 столбцов, 9 блоков). Если вы хотите, чтобы потоки завершили эту группу из 27 единиц работы, и если вы хотите завершить работу без двойной работы, то потоки должны быть синхронизированы. Если, с другой стороны, вы счастливы иметь потоки, которые могут выполнять рабочий блок, который был выполнен другим потоком, тогда вам не нужно синхронизировать потоки.

+0

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

+0

Действительно. Возможно, существует алгоритм monte-carlo с ненулевой вероятностью дублирования работы _some_, но в среднем, в среднем, с достаточным количеством процессоров, за меньшее время, чем другой алгоритм, гарантирующий никогда не дублировать работу. –

+0

@voo В этом простом примере проверки правильности платы sudoku да, можно статически распределять работу между различными потоками, и не требуется блокировка. Но в целом, если каждый рабочий блок занимает различное количество времени для завершения, то не оптимально разделить работу между потоками статически. Некоторые потоки будут перегружены, а некоторым потокам может не хватить работы. jameslarge, как вы можете сделать вывод, что дублирующая работа заканчивается в среднем за меньшее время, чем алгоритм, который никогда не дублирует работу?В общем, я бы сказал, что дублирующая работа - БОЛЬШОЙ НЕТ. – anonymous

0

Сценарий, в котором Thread1 записывает некоторые данные, а затем куча потоков, необходимая для чтения этих данных, не требует блокировки, если выполнена правильно. Должным образом я имею в виду, что ваша плата SUDOKU непреложного объект, и неизменным объектом я имею в виду:

  • государство не может быть изменен после строительства
  • государства фактически не изменяется с помощью некоторого отражения темной магии
  • Все поля являются окончательными
  • «эта» ссылка не исчезает во время строительства (это может произойти, если во время строительства вы делаете что-то вдоль линий MyClass.instnce = this).

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

Вот очень хорошая статья о immutable objects.

0

Аннотация

Для нити должно быть гарантировано, чтобы наблюдать эффекты записи в основную память, запись должна произойти, прежде, чем чтение. Если запись и чтение происходят в разных потоках, для этого требуется синхронизация. Спецификация определяет множество различных действий синхронизации. Одним из таких действий является выполнение инструкции synchronized, но существуют альтернативы.

Детали

Язык Java Спецификация writes:

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

и

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

В вашем случае вы хотите, чтобы потоки чтения решали правильную судоку. То есть инициализация объекта sudoku должна быть видимой для потоков чтения, и поэтому инициализация должна произойти до потоков чтения, считанных с судоку.

Спецификация определяет происходит, перед тем следующим образом:

Если у нас есть два действия х и у, мы пишем hb(x, y), чтобы указать, что xпроисходит, перед темy.

  • Если x и y являются действия той же нити и x идет перед y в программном порядке, то hb(x, y).

  • Для этого объекта существует конец до конца от конструктора объекта до начала финализатора (§12.6).

  • Если действие x синхронизируется со следующим действием y, то мы также имеем hb (x, y).

  • Если hb (x, y) и hb (y, z), то hb (x, z).

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

  • Действие отпирания на мониторе м синхронизирует-все последующие действия блокировки на т (где «последующее» определяется в соответствии с порядком синхронизации).

  • Запись в изменчивую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где «последующий» определяется в соответствии с порядком синхронизации).

  • Действие, которое запускает нить, синхронизируется с первым действием в потоке, которое оно запускает.

  • Запись значений по умолчанию (ноль, ложь или нуль) для каждой переменной синхронизируется с первым действием в каждом потоке. (Хотя может показаться немного странным писать значение по умолчанию для переменной до выделения объекта, содержащего эту переменную, концептуально каждый объект создается в начале программы с его инициализированными значениями по умолчанию.)

  • Окончательный действие в потоке T1 синхронизирует-с какими-либо действиями в другом потоке Т2, который обнаруживает, что Т1 прекращается (Т2 может сделать это путем вызова T1.isAlive() или T1.join())

  • Если поток T1 прерывает поток T2, прерывание от T1 синхронизирует-с любая точка, в которой любой другой поток (включая T2) определяет, что T2 был прерван (путем исключения InterruptedException или путем вызова Thread.interrupted или Thread.isInterrupte г).

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

Смежные вопросы