Поскольку от написания кода без использования блокировок может начать болеть голова, вам, вероятно, имеет смысл переложить эту обязанность на каких-нибудь других людей, чтобы голова болела у них. И такими людьми являются парни из команды работки ядра 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); }
Таким образом, шаблон для асинхронной инициализации состоит из следующих шагов:
Несмотря на то, что описание алгоритма довольно объемно, с концептуальной точки зрения он довольно прост. По крайней мере, теперь он написан в форме пошаговой инструкции.
Упражнение: что будет, если вместо вызова InitOnceComplete со значением INIT_ONCE_INIT_FAILED функция просто вернет управление без завершения однократной инициализации?
Упражнение: что будет, если два потока попытаются выполнить асинхронную инициализацию и поток, который первым дойдет до конечного этапа инициализации, потерпит неудачу?
Упражнение: объедините результаты двух предыдущих упражнений и предположите, что получится в итоге.