Чи може хтось дати хороше пояснення мінливого ключового слова в C #? Які проблеми воно вирішує, а які - ні? У яких випадках врятує мене використання блокування?
Чи може хтось дати хороше пояснення мінливого ключового слова в C #? Які проблеми воно вирішує, а які - ні? У яких випадках врятує мене використання блокування?
Відповіді:
Я не думаю, що краще відповісти на це, ніж Ерік Ліпперт (наголос в оригіналі):
У C # "мінливий" означає не лише "переконайтесь, що компілятор і тремтіння не виконують жодної переупорядкування коду або реєструють кешування оптимізації цієї змінної". Це також означає "сказати процесорам робити все, що потрібно, щоб переконатися, що я читаю останнє значення, навіть якщо це означає зупинку інших процесорів і змушення їх синхронізувати основну пам'ять зі своїми кешами".
Власне, останній шматочок - брехня. Справжня семантика мінливих читань і записів значно складніша, ніж я окреслив тут; насправді вони насправді не гарантують, що кожен процесор зупиняє те, що він робить, і оновлює кеші в / з основної пам'яті. Швидше, вони дають більш слабкі гарантії щодо того, як доступ до пам'яті до і після читання і запису може спостерігатися, щоб вони були впорядковані один щодо одного . Окремі операції, такі як створення нової нитки, введення блокування або використання одного з методів сімейства Interlocked, дають більш гарантії щодо спостереження за замовленням. Якщо ви хочете отримати більше деталей, прочитайте розділи 3.10 та 10.5.3 специфікації C # 4.0.
Чесно кажучи, я заважаю тобі коли-небудь робити мінливе поле . Летючі поля - знак того, що ви робите щось прямо-таки божевільне: ви намагаєтесь прочитати та записати одне і те ж значення на двох різних потоках, не ставлячи замок на місце. Блокування гарантує, що пам'ять, яку читають чи змінюють пам'ять всередині блоку, є послідовною, блокування гарантує, що лише один потік отримує доступ до заданого фрагменту пам'яті одночасно тощо. Кількість ситуацій, в яких блокування занадто повільне, дуже мала, і ймовірність того, що ви збираєтеся помилитися з кодом, оскільки ви не розумієте точну модель пам'яті, дуже велика. Я не намагаюся записати код з низьким рівнем блокування, за винятком самих тривіальних звичаїв операцій Interlocked. Я залишаю використання "летючого" справжнім експертам.
Для подальшого читання див:
volatile
будуть існувати в силу блокування
Якщо ви хочете трохи детальніше ознайомитись з тим, що робить мінливе ключове слово, врахуйте наступну програму (я використовую DevStudio 2005):
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
Використовуючи стандартні оптимізовані (випущені) настройки компілятора, компілятор створює такий асемблер (IA32):
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
Дивлячись на вихід, компілятор вирішив використовувати регістр ecx для зберігання значення змінної j. Для енергонезалежного циклу (перший) компілятор призначив i в регістр eax. Досить прямолінійний. Хоча є кілька цікавих бітів - інструкція lea ebx, [ebx] - це фактично багатобайтова команда nop, завдяки чому цикл переходить на 16-байтну адресу пам'яті. Інший - використання edx для збільшення лічильника циклу замість використання інструкції inc eax. Інструкція додати reg, reg має нижчу затримку на декілька ядер IA32 порівняно з інструкцією inc reg, але ніколи не має більш високої затримки.
Тепер для петлі з лічильником летючої петлі. Лічильник зберігається у [esp], а мінливе ключове слово повідомляє компілятору, що значення завжди слід читати з / записувати в пам'ять і ніколи не призначати реєстру. Компілятор навіть йде так далеко, що не виконує завантаження / приріст / зберігання, як три різних кроки (load eax, inc eax, save eax) при оновленні значення лічильника, натомість пам'ять безпосередньо змінюється в одній інструкції (add mem , рег.). Спосіб створення коду забезпечує значення лічильника циклу завжди актуальним у контексті одного ядра процесора. Жодна операція з даними не може призвести до пошкодження або втрати даних (отже, не використовувати навантаження / інк / магазин, оскільки значення може змінюватися під час inc, таким чином, втрачається в магазині). Оскільки переривання можна обслуговувати лише після завершення поточної інструкції,
Після того, як ви введете в систему другий процесор, мінливе ключове слово не захистить даних, які одночасно оновлюються іншим процесором. У наведеному вище прикладі вам знадобляться дані, щоб їх не було, щоб отримати потенційну корупцію. Ефірне ключове слово не запобіжить потенційній корупції, якщо з даними не вдасться обробляти атомно, наприклад, якщо лічильник циклу був типовим довгим (64 біт), то для оновлення значення знадобиться дві 32-бітні операції в середині яка може статися переривання та змінити дані.
Отже, мінливе ключове слово добре лише для вирівняних даних, розмір яких є меншим або рівним розміру нативних регістрів, таким чином, що операції завжди є атомними.
Ефірне ключове слово було задумане для використання в операціях вводу-виводу, де IO постійно змінювався, але мав постійну адресу, наприклад пристрій UART, нанесений на пам'ять, і компілятор не повинен повторно використовувати перше значення, прочитане з адреси.
Якщо ви обробляєте великі дані або маєте декілька процесорів, то для правильного доступу до даних вам знадобиться система блокування вищого рівня (ОС).
Якщо ви використовуєте .NET 1.1, непостійне ключове слово потрібне при подвійному перевіреному блокуванні. Чому? Оскільки до .NET 2.0 наступний сценарій може викликати доступ другого потоку до ненульового, але ще не повністю сконструйованого об'єкта:
До .NET 2.0, цьому.foo можна було призначити новий екземпляр Foo до того, як конструктор закінчився. У цьому випадку може вступити другий потік (під час виклику потоку 1 до конструктора Foo) і зазнати наступного:
До .NET 2.0 ви можете оголосити this.foo як мінливий, щоб подолати цю проблему. Оскільки .NET 2.0, вам більше не потрібно використовувати мінливе ключове слово, щоб виконати подвійне перевірене блокування.
У Вікіпедії є хороша стаття про подвійне перевірене блокування та коротко зачіпає цю тему: http://en.wikipedia.org/wiki/Double-cont_locking
foo
? Чи не this.bar
блокується нитка 1, і тому лише нитка 1 зможе ініціалізувати foo у певний момент часу? Я маю на увазі, ви перевіряєте це значення після того, як блокування буде відпущене знову, коли в будь-якому випадку воно повинно мати нове значення з потоку 1.
Іноді компілятор оптимізує поле та використовує реєстр для його зберігання. Якщо потік 1 робить запис у поле, а інший потік отримує доступ до нього, оскільки оновлення зберігалося в реєстрі (а не пам'яті), 2-й потік отримав би несвіжі дані.
Ви можете подумати про мінливе ключове слово, як сказати компілятору "Я хочу, щоб ти зберігав це значення в пам'яті". Це гарантує, що другий потік отримує останнє значення.
Від MSDN : мінливий модифікатор зазвичай використовується для поля, до якого звертаються декілька потоків без використання оператора блокування для серіалізації доступу. Використання летючого модифікатора гарантує, що один потік отримує найсвіжіше значення, записане іншим потоком.
CLR любить оптимізувати інструкції, тому, коли ви отримуєте доступ до поля в коді, він не завжди може отримати доступ до поточного значення поля (це може бути зі стека тощо). Позначення поля таким чином, що volatile
забезпечує доступ до поточного значення поля за допомогою інструкції. Це корисно, коли значення може бути змінено (у сценарії, що не блокується) за допомогою одночасного потоку у вашій програмі або якогось іншого коду, що працює в операційній системі.
Ви, очевидно, втрачаєте деяку оптимізацію, але це робить код простішим.
Я вважаю цю статтю Джойдіпа Канджілала дуже корисною!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
Я просто залишлю це для довідки
Іноді компілятор змінює порядок висловлювань у коді, щоб оптимізувати його. Зазвичай це не проблема в однопотоковому середовищі, але це може бути проблемою в багатопотоковому середовищі. Дивіться наступний приклад:
private static int _flag = 0;
private static int _value = 0;
var t1 = Task.Run(() =>
{
_value = 10; /* compiler could switch these lines */
_flag = 5;
});
var t2 = Task.Run(() =>
{
if (_flag == 5)
{
Console.WriteLine("Value: {0}", _value);
}
});
Якщо ви запускаєте t1 і t2, ви не очікуєте, що в результаті не буде результату або "Значення: 10". Можливо, компілятор перемикає лінію всередині функції t1. Якщо t2 тоді виконується, може бути, що _flag має значення 5, а _value 0. Очікувана логіка може бути порушена.
Для виправлення цього можна використовувати летючі ключове слово, яке можна застосувати до цього поля. Це твердження вимикає оптимізацію компілятора, щоб ви могли примусити правильний порядок в коді.
private static volatile int _flag = 0;
Ви можете використовувати летючі, лише якщо вам це справді потрібно, оскільки це відключає певні оптимізації компілятора, це зашкодить продуктивності. Він також не підтримується всіма мовами .NET (Visual Basic не підтримує його), тому він перешкоджає сумісності мови.
Отже, підсумовуючи все це, правильна відповідь на питання: Якщо ваш код працює в режимі виконання 2.0 або пізнішого періоду, мінливе ключове слово майже ніколи не потрібне та приносить більше шкоди, ніж користі, якщо його використовувати не потрібно. IE Ніколи не використовуйте його. АЛЕ в попередніх версіях часу виконання, це необхідно для правильного подвійного блокування перевірки на статичні поля. Зокрема статичні поля, клас яких має код ініціалізації статичного класу.
декілька потоків можуть отримати доступ до змінної. Останнє оновлення буде на змінній