Initonce - блог Реймонда Чена (переклад)

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

Насправді, в найбільш простих функціях одноразової ініціалізації блокування використовуються, але при цьому вони реалізовані за шаблоном блокування з подвійною перевіркою, що дозволяє вам не турбуватися про ці деталі. Алгоритм використання функції InitOnceExecuteOnce є досить простим. Ось найпримітивніший варіант його використання:

BOOL CALLBACK ThisRunsAtMostOnce (
PINIT_ONCE initOnce,
PVOID Parameter,
PVOID * Context)
calculate_an_integer (SomeGlobalInteger);
return TRUE;
>

void InitializeThatGlobalInteger ()
static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
InitOnceExecuteOnce (initOnce,
ThisRunsAtMostOnce,
nullptr, nullptr);
>

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

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

BOOL CALLBACK ThisSucceedsAtMostOnce (
PINIT_ONCE initOnce,
PVOID Parameter,
PVOID * Context)
return SUCCEEDED (calculate_an_integer (SomeGlobalInteger));
>

BOOL TryToInitializeThatGlobalInteger ()
static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
return InitOnceExecuteOnce (initOnce,
ThisSucceedsAtMostOnce,
nullptr, nullptr);
>

Якщо ваша ініціалізації функція поверне FALSE, то ініціалізація буде вважатися неуспішною і коли наступного разу хтось викличе функцію InitOnceExecuteOnce, вона знову спробує виконати ініціалізацію.
Ще трішки більше цікавий варіант використання функції InitOnceExecuteOnce бере до уваги параметр Context. Хлопці з команди розробки ядра Windows помітили, що структура INIT_ONCE в стані «проініціалізувати» містить безліч невикористовуваних бітів, і вони запропонували вам використовувати їх для власних потреб. Це досить зручно в тому випадку, коли те, що ви ініціалізіруете, є покажчиком на об'єкт C ++, тому що це означає, що тепер вам потрібно піклуватися лише про одну річ замість двох.

BOOL CALLBACK AllocateAndInitializeTheThing (
PINIT_ONCE initOnce,
PVOID Parameter,
PVOID * Context)
* Context = new (nothrow) Thing ();
return * Context! = nullptr;
>

Thing * GetSingletonThing (int arg1, int arg2)
static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
void * Result;
if (InitOnceExecuteOnce (initOnce,
AllocateAndInitializeTheThing,
nullptr, Result))
return static_cast(Result);
>
return nullptr;
>

Останній параметр функції InitOnceExecuteOnce приймає «чарівні» дані, за розміром майже ідентичні вказівником, які функція запам'ятає для вас. Потім ваша функція зворотного виклику передає ці «чарівні» дані назад, через параметр Context, а функція InitOnceExecuteOnce повертає їх вам у вигляді параметра Result.
Як і в попередньому випадку, якщо два потоки викличуть функцію InitOnceExecuteOnce одночасно, використовуючи неініціалізованих структуру INIT_ONCE, один з них викличе функцію ініціалізації, а інший потік буде припинений.

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

Набагато цікавіше асинхронний шаблон. Ось приклад такого шаблону стосовно нашого завдання з класом SingletonManager:

SingletonManager (const SINGLETONINFO * rgsi, UINT csi)
. m_rgsi (rgsi), m_csi (csi),
m_rgio (new INITONCE [csi]) for (UINT iio = 0; iio >
>
.
// Масив, що описує створені об'єкти
// об'єкти в цьому масиві розташовані паралельно елементам масиву m_rgsi
INIT_ONCE * m_rgio;
>;

ITEMCONTROLLER * SingletonManager :: Lookup (DWORD dwId)
. все точно так же, як і в попередньому варіанті, аж до того місця,
де починається реалізація шаблону «singleton-конструктор»

void * pv = NULL;
BOOL fPending;
if (! InitOnceBeginInitialize (m_rgio [i], INIT_ONCE_ASYNC,
fPending, pv)) return NULL;

if (fPending) ITEMCONTROLLER * pic = m_rgsi [i] .pfnCreateController ();
DWORD dwResult = pic. 0. INIT_ONCE_INIT_FAILED;
if (InitOnceComplete (m_rgio [i],
INIT_ONCE_ASYNC | dwResult, pic)) pv = pic;
> Else // програв в гонці - тепер знищ непотрібну копію і отримай результат переможця
delete pic;
InitOnceBeginInitialize (m_rgio [i], INIT_ONCE_CHECK_ONLY,
XfPending, pv);
>
>
return static_cast(Pv);
>

Таким чином, шаблон для асинхронної ініціалізації складається з наступних кроків:

  • Викличте функцію InitOnceBeginInitialize в асинхронному режимі.
  • Якщо вона поверне fPending == FALSE, значить ініціалізація вже була проведена і ви можете використовувати результат, переданий в якості останнього параметра.
  • В іншому випадку ініціалізація ще не виконувалася (або ще не завершилася). Виконайте свою ініціалізацію, але пам'ятайте, що оскільки це алгоритм без використання блокувань, може виявитися, що цю ініціалізацію в даний момент виконують ще кілька інших потоків, отже, вам потрібно бути дуже обережними з операціями над об'єктами, які зберігають глобальне стан. Цей шаблон працює найкраще, коли ініціалізація реалізована у вигляді створення нового об'єкта (бо в цьому випадку, якщо кілька потоків будуть виконувати ініціалізацію, кожен з них буде створювати окремий незалежний об'єкт).
  • Викличте InitOnceComplete з результатами вашої ініціалізації.
  • Якщо виконання функції InitOnceComplete завершиться успішно, значить ви виграли гонку і на цьому процедура ініціалізації закінчена.
  • Якщо виконання функції InitOnceComplete закінчиться невдачею, значить ви програли ініціалізацій гонку і повинні скасувати результати вашої неуспішною ініціалізації. Також в цьому випадку вам потрібно викликати InitOnceBeginInitialize ще один, останній раз, для того, щоб отримати результат ініціалізації, виконаної в потоці, який виграв гонку.

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

Вправа. що буде, якщо замість виклику InitOnceComplete зі значенням INIT_ONCE_INIT_FAILED функція просто поверне управління без завершення одноразової ініціалізації?

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

Вправа. об'єднайте результати двох попередніх вправ і припустити, що вийде в результаті.