Резюме:
Мне кажется, что:
- упаковка полей, представляющих логическое состояние, в один неизменный потребляемый объект
- обновление авторитетной ссылки объекта путем вызова
Interlocked.CompareExchange<T>
- и соответствующая обработка ошибок обновления
обеспечивает своего рода параллелизм, который делает конструкцию «блокировки» не только ненужной, но и действительно вводящей в заблуждение конструкцией, которая уклоняется от некоторых реалий о параллелизме и в результате приводит к множеству новых проблем.
Обсуждение проблемы:
Сначала рассмотрим основные проблемы с использованием блокировки:
- Блокировки вызывают снижение производительности и должны использоваться в тандеме для чтения и записи.
- Блокирует выполнение потока блока, препятствует параллелизму и рискует тупиков .
Подумайте о нелепом поведении, вдохновленном «замком». Когда возникает необходимость одновременного обновления логического набора ресурсов, мы «блокируем» набор ресурсов и делаем это с помощью свободно связанного, но выделенного объекта блокировки, который в противном случае бесполезен (красный флаг # 1).
Затем мы используем шаблон «блокировки» для разметки области кода, где происходит логически непротиворечивое изменение состояния на множестве полей данных, и все же мы стреляем себе в ногу, смешивая поля с несвязанными полями в одном и том же объекте, оставляя их все изменяемыми, а затем загоняя себя в угол (красный флаг # 2), где мы также должны использовать блокировки при чтении этих различных полей, поэтому мы не перехватываем их в несогласованных состояние.
Очевидно, есть серьезная проблема с этим дизайном. Это несколько нестабильно, поскольку требует тщательного управления объектами блокировки (порядок блокировки, вложенные блокировки, координация между потоками, блокировка / ожидание ресурса, используемого другим потоком, который ожидает, чтобы вы что-то сделали, и т. Д.), Что зависит от контекст. Мы также слышим, как люди говорят о том, как избежать тупика «сложно», хотя на самом деле это очень просто: не кради туфли человека, которого ты собираешься попросить устроить гонку для тебя!
Решение:
Полностью прекратите использование «блокировки». Правильно сверните свои поля в нетленный / неизменный объект, представляющий согласованное состояние или схему. Возможно, это просто пара словарей для преобразования в и из отображаемых имен и внутренних идентификаторов, или, может быть, это головной узел очереди, содержащий значение и ссылку на следующий объект; что бы это ни было, заверните его в свой собственный объект и запечатайте его для согласованности.
Признать ошибку записи или обновления как возможную, обнаружить ее, когда она происходит, и принять контекстуально обоснованное решение повторить попытку (или позже) немедленно или сделать что-то еще вместо бесконечной блокировки.
В то время как блокировка кажется простым способом поставить задачу в очередь, которую кажется такой, как она должна быть , не все потоки являются настолько выделенными и эгоистичными, что они могут себе позволить такая вещь рискует поставить под угрозу всю систему. Не только лениво сериализовать вещи с «блокировкой», но и побочным эффектом попытки сделать вид, что запись не должна завершиться неудачей, вы блокируете / замораживаете свой поток, так что он становится там безответственным и бесполезным, оставляя все другие обязанности в его упрямый ждать, чтобы сделать то, что он намеревался сделать некоторое время раньше, не зная о том, что оказание помощи другим иногда необходимо для выполнения его собственных обязанностей.
Условия гонки являются нормальными, когда независимые, спонтанные действия происходят одновременно, но в отличие от неконтролируемых коллизий Ethernet, поскольку у программистов мы имеем полный контроль над нашей «системой» (то есть детерминированным цифровым оборудованием) и ее входами (независимо от того, насколько случайны и как случайным может быть ноль или единица на самом деле?) и выходы, и память, в которой хранится состояние нашей системы, поэтому живая блокировка не должна быть проблемой; кроме того, у нас есть атомарные операции с барьерами памяти, которые решают тот факт, что одновременно может работать много процессоров.
Подведем итог:
- Захватите объект текущего состояния, используйте его данные и создайте новое состояние.
- Поймите, что другие активные потоки будут делать то же самое и могут побить вас этим, но все соблюдают авторитетную контрольную точку, представляющую "текущее" состояние.
- Используйте Interlocked.CompareExchange, чтобы одновременно увидеть, является ли объект состояния, на котором вы основали свою работу, по-прежнему самым текущим состоянием, и заменить его новым, в противном случае произойдет сбой (потому что другой поток завершился первым) и предпринять соответствующие корректирующие действия.
Самая важная часть - как вы справляетесь с неудачей и возвращаетесь на лошадь. Здесь мы избегаем блокировок, слишком много думаем, недостаточно делаем или делаем правильные вещи. Я бы сказал, что замки создают иллюзию того, что вы никогда не упадете с лошади, несмотря на то, что едете в давке, и хотя в такой фантастической стране мечтает нить, остальная часть системы может развалиться, разбиться и сгореть.
Итак, есть ли что-то, что может сделать конструкция "блокировки", чего нельзя достичь (лучше, менее нестабильно) с реализацией без блокировки, использующей CompareExchange и неизменяемые объекты логического состояния?
Все это - осознание, к которому я пришел самостоятельно после интенсивной работы с блокировками, но после некоторого поиска в другом потоке Упрощает ли многопоточное программирование без блокировок что-то более легкое? , кто-то упоминает, что Программирование без блокировок будет очень важно, когда мы сталкиваемся с высокопараллельными системами с сотнями процессоров, в которых мы не можем позволить себе использовать жесткие блокировки.