Допустим, есть класс, содержащий публичное поле счётчика int, доступное несколькими потоками, и это число будет только увеличиваться или уменьшаться.
При добавлении этого поля какую из следующих схем следует использовать и почему?
- lock(this.locker) this.counter++;
- Interlocked.Increment (см. this.counter);
- Измените модификатор доступа от counter на публичный волатильный
Хуже всего (ни один из них не работает)
Измените модификатор доступа от counter на публичный волатильный
Этот подход на самом деле совсем не безопасен, и суть волатильности в том, что несколько потоков, работающих на нескольких процессорах, буферизируют данные и перестраивают выполненные инструкции.
Если он неволатильный, то при увеличении числа CPU B нужно подождать некоторое время, чтобы увидеть это значение, что может привести к проблемам.
Если он волатильный, это гарантирует, что оба процессора одновременно видят одно и то же значение. Но это не позволяет избежать пересечения операций чтения и записи.
Добавление значения переменной фактически требует трёх шагов
1. Чтение, 2. Добавить 3. Писать
Предположим, что поток A читает значение счётчика как 1 и не готов к увеличению, затем поток B также считывает значение счетчика как 1, и тогда оба потока начинают выполнять инкрементальные и записывающие операции. Значение финального счётчика — 2. Это неправильно, оба потока выполнили операцию увеличения, и правильный результат должен быть 3. Поэтому называть его нестабильным — это просто небезопасно.
Так лучше
lock(this.locker) this.counter++;
Так это безопасно (конечно, не забудьте заблокировать везде, где хотите получить доступ к этому прилавку). Это мешает любому другому потоку выполнять заблокированный код. Кроме того, это предотвращает проблему последовательности инструкций на нескольких процессорах, упомянутую выше. Проблема в том, что блокировка работает медленно, и если вы используете блокировку в других местах, это может заблокировать другие потоки.
Лучшие
Interlocked.Increment (см. this.counter);
Это безопасно и очень эффективно. Он выполняет три операции чтения, увеличения, записи в одном атоме без прерывания посередине. Поскольку это не влияет на другой код, вам не нужно запоминать блокировки в других местах. И он также очень быстрый (как говорит MSDN, на современных процессорах это часто просто инструкция).
Но я не совсем уверен, сможет ли это также решить проблему порядка инструкций процессора, или его нужно использовать вместе с волатильностью и этим инкрементом.
Дополнение: Какие проблемы волатил решает хорошо?
Поскольку волатильный не может предотвратить многопоточность, что он может сделать? Хороший пример — у вас есть два потока: один всегда записывает в переменную, допустим, эта переменная — queneLength, а другой всегда читает данные из этой переменной. Если queueLenght не волатильна, поток A может читать 5 раз, но поток B может видеть задержанные данные или даже данные в неправильном порядке. Один из вариантов — использовать блокировку, но в данном случае можно использовать и волатильный. Это гарантирует, что поток B всегда видит последние данные, записанные потоком A, но эта логика работает только если вы не читаете их при записи и не записываете их во время чтения. Когда несколько потоков хотят выполнять операции чтения-модификации-записи, нужно использовать Interlocked или Lock.
|