Чому енергонезалежність не вважається корисною в багатопотоковому програмуванні на C або C ++?


165

Як показано у цій відповіді, яку я нещодавно опублікував, я, мабуть, розгублений щодо корисності (або її відсутності) volatileв контекстах багатопотокового програмування.

Моє розуміння таке: щоразу, коли змінна може бути змінена поза потоком контролю фрагмента коду, що отримує доступ до неї, ця змінна повинна бути оголошена такою volatile. Обробники сигналів, регістри вводу / виводу та змінні, змінені іншим потоком, є такими ситуаціями.

Отже, якщо у вас є глобальний int fooі fooчитається одним потоком і встановлюється атомно іншим потоком (можливо, використовуючи відповідну машинну інструкцію), нитка для читання бачить цю ситуацію так само, як вона бачить змінну, налаштовану обробником сигналу або модифікований зовнішнім апаратним станом і, таким чином, fooповинен бути оголошений volatile(або, для багатопотокових ситуацій, доступ із обмеженим навантаженням навантаженням, що, мабуть, є кращим рішенням).

Як і де я помиляюся?


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

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

14
@neil Я шукав інші питання і знайшов одне, але будь-яке наявне пояснення, яке я бачив, якимось чином не викликало те, що мені потрібно, щоб дійсно зрозуміти, чому я помиляюся. Це питання викликало таку відповідь.
Майкл Екстранд

1
Для великого поглибленого дослідження про те , що процесори робити з даними (через їх кеша) перевірити: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot

1
@curiousguy Це те, що я мав на увазі під "не випадком у C", де його можна використовувати для запису до апаратних реєстрів тощо, і не використовується для багатопотокових записів, як це зазвичай використовується в Java.
Монсьє

Відповіді:


213

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

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

Для безпечного доступу до спільних даних нам потрібна гарантія, що:

  • насправді відбувається читання / запис (що компілятор не просто збереже значення в регістрі, а відкладе оновлення основної пам'яті на набагато пізніше)
  • що ніякого переупорядкування не відбувається. Припустимо, що ми використовуємо volatileзмінну як прапор, щоб вказати, чи готові деякі дані для читання. У нашому коді ми просто встановлюємо прапор після підготовки даних, тому все виглядає нормально. Але що робити, якщо інструкції упорядковані, щоб прапор був встановлений спочатку ?

volatileгарантує перший пункт. Це також гарантує, що не відбувається переупорядкування між різними летучими читаннями / записами . Усі volatileдоступ до пам'яті відбуватиметься в тому порядку, в якому вони вказані. Це все, що нам потрібно для того, для чого volatileпризначено: маніпулювання регістрами вводу-виводу або апаратним забезпеченням, але це не допомагає нам у багатопотоковому коді, деvolatile об'єкт часто використовується лише для синхронізації доступу до енергонезалежних даних. Ці звернення все ще можуть бути впорядковані щодо доступних volatile.

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

Однак бар'єри пам'яті також гарантують, що всі очікувані читання / записи виконуються, коли бар'єр досягнутий, тому він ефективно дає нам все необхідне сам, роблячи volatileнепотрібним. Ми можемо просто знятиvolatile класифікатор повністю.

Оскільки C ++ 11, атомні змінні ( std::atomic<T>) дають нам усі відповідні гарантії.


5
@jbcreix: Про яке "це" ви питаєте? Нестабільні або бар'єри пам'яті? У будь-якому випадку, відповідь майже однакова. Їм обом доводиться працювати як на рівні компілятора, так і на процесорі, оскільки вони описують спостережувану поведінку програми --- тому вони повинні переконатися, що ЦП не переробляє все, змінюючи поведінку, яку вони гарантують. Наразі ви не можете записати синхронізацію портативних потоків, оскільки бар'єри пам'яті не є частиною стандартного C ++ (тому вони не є портативними) і volatileне є достатньо сильними, щоб бути корисними.
джельф

4
Приклад MSDN робить це, і стверджує, що вказівки не можуть бути перепорядковані через нестабільний доступ: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW

27
@OJW: Але компілятор Майкрософт переосмислює volatileповний бар'єр пам’яті (запобігаючи переупорядкуванню). Це не є частиною стандарту, тому ви не можете розраховувати на таку поведінку в портативному коді.
джельф

4
@Skizz: ні, саме тут входить частина рівняння "магія компілятора". Бар'єр пам'яті повинен розуміти і процесор, і компілятор. Якщо компілятор розуміє семантику бар'єру пам’яті, він знає уникати подібних хитрощів (а також переупорядковувати читання / запис через бар’єр). І на щастя, компілятор робить зрозуміти семантику бар'єр пам'яті, так що врешті-решт, все це працює. :)
jalf

13
@Skizz: Нитки самі завжди є залежним від платформи розширенням до C ++ 11 та C11. Наскільки мені відомо, кожне середовище C і C ++, яке забезпечує розширення для потоків, також забезпечує розширення "бар'єр пам'яті". Незалежно від цього, volatileвін завжди марний для багатопотокового програмування. (За винятком Visual Studio, де мінливим є розширення бар'єру пам’яті.)
Немо

49

Ви можете також розглянути це з Документації ядра Linux .

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

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

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

Розглянемо типовий блок коду ядра:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Якщо весь код дотримується правил блокування, значення shared_data не може несподівано змінитися під час утримування the_lock. Будь-який інший код, який може захотіти зіграти з цими даними, буде чекати на блокування. Спінлок-примітиви виступають як бар'єри пам'яті - вони прямо написані для цього - це означає, що доступ до даних не буде оптимізований через них. Таким чином, компілятор може подумати, що він знає, що буде у спільних_даних, але виклик spin_lock (), оскільки він виступає як бар'єр пам'яті, змусить його забути все, що він знає. Не буде проблем з оптимізацією доступу до цих даних.

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

Клас енергонезалежної пам'яті спочатку призначений для регістрів вводу / виводу, відображених на пам'ять. У межах ядра реєстрація доступу також повинна бути захищена блокуваннями, але також не хочеться, щоб компілятор "оптимізував" реєстрацію доступу в критичному розділі. Але всередині ядра доступ до пам'яті вводу / виводу завжди здійснюється через функції аксесуара; доступ до пам’яті вводу-виводу безпосередньо через покажчики нахмуриться і працює не у всіх архітектурах. Ці аксесуари написані для запобігання небажаної оптимізації, тому, знову ж таки, непостійні непотрібні.

Інша ситуація, коли можна спокусити використовувати летючі, - це коли процесор зайнятий-чекає значення змінної. Правильний спосіб зайняти очікування:

while (my_variable != what_i_want)
    cpu_relax();

Виклик cpu_relax () може знижувати споживання енергії процесора або поступатися подвійному процесору з гіперпотоком; це також служить бар'єром пам’яті, тому, знову ж таки, мінливі зайві. Звичайно, очікування на зайнятість - це, як правило, антисоціальний акт, для початку.

Є ще кілька рідкісних ситуацій, коли непостійний має сенс у ядрі:

  • Вищезазначені функції аксесуарів можуть використовувати мінливі в архітектурах, де працює прямий доступ до пам'яті вводу / виводу. По суті, кожен виклик доступу стає самостійно критичним розділом і забезпечує доступ до нього так, як очікував програміст.

  • Вбудований код складання, який змінює пам'ять, але не має інших видимих ​​побічних ефектів, ризикує видалити GCC. Додавання мінливого ключового слова до операторів ASM запобігає цьому видаленню.

  • Змінна jiffies особлива тим, що вона може мати різне значення кожного разу, коли вона посилається, але її можна читати без спеціального блокування. Таким чином, джиффи можуть бути мінливими, але додавання інших змінних цього типу сильно нахмуриться. У цьому плані Джиффі вважається проблемою "дурної спадщини" (слова Лінуса); виправити це було б більше проблем, ніж варто.

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

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


3
@curiousguy: Так. Дивіться також gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Себастьян Мах

1
Spin_lock () виглядає як звичайний виклик функції. У цьому особливість полягає в тому, що компілятор буде поводитися з ним спеціально, щоб згенерований код "забув" будь-яке значення shared_data, прочитане перед spin_lock () і збережене в реєстрі, так що значення потрібно прочитати заново в do_something_on () після spin_lock ()?
Синкопійовано

1
@underscore_d Моя думка полягає в тому, що я не можу сказати з назви функції spin_lock (), що вона робить щось особливе. Я не знаю, що в ньому. Зокрема, я не знаю, що в реалізації, що заважає компілятору оптимізувати подальші читання.
Синкопат

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

1
@Tuntable: приватну статику можна торкатися будь-яким кодом за допомогою вказівника. І його адреса приймається. Можливо, аналіз потоку даних здатний довести, що вказівник ніколи не уникає, але це взагалі дуже складна проблема, суперлінійна за розміром програми. Якщо у вас є спосіб гарантувати відсутність псевдонімів, то переміщення доступу через фіксований замок насправді має бути нормальним. Але якщо псевдонімів немає, volatileтакож безглуздо. У всіх випадках поведінка "виклик функції, тіло якої неможливо побачити" буде правильною.
Ben Voigt

11

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

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

Таким чином, мінливі відмінно, якщо ви пам’ятаєте, що «спостерігаються» зміни мінливої ​​змінної можуть не відбутися в той час, коли ви думаєте, що вони відбудуться. Зокрема, не намагайтеся використовувати мінливі змінні як спосіб синхронізувати чи замовляти операції в потоках, оскільки це не буде надійно працювати.

Особисто моє основне (єдине?) Використання для мінливого прапора - це булевий "pleaseGoAwayNow". Якщо у мене є робоча нитка, яка циклічно триває, я змушу її перевірити мінливий булевий цикл на кожній ітерації циклу та вийти, якщо булеві колись справдиться. Потім основний потік може безпечно очистити робочий потік, встановивши булеве значення true, а потім викликаючи pthread_join (), щоб зачекати, поки робоча нитка не піде.


2
Ваш булевий прапор, ймовірно, небезпечний. Як ви гарантуєте, що працівник виконає своє завдання, і що прапор залишиться в обсягах, поки його не прочитають (якщо він прочитаний)? Це робота для сигналів. Летючий хороший для реалізації простих відкруток, якщо не задіяний мютекс , оскільки псевдонім безпеки означає, що компілятор припускає mutex_lock(і будь-яку іншу функцію бібліотеки) може змінювати стан змінної прапора.
Potatoswatter

6
Очевидно, що це працює лише в тому випадку, якщо характер рутинної робочої нитки такий, що гарантовано періодично перевіряти булеві. Гарантований прапор-bool-bool залишається в області застосування, оскільки послідовність відключення потоку завжди відбувається перед тим, як об'єкт, який утримує летучий-булевий, знищується, а послідовність відключення потоку викликає pthread_join () після встановлення bool. pthread_join () заблокує, поки робоча нитка не піде. У сигналів є свої проблеми, особливо, коли вони використовуються в поєднанні з багатопотоковою читанням.
Джеремі Фріснер

2
Робоча нитка не гарантовано завершить свою роботу до того, як булева буде істинна - насправді вона майже напевно опиниться в середині робочого підрозділу, коли bool буде встановлений на true. Але не має значення, коли робоча нитка завершує свій робочий блок, тому що головна нитка не буде робити нічого, крім блокування всередині pthread_join () до тих пір, поки робоча нитка не вийде, в будь-якому випадку. Таким чином, послідовність відключення добре впорядкована - мінливий bool (та будь-які інші спільні дані) не будуть звільнені, поки після повернення pthread_join (), а pthread_join () не повернеться, поки робоча нитка не піде.
Джеремі Фріснер

10
@ Jeremy, ти правдивий на практиці, але теоретично це все одно може зламатися. У двох основних системах одне ядро ​​постійно виконує вашу робочу нитку. Інше ядро ​​встановлює bool істинним. Однак немає гарантії, що ядро ​​робочої нитки коли-небудь побачить цю зміну, тобто воно ніколи не зупиниться, навіть якщо повторно перевіряє bool. Така поведінка дозволена моделями пам'яті c ++ 0x, java та c #. На практиці цього ніколи не відбудеться, оскільки зайнятий потік, швидше за все, десь вставить бар'єр пам’яті, після чого він побачить зміну bool.
deft_code

4
Візьміть систему POSIX, використовуйте політику планування в режимі реального часу SCHED_FIFO, вищий статичний пріоритет, ніж інші процеси / потоки в системі, достатньо ядер, повинен бути ідеально можливим. У Linux можна вказати, що процес у режимі реального часу може використовувати 100% часу процесора. Вони ніколи не будуть перемикатися в контексті, якщо немає потоку / процесу вищого пріоритету і ніколи не блокуються введенням-виведенням. Але справа в тому, що C / C ++ volatileне призначений для забезпечення належної семантики обміну / синхронізації даних. Мені здається, що я шукаю спеціальні випадки, щоб довести, що неправильний код, який іноді може спрацювати, є марною вправою.
FooF

7

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

Типовим способом багатопотокового програмування є не захист кожної загальної змінної на машинному рівні, а навпаки, введення змінних охоронців, які керують потоком програми. Замість volatile bool my_shared_flag;вас слід було

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Мало того, що це інкапсулює "важку частину", це принципово необхідно: C не включає атомні операції, необхідні для реалізації мютексу; він volatileповинен лише дати додаткові гарантії щодо звичайних операцій.

Тепер у вас є щось подібне:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag не потрібно бути непостійним, незважаючи на те, що він не може бути керованим, тому що

  1. Інший потік має доступ до нього.
  2. Значення посилання на нього, можливо, було прийнято колись (з & оператором).
    • (Або було зроблено посилання на містить структуру)
  3. pthread_mutex_lock - це функція бібліотеки.
  4. Це означає, що компілятор не може визначити, pthread_mutex_lockчи отримує це посилання якось.
  5. Значення компілятор повинен вважати, що pthread_mutex_lockмодифікує загальний прапор !
  6. Отже змінна повинна бути перезавантажена з пам'яті. volatile, хоча змістовне в цьому контексті є стороннім.

6

Ваше розуміння дійсно неправильне.

Властивість, яку мають мінливі змінні, - це "зчитування та запис у цю змінну - частина сприйнятої поведінки програми". Це означає, що ця програма працює (з урахуванням відповідного обладнання):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

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

Наприклад, безпечним для потоків лічильником буде просто (код, схожий на Linux-ядро, не знаю еквівалента c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

Це атомно, без бар'єру пам’яті. Ви повинні додати їх у разі потреби. Додавання змінних, ймовірно, не допоможе, оскільки не пов’язуватиме доступ до коду, що знаходиться поруч (наприклад, до додавання елемента до списку, на який лічильник рахується). Звичайно, вам не потрібно бачити лічильник, який збільшується поза вашою програмою, і оптимізація все ще бажана, наприклад.

atomic_inc(&counter);
atomic_inc(&counter);

ще можна оптимізувати до

atomically {
  counter+=2;
}

якщо оптимізатор досить розумний (він не змінює семантику коду).


6

Щоб ваші дані були узгодженими в одночасному середовищі, вам потрібно застосувати дві умови:

1) Атомність, тобто якщо я читаю або записую деякі дані в пам'ять, то ці дані читаються / записуються одним проходом і не можуть бути перервані або спростовані, наприклад, через контекстний комутатор

2) Послідовність, тобто порядок читання / запису операцій, слід бачити однаковий між кількома одночасними середовищами - будь то потоки, машини тощо

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

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

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

c # і java AFAIK виправляють це, роблячи мінливі дотримання 1) і 2), однак цього не можна сказати для компіляторів c / c ++, так що в основному це робити з вами, як ви вважаєте за потрібне.

Для дещо більш глибокої (хоча і не неупередженої) дискусії з цього питання читайте це


3
+1 - гарантована атомність була ще однією частиною того, чого я бракував. Я припускав, що завантаження int є атомним, так що мінливі перешкоди переупорядкуванню забезпечують повне рішення на стороні читання. Я думаю, що це гідне припущення для більшості архітектур, але це не гарантія.
Майкл Екстранд

Коли окремі читання та запису в пам'ять перериваються та безотомні? Чи є якась користь?
батбрат

5

Поширені запитання comp.programming.threads мають класичне пояснення Дейва Бутенгофа:

Q56: Чому мені не потрібно оголошувати загальні змінні VOLATILE?

Мене, однак, турбують випадки, коли і компілятор, і бібліотека потоків виконують свої відповідні характеристики. Відповідний компілятор C може глобально виділити деяку спільну (енергонезалежну) змінну до реєстру, який зберігається та відновлюється, коли процесор передається від потоку до потоку. Кожен потік матиме своє приватне значення для цієї загальної змінної, що не є тим, чого ми хочемо від загальної змінної.

У деякому сенсі це вірно, якщо компілятор достатньо знає про відповідні області змінної та функції pthread_cond_wait (або pthread_mutex_lock). На практиці більшість компіляторів не намагатиметься зберігати регістрові копії глобальних даних під час виклику до зовнішньої функції, оскільки надто важко знати, чи може звичайна програма якось мати доступ до адреси даних.

Так так, це правда, що компілятор, який суворо (але дуже агресивно) відповідає ANSI C, може не працювати з декількома потоками без змін. Але комусь краще це виправити. Оскільки будь-яка СИСТЕМА (тобто прагматично комбінація ядра, бібліотек та компілятора C), яка не забезпечує гарантії когерентності пам’яті POSIX, не відповідає стандарту POSIX. Період. Система CANNOT вимагає використання нестабільних на спільних змінних для правильної поведінки, оскільки POSIX вимагає лише необхідності функцій синхронізації POSIX.

Тож якщо ваша програма перервана через те, що ви не використовували енергонезалежних, це БУГ. Це може бути не помилка в C, або помилка в бібліотеці ниток або помилка в ядрі. Але це помилка SYSTEM, і над її виправленням доведеться працювати один або кілька таких компонентів.

Ви не хочете використовувати енергонезалежних, тому що в будь-якій системі, де це має значення, це буде значно дорожче, ніж правильна енергонезалежна змінна. (ANSI C вимагає "точок послідовності" для мінливих змінних у кожному виразі, тоді як POSIX вимагає їх лише під час операцій синхронізації. Обчислювальна потокова програма буде бачити значно більше активності пам'яті, використовуючи летючі, і, зрештою, саме активність в пам'яті дійсно сповільнює вас.)

/ --- [Дейв Бутенхоф] ----------------------- [butenhof@zko.dec.com] --- \
| Корпорація цифрового обладнання 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Краще життя за рахунок валюти] ---------------- /

Г-н Butenhof охоплює майже все те ж саме, що і в цій публікації про мережу :

Використання "летючого" недостатньо для забезпечення належної видимості пам'яті або синхронізації між потоками. Використання mutex є достатнім, і, за винятком застосування інших альтернативних машинних кодних альтернатив, (або більш тонких наслідків правил пам'яті POSIX, які набагато складніше застосовувати загалом, як пояснено в попередньому дописі), mutex НЕОБХІДНО.

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

Все, що однаково стосується C ++.


Ланка розірвана; це, здається, не вказує на те, що ви хотіли цитувати. Без тексту його різновид безглуздої відповіді.
jww

3

Це все, що робить "непостійний": "Ей, компілятор, ця змінна може змінюватися В ЯКІЙ МОМЕНТ (на будь-якій галочці годинника), навіть якщо на неї НЕ МІСЦЕ МІСЦЕВИХ ВКАЗІВ. Не кешуйте це значення в регістрі."

Це ІТ. Він повідомляє компілятору, що ваше значення є, таким чином, непостійним - це значення може бути змінено в будь-який момент зовнішньою логікою (інший потік, інший процес, ядро ​​тощо). Він існує більш-менш виключно для придушення оптимізацій компілятора, які мовчки кешуватимуть значення в реєстрі, яке воно по суті є небезпечним для кешу EVER.

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


3

Згідно з моїм старим стандартом C, "те, що являє собою доступ до об'єкта, який має тип, що не змінюється, визначається реалізацією" . Тож письменники-компілятори C могли обрати "непостійний" середній "безпечний доступ до потоку в багатопроцесорному середовищі" . Але вони цього не зробили.

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

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


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

На більшості платформ було б досить легко розпізнати, що volatileпотрібно зробити, щоб дозволити писати ОС таким чином, що не залежить від апаратури, але не залежить від компілятора. Вимога програмістів використовувати функції, що залежать від впровадження, а не робити volatileроботу за потребою, підриває мету мати стандарт.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.