Является ли конструкция C # «lock» устаревшей Interlocked.CompareExchange <T>? - PullRequest
2 голосов
/ 16 сентября 2009

Резюме:

Мне кажется, что:

  1. упаковка полей, представляющих логическое состояние, в один неизменный потребляемый объект
  2. обновление авторитетной ссылки объекта путем вызова Interlocked.CompareExchange<T>
  3. и соответствующая обработка ошибок обновления

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

Обсуждение проблемы:

Сначала рассмотрим основные проблемы с использованием блокировки:

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

Подумайте о нелепом поведении, вдохновленном «замком». Когда возникает необходимость одновременного обновления логического набора ресурсов, мы «блокируем» набор ресурсов и делаем это с помощью свободно связанного, но выделенного объекта блокировки, который в противном случае бесполезен (красный флаг # 1).

Затем мы используем шаблон «блокировки» для разметки области кода, где происходит логически непротиворечивое изменение состояния на множестве полей данных, и все же мы стреляем себе в ногу, смешивая поля с несвязанными полями в одном и том же объекте, оставляя их все изменяемыми, а затем загоняя себя в угол (красный флаг # 2), где мы также должны использовать блокировки при чтении этих различных полей, поэтому мы не перехватываем их в несогласованных состояние.

Очевидно, есть серьезная проблема с этим дизайном. Это несколько нестабильно, поскольку требует тщательного управления объектами блокировки (порядок блокировки, вложенные блокировки, координация между потоками, блокировка / ожидание ресурса, используемого другим потоком, который ожидает, чтобы вы что-то сделали, и т. Д.), Что зависит от контекст. Мы также слышим, как люди говорят о том, как избежать тупика «сложно», хотя на самом деле это очень просто: не кради туфли человека, которого ты собираешься попросить устроить гонку для тебя!

Решение:

Полностью прекратите использование «блокировки». Правильно сверните свои поля в нетленный / неизменный объект, представляющий согласованное состояние или схему. Возможно, это просто пара словарей для преобразования в и из отображаемых имен и внутренних идентификаторов, или, может быть, это головной узел очереди, содержащий значение и ссылку на следующий объект; что бы это ни было, заверните его в свой собственный объект и запечатайте его для согласованности.

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

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

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

Подведем итог:

  1. Захватите объект текущего состояния, используйте его данные и создайте новое состояние.
  2. Поймите, что другие активные потоки будут делать то же самое и могут побить вас этим, но все соблюдают авторитетную контрольную точку, представляющую "текущее" состояние.
  3. Используйте Interlocked.CompareExchange, чтобы одновременно увидеть, является ли объект состояния, на котором вы основали свою работу, по-прежнему самым текущим состоянием, и заменить его новым, в противном случае произойдет сбой (потому что другой поток завершился первым) и предпринять соответствующие корректирующие действия.

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


Итак, есть ли что-то, что может сделать конструкция "блокировки", чего нельзя достичь (лучше, менее нестабильно) с реализацией без блокировки, использующей CompareExchange и неизменяемые объекты логического состояния?

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

Ответы [ 6 ]

3 голосов
/ 16 сентября 2009

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

2 голосов
/ 16 сентября 2009

Есть четыре условия для гонки.

  1. Первым условием является наличие областей памяти, доступных из более чем одного потока. Как правило, эти местоположения являются глобальными / статическими переменными или достижимы в куче памяти из глобальных / статических переменных.
  2. Вторым условием является наличие свойства (часто называемого инвариантом), которое связано с этими местами общей памяти, которые должны быть истинными или действительными, чтобы программа функционировала правильно. Обычно свойство должно иметь значение true, прежде чем произойдет обновление, чтобы обновление было корректным.
  3. Третье условие заключается в том, что свойство инварианта не выполняется во время некоторой части фактического обновления. (Это временно недействительно или ложно во время некоторой части обработки).
  4. Четвертое и последнее условие, которое должно произойти для гонки, состоит в том, что другой поток обращается к памяти, в то время как инвариант нарушается, вызывая, таким образом, непоследовательное или неправильное поведение.

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

    2. Если не существует инварианта (скажем, все, что вы делаете, это записываете в эту общую память, и ничто в операции потока не считывает ее значение), то опять же, нет проблем.

    3. Если инвариант никогда не является недействительным, опять же нет проблем. (скажем, разделяемая память - это поле даты и времени, в котором хранится дата и время последнего выполнения кода, тогда она не может быть недействительной, если поток вообще не может ее записать ...

    4. Чтобы исключить nbr 4, необходимо ограничить доступ на запись к блоку кода, который обращается к общей памяти из более чем одного потока одновременно, используя блокировку или некоторую сопоставимую методологию синхронизации.

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

1 голос
/ 27 июля 2010

Я бы сказал, что это не более устарело, чем говорить в целом, что пессимистический параллелизм устарел при оптимистичном параллелизме или что паттерн А устарел из-за паттерна Б. Без блокировки является мощным, но не может быть смысла применять его в одностороннем порядке, потому что не каждая проблема идеально подходит для этого. Есть компромиссы. Тем не менее, было бы хорошо иметь универсальный оптимистический подход без блокировок, если он не был реализован традиционно. Короче говоря, да, lock может сделать что-то, чего нельзя достичь другим подходом: представить потенциально более простое решение. С другой стороны, может случиться так, что оба имеют одинаковый результат, если определенные вещи не имеют значения. Полагаю, я немного двусмысленна ...

1 голос
/ 30 октября 2009

Большое преимущество блокировки над операцией CAS, такой как Interlocked.CompareExchange, заключается в том, что вы можете изменять несколько областей памяти в пределах блокировки, и все изменения будут видны другим потокам / процессам одновременно.

В CAS атомарно обновляется только одна переменная. Код без блокировки, как правило, значительно сложнее, потому что вы можете не только представить обновление только одной переменной (или двух смежных переменных с CAS2) одновременно другим потокам, вы также должны иметь возможность обрабатывать условия «сбой», когда CAS не не удастся Кроме того, вам необходимо решить проблемы с ABA и другие возможные осложнения.

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

Тем не менее, существует множество интересных применений как для кода блокировки, так и для кода без блокировки. Однако, если вы ДЕЙСТВИТЕЛЬНО не знаете, что делаете, создавая свой собственный код без блокировки, не для новичка. Используйте либо код без блокировки, либо хорошо зарекомендовавшие себя алгоритмы, и тщательно их протестируйте, потому что очень трудно найти граничные условия, которые вызывают сбой во многих попытках без блокировки.

1 голос
/ 16 сентября 2009

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

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

0 голосов
/ 12 февраля 2013

Теоретически, если есть определенный объем работы, программа, использующая Interlocked.CompareExchange, сможет выполнить все это без блокировки. К сожалению, в условиях высокой конкуренции цикл read / compute-new / compareExchange может привести к такому ускорению, что 100 процессорам, каждый из которых пытается выполнить одно обновление для общего элемента данных, может потребоваться больше времени - в real время - чем один процессор выполняет 100 последовательных обновлений. Параллелизм не улучшит производительность - он убьет ее. Использование блокировки для защиты ресурса будет означать, что только один процессор за раз сможет его обновить, но улучшит производительность, чтобы соответствовать случаю с одним ЦП.

Единственным реальным преимуществом программирования без блокировок является то, что функциональность системы не пострадает, если поток будет задержан на произвольное количество времени. Можно сохранить это преимущество, избегая при этом ошибок в производительности при чисто программировании на основе CompareExchange, используя комбинацию блокировок и тайм-аутов. Основная идея заключается в том, что при наличии конкуренции ресурс переключается на синхронизацию на основе блокировок, но если поток слишком долго удерживает блокировку, будет создан новый объект блокировки, а более ранняя блокировка будет проигнорирована. Это будет означать, что если этот прежний поток все еще пытался выполнить цикл CompareExchange, он потерпит неудачу (и должен начинаться заново), но более поздние потоки не будут заблокированы и не будет принесена в жертву правильность.

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

...