Припустимо, існує клас, який містить публічне поле лічильника інтелекту, доступне кількома потоками, і це число лише зростає або зменшується.
При додаванні цього поля, яку з наступних схем слід використовувати і чому?
- lock(this.locker) this.counter++;
- Interlocked.Increment (посилається на this.counter);
- Змінити модифікатор доступу counter на публічний волатильний
Найгірше (жоден із них насправді не працює)
Змінити модифікатор доступу counter на публічний волатильний
Цей підхід насправді зовсім не є безпечним, і суть волатильності в тому, що кілька потоків, що працюють на кількох процесорах, буферизують дані та переставляють виконані інструкції.
Якщо він енергонезалежний, то коли CPU A збільшується на значення, CPU B має почекати деякий час, щоб побачити зростання, що може призвести до проблем.
Якщо він волатильний, це гарантує, що обидва процесори бачать однакове значення одночасно. Але це не уникає перехресних операцій читання та запису.
Додавання цінності змінній фактично займає три етапи
1. Читання, 2. Додати 3. Писати
Припустимо, що потік A читає значення лічильника як 1 і не готовий до збільшення, тоді потік B також зчитує значення лічильника як 1, і обидва потоки починають виконувати операції інкрементального та запису. Значення фінального лічильника — 2. Це неправильно, обидва потоки виконали операцію збільшення, і правильний результат має бути 3. Тож маркувати його як мінливий — це просто небезпечно.
Так краще
lock(this.locker) this.counter++;
Так це безпечно (звісно, не забувайте блокувати всюди, де хочете отримати доступ до цього прилавка). Це запобігає виконанню будь-якого іншого потоку заблокованого коду. І це також запобігає згаданій вище проблемі послідовності інструкцій у багатьох процесорах. Проблема в тому, що блокування працює повільно, і якщо ви використовуєте блокування в інших, не пов'язаних з цим місцях, це може заблокувати інші потоки.
Найкраще
Interlocked.Increment (посилається на this.counter);
Це безпечно і дуже ефективно. Він виконує три операції читання, збільшення та запис в одному атомі без переривання посередині. Оскільки це не впливає на інший код, вам не потрібно запам'ятовувати блокування в інших місцях. І він також дуже швидкий (як каже MSDN, на сучасних процесорах це часто просто інструкція).
Але я не зовсім впевнений, чи може це також вирішити проблему впорядкування інструкцій процесора, чи його потрібно використовувати разом із волатильним і цим інкрементом.
Додаток: Які проблеми волатильний продукт вирішує добре?
Оскільки волатильний не може запобігти багатопотоковості, що він може робити? Гарний приклад — у вас є два потоки: один завжди записує у змінну, припустимо, ця змінна queneLength, а інший завжди читає дані з цієї змінної. Якщо queueLongght не є волатильним, потік A може зчитувати 5 разів, але потік B може бачити затримані дані або навіть дані у неправильному порядку. Один із варіантів — використовувати блокування, але в цьому випадку можна також використовувати волатильний. Це гарантує, що потік B завжди бачить останні дані, написані потоком A, але ця логіка працює лише якщо ви не читаєте їх під час написання і не записуєте під час читання. Якщо кілька потоків хочуть виконувати операції читання-модифікації-запису, потрібно використовувати Interlocked або Locked.
|