Призначення посилань є атомним, тому для чого потрібен Interlocked.Exchange (ref об'єкт, об'єкт)?


108

У своєму багатопотоковому веб-службі asmx у мене було поле класу _allData власного типу SystemData, яке складається з кількох List<T>і Dictionary<T>позначене як volatile. Дані системи ( _allData) оновлюються раз у раз, і я роблю це, створюючи інший об’єкт, який називається, newDataі наповнюю його структури даних новими даними. Коли це зроблено, я просто призначаю

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Це має працювати, оскільки призначення є атомним, а потоки, які мають посилання на старі дані, продовжують використовувати його, а решта отримують нові системні дані відразу після призначення. Однак мій колега сказав, що замість використання volatileключового слова та простого призначення я повинен використовувати, InterLocked.Exchangeоскільки він сказав, що на деяких платформах не гарантується, що присвоєння довідок є атомним. Більш того: коли я оголосити the _allDataполе якvolatile

Interlocked.Exchange<SystemData>(ref _allData, newData); 

створює попередження: "посилання на мінливе поле не трактуватиметься як мінливе". Що я повинен думати з цього приводу?

Відповіді:


179

Тут є численні запитання. Розглядаючи їх по одному:

Призначення посилань є атомним, тому для чого потрібен Interlocked.Exchange (ref об'єкт, об'єкт)?

Призначення довідників є атомним. Interlocked.Exchange не виконує лише призначення посилань. Він робить зчитування поточного значення змінної, приховує старе значення і присвоює нове значення змінній, все як атомна операція.

мій колега сказав, що на деяких платформах не гарантується, що присвоєння довідок є атомним. Правильний був мій колега?

Ні. Призначення посилань гарантується атомним на всіх платформах .NET.

Мій колега міркує з помилкових приміщень. Чи означає це, що їхні висновки невірні?

Не обов'язково. Ваш колега міг би дати вам гарну пораду з поганих причин. Можливо, є якась інша причина, чому ви повинні використовувати Interlocked.Exchange. Програмування без блокувань є шалено важким, і в момент, коли ви відходите від усталених практик, що застосовуються фахівцями в цій галузі, ви перебуваєте в бур'яні і ризикуєте найгіршими умовами змагань. Я не є ані експертом у цій галузі, ані експертом у вашому коді, тому не можу виносити судження так чи інакше.

створює попередження: "посилання на мінливе поле не трактуватиметься як мінливе". Що я повинен думати з цього приводу?

Ви повинні зрозуміти, чому це взагалі проблема. Це призведе до розуміння того, чому попередження є неважливим у даному конкретному випадку.

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

(Я припускаю, що ви все це вже розумієте. Якщо ви не маєте детального розуміння значення летючого та як це впливає на семантику кешу процесора, ви не розумієте, як це працює і не слід використовувати летючі. Програми без блокування дуже важко отримати право; переконайтеся, що ваша програма правильна, тому що ви розумієте, як вона працює, не випадково.)

Тепер припустимо, що ви зробите змінну, яка є псевдонімом летючого поля, передавши посилання на це поле. Всередині названого методу компілятор не має жодної причини знати, що у посиланні має бути мінлива семантика! Компілятор бадьоро генерує код для методу, який не реалізує правила для летючих полів, але змінна - це мінливе поле. Це може повністю зруйнувати вашу логіку без блокування; припущення завжди полягає в тому, що до летючого поля завжди звертається летюча семантика. Немає сенсу трактувати це як мінливий іноді, а не інший час; ви завжди повинні бути послідовними, інакше ви не можете гарантувати послідовність щодо інших доступу.

Тому компілятор попереджає, коли ви це зробите, адже це, ймовірно, повністю зіпсує вашу ретельно розроблену логіку без блокування.

Звичайно, Interlocked.Exchange буде написано очікувати летюча поле і робити правильні речі. Тому попередження є оманливим. Я дуже шкодую про це; що ми повинні були зробити, це реалізувати якийсь механізм, за допомогою якого автор такого методу, як Interlocked.Exchange міг би поставити атрибут методу, що говорить "цей метод, який приймає ref, примушує мінливу семантику змінної, тому придушуйте попередження". Можливо, у майбутній версії компілятора ми це зробимо.


1
З того, що я чув, що Interlocked.Exchange також гарантує створення бар'єру пам'яті. Отже, якщо ви, наприклад, створюєте новий об'єкт, призначаєте пару властивостей, а потім зберігаєте об'єкт у іншій посилання без використання Interlocked.Exchange, тоді компілятор може зіпсувати порядок цих операцій, таким чином, зробивши доступ до другої посилання не потоковою, сейф. Це справді так? Чи має сенс використовувати Interlocked.Exchange такий сценарій?
Майк

12
@Mike: Якщо мова йде про те, що, можливо, спостерігається у багатопотокових ситуаціях із низьким рівнем блокування, я настільки ж необізнаний, як наступний хлопець. Відповідь, ймовірно, буде відрізнятися від процесора до процесора. Ви повинні звернутися зі своїм питанням до експерта або прочитати цю тему, якщо вона вас цікавить. Книга Джо Даффі та його блог - хороші місця для початку. Моє правило: не використовуйте багатопотокове читання. Якщо потрібно, використовуйте незмінні структури даних. Якщо ви не можете, користуйтеся замками. Тільки тоді, коли у вас повинні бути мутаційні дані без блокування, ви повинні розглядати методи з низьким рівнем блокування.
Ерік Ліпперт

Дякую за вашу відповідь, Ерік. Мене це справді цікавить, тому я читав книги та блоги про багатопотокові та блокуючі стратегії, а також намагався втілити їх у свій код. Але ще є чому навчитися ...
Майк

2
@EricLippert Між "не використовувати багатопотокове" та "якщо потрібно, використовуй незмінні структури даних", я б вставив проміжний і дуже поширений рівень ", якщо дочірній потік використовує лише виключно власні вхідні об'єкти, а батьківський потік споживає результати лише тоді, коли дитина закінчиться ». Як у var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
Іван

1
@John: Це гарна ідея. Я намагаюся ставитись до потоків як до дешевих процесів: вони там, щоб виконати роботу і створити результат, а не бігати навколо того, щоб бути другим потоком управління в структурах даних основної програми. Але якщо обсяг роботи, яку нитка виконує, настільки великий, що розумно ставитися до цього як до процесу, то я кажу, просто зробіть це процесом!
Ерік Ліпперт

9

Або ваш колега помиляється, або він знає щось, чого не має специфікація мови C #.

5.5 Атомність змінних посилань :

"Читання та записи таких типів даних є атомними: bool, char, byte, sbyte, short, ushort, uint, int, float та reference reference."

Отже, ви можете записати на мінливу посилання, не ризикуючи отримати зіпсоване значення.

Звичайно, слід бути обережним з тим, як ви вирішите, який потік повинен отримати нові дані, щоб мінімізувати ризик того, що одночасно це робить більше, ніж один потік.


3
@guffa: так, я також це читав. це залишає первісне питання "присвоєння посилання є атомним, тому для чого потрібна Interlocked.Exchange (ref об'єкт, об'єкт)?" без відповіді
чар м

@zebrabox: що ти маєш на увазі? коли їх немає? що б ти зробив?
чар м

@matti: Це потрібно, коли потрібно читати і записувати значення як атомна операція.
Гуффа

Як часто вам доводиться турбуватися про неправильне вирівнювання пам'яті в .NET? Interop-важкі речі?
Скурмедель

1
@zebrabox: У специфікації не вказано цей застереження, він дає дуже чітке твердження. Чи є у вас посилання на ситуацію, що не узгоджується з пам’яттю, коли посилання читання або запису не може бути атомним? Схоже, це порушило б дуже чітку мову в специфікації.
TJ Crowder

6

Перемикається. Обмін <T>

Встановлює змінну вказаного типу T на вказане значення і повертає початкове значення, як атомну операцію.

Це змінює і повертає початкове значення, воно марне, оскільки ви хочете змінити його, і, як сказав Гуффа, воно вже атомне.

Якщо профілером не доведено, що це вузьке місце у вашій програмі, вам слід розглянути питання про розблокування блоків, простіше зрозуміти і довести, що ваш код правильний.


3

Iterlocked.Exchange() це не лише атомний стан, але також дбає про видимість пам'яті:

Наступні функції синхронізації використовують відповідні бар'єри для забезпечення впорядкування пам'яті:

Функції, які входять або залишають критичні розділи

Функції, що сигналізують про об'єкти синхронізації

Зачекайте функції

Заблоковані функції

Проблеми синхронізації та мультипроцесора

Це означає, що крім атомності він забезпечує:

  • Для потоку, що називає його:
    • Переупорядкування інструкцій не проводиться (компілятором, часом виконання або апаратним забезпеченням).
  • Для всіх тем:
    • Жодне зчитування в пам'яті, що відбулося до цієї інструкції, не побачить зміни, які внесла ця інструкція.
    • Усі прочитані після цієї інструкції побачать зміни, внесені цією інструкцією.
    • Усі записи в пам'ять після цієї інструкції відбудуться після того, як ця зміна інструкції досягне основної пам’яті (прошивши цю інструкцію, перейдіть на основну пам'ять, коли її виконано, і не дайте апарату залишити її власною на час).
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.