Шаблоны использования функций InitOnce

Поскольку от написания кода без использования блокировок может начать болеть голова, вам, вероятно, имеет смысл переложить эту обязанность на каких-нибудь других людей, чтобы голова болела у них. И такими людьми являются парни из команды работки ядра Windows, которые написали довольно много готовых программных компонентов, не использующих блокировки, которые теперь не нужно разрабатывать вам. Среди них, к примеру, есть набор функций для работы с неблокирующими потокобезопасными списками. Но сегодня мы рассмотрим функции однократной инициализации.

На самом деле, в наиболее простых функциях однократной инициализации блокировки используются, но при этом они реализованы по шаблону блокировки с двойной проверкой, что позволяет вам не заботиться об этих деталях. Алгоритм использования функции InitOnceExecuteOnce является довольно простым. Вот самый примитивный вариант его использования:

int SomeGlobalInteger; 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<Thing*>(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 < csi; iio++) {       InitOnceInitialize(&m_rgio[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,                               X&fPending, &pv);     }   }   return static_cast<ITEMCONTROLLER *>(pv); }

Таким образом, шаблон для асинхронной инициализации состоит из следующих шагов:

  • Вызовите функцию InitOnceBeginInitialize в асинхронном режиме.
  • Если она вернет fPending == FALSE, значит инициализация уже была произведена и вы можете использовать результат, переданный в качестве последнего параметра.
  • В противном случае инициализация еще не выполнялась (или еще не завершилась). Выполните свою инициализацию, но помните, что поскольку это алгоритм без использования блокировок, может оказаться, что эту инициализацию в данный момент выполняют еще несколько других потоков, следовательно, вам нужно быть очень осторожными с операциями над объектами, хранящими глобальное состояние. Этот шаблон работает лучше всего, когда инициализация реализована в виде создания нового объекта (потому что в этом случае, если несколько потоков будут выполнять инициализацию, каждый из них будет создавать отдельный независимый объект).
  • Вызовите InitOnceComplete с результатами вашей инициализации.
  • Если выполнение функции InitOnceComplete завершится успешно, значит вы выиграли гонку и на этом процедура инициализации закончена.
  • Если выполнение функции InitOnceComplete закончится неудачей, значит вы проиграли инициализационную гонку и должны отменить результаты вашей неуспешной инициализации. Также в этом случае вам нужно вызвать InitOnceBeginInitialize еще один, последний раз, для того, чтобы получить результат инициализации, выполненной в потоке, который выиграл гонку.

Несмотря на то, что описание алгоритма довольно объемно, с концептуальной точки зрения он довольно прост. По крайней мере, теперь он написан в форме пошаговой инструкции.

Упражнение: что будет, если вместо вызова InitOnceComplete со значением INIT_ONCE_INIT_FAILED функция просто вернет управление без завершения однократной инициализации?

Упражнение: что будет, если два потока попытаются выполнить асинхронную инициализацию и поток, который первым дойдет до конечного этапа инициализации, потерпит неудачу?

Упражнение: объедините результаты двух предыдущих упражнений и предположите, что получится в итоге.