Худший (на самом деле не будет работать)
Измените модификатор доступа counter
наpublic volatile
Как уже упоминали другие люди, это само по себе вообще не безопасно. Дело в volatile
том, что несколько потоков, работающих на нескольких процессорах, могут и будут кешировать данные и изменять порядок команд.
Если это не так volatile
, и ЦП A увеличивает значение, то ЦП B может фактически не увидеть это увеличенное значение до некоторого времени спустя, что может вызвать проблемы.
Если это так volatile
, то это гарантирует, что два процессора одновременно видят одни и те же данные. Это не мешает им чередовать операции чтения и записи, и это та проблема, которую вы пытаетесь избежать.
Второе место:
lock(this.locker) this.counter++
;
Это безопасно (при условии, что вы помните, lock
где бы вы ни находились this.counter
). Это препятствует тому, чтобы любые другие потоки выполнили любой другой код, который охраняется locker
. Использование блокировок также предотвращает проблемы переупорядочения многопроцессорных систем, как описано выше, и это здорово.
Проблема в том, что блокировка медленная, и если вы повторно используете ее locker
в каком-то другом месте, которое на самом деле не связано, то вы можете в конечном итоге заблокировать другие потоки без причины.
Лучший
Interlocked.Increment(ref this.counter);
Это безопасно, поскольку эффективно выполняет чтение, приращение и запись в одно нажатие, которое не может быть прервано. Из-за этого это не повлияет на любой другой код, и вам также не нужно забывать блокировать в другом месте. Это также очень быстро (как говорит MSDN, на современных ЦП это часто буквально одна инструкция ЦП).
Я не совсем уверен, однако, если это обойти другие процессоры, переупорядочивающие вещи, или вам также нужно объединить volatile с приращением.
InterlockedNotes:
- Взаимосвязанные методы безопасны одновременно на любом количестве ядер или процессоров.
- Взаимосвязанные методы применяют полную границу вокруг выполняемых инструкций, поэтому изменение порядка не происходит.
- Методы с блокировкой не нуждаются или даже не поддерживают доступ к изменчивому полю , поскольку volatile размещает половину ограждения вокруг операций на заданном поле, а блокированное использует полное ограждение.
Сноска: Что на самом деле хорошо для изменчивого?
Как volatile
не предотвратить такие проблемы многопоточности, для чего это нужно? Хорошим примером является то, что у вас есть два потока, один из которых всегда записывает в переменную (скажем queueLength
), а другой всегда читает из этой же переменной.
Если queueLength
он не является энергозависимым, поток A может записывать пять раз, но поток B может видеть, что эти записи задерживаются (или даже могут быть в неправильном порядке).
Решением будет блокировка, но вы также можете использовать volatile в этой ситуации. Это гарантирует, что поток B всегда будет видеть самую последнюю информацию, которую написал поток A. Однако обратите внимание, что эта логика работает, только если у вас есть писатели, которые никогда не читают, и читатели, которые никогда не пишут, и если то, что вы пишете, является атомарной ценностью. Как только вы выполните одну операцию чтения-изменения-записи, вам нужно перейти к блокированным операциям или использовать блокировку.