Windows и C++
Эволюция синхронизации в Windows и C++
Когда я впервые занялся написанием ПО с параллельной обработкой, в C++ не было поддержки синхронизации. В самой Windows был лишь набор синхронизирующих примитивов, и все они были реализованы в ядре. В основном я использовал критические секции, если не возникало потребности в синхронизации между процессами, и тогда я применял мьютекс. В общих чертах, эти синхронизирующие примитивы были блокировками, или блокирующими объектами (lock objects).
Термин «мьютекс» (mutex) взят из концепции взаимоисключения (mutual exclusion); это еще одно название синхронизации. Мьютекс гарантирует, что в данный момент только один поток может получить доступ к некоему ресурсу. Название «критическая секция» связано с тем, что к такому ресурсу может обращаться только определенная секция кода. Чтобы обеспечить корректность, единовременно эту критическую секцию кода может выполнять только один поток. Эти два блокирующих объекта имеют разные возможности, но полезно помнить, что это всего лишь блокировки, оба гарантируют взаимоисключение и оба могут быть использованы для демаркации критических секций кода.
Сегодня ландшафт синхронизации изменился кардинально. Программисту на C++ предоставляется обширный выбор. Теперь Windows поддерживает намного больше синхронизирующих функций, а сам C++ наконец-то обладает интересным набором средств параллельной обработки и синхронизации для тех, кто использует компилятор, поддерживающий стандарт C++11.
В этой статье я намерен исследовать состояние синхронизации в Windows и C++ на данный момент. Начну с обзора синхронизирующих примитивов, предоставляемых самой Windows, а затем рассмотрю альтернативы, предлагаемые Standard C++ Library. Если вам важна портируемость, тогда новые библиотечные дополнения C++ будут для вас очень привлекательны. Однако, если портируемость для вас не столь значима и на первом месте стоит производительность, в таком случае гораздо важнее изучение того, что предлагается теперь операционной системой Windows. Давайте приступим прямо сейчас.
Критическая секция
Первым у нас будет объект «критическая секция». Эта блокировка интенсивно используется в бесчисленном множестве приложений, но имеет плохую репутацию. Когда я впервые начал использовать критические секции, они были по-настоящему простыми. Чтобы создать такую блокировку, достаточно было создать структуру CRITICAL_SECTION и вызвать функцию InitializeCriticalSection, чтобы подготовить эту структуру к использованию. Эта функция не возвращала никаких значений, и тем самым подразумевалось, что она не может завершиться неудачей. Однако в те дни этой функции требовалось создавать различные системные ресурсы, в том числе объект события режима ядра, и существовала вероятность, что в ситуациях с острой нехваткой памяти это не удастся, что приведет к появлению структурного исключения (structured exception). Тем не менее, эта вероятность была весьма мала, и большинство разработчиков игнорировало ее.
С распространением COM использование критических секций резко расширилось, потому что во многих COM-классах критические секции применялись для синхронизации, но в большинстве случаев конкуренции либо не было, либо она была очень низка. С появлением многоядерных процессоров внутреннее событие критической секции стало использоваться гораздо реже, так как критическая секция короткое время «крутилась» в цикле в пользовательском режиме, ожидая захвата блокировки. Малое число кручений в цикле означало, что во многих случаях кратковременной конкуренции можно было бы избавиться от перехода в режим ядра, что значительно повысило бы производительность.
Примерно в то же время некоторые разработчики ядра осознали, что они могли бы радикально улучшить масштабируемость Windows, если бы у них была возможность откладывать создание объектов событий критической секции до тех пор, пока их присутствие не будет оправдано нарастанием конкуренции. Идея казалась неплохой, пока эти разработчики не поняли, что, хотя InitializeCriticalSection теперь вряд ли могла бы завершиться неудачей, функция EnterCriticalSection (используемая для ожидания захвата блокировки) больше не является надежной. Игнорировать это было нельзя, потому что иначе появилось бы множество сбойных ситуаций, которые практически не позволили бы критическим секциям работать корректно, а это разрушило бы бесчисленную массу приложений. Тем не менее, преимущества масштабируемости были достаточно очевидны.
В конце концов, один из разработчиков ядра пришел к решению в форме нового, недокументированного объекта события режима ядра, который был назван событием с ключом (keyed event). Вы можете почитать о нем в книге «Windows Internals» за авторством Марка Руссиновича (Mark Russinovich), Дэвида Соломона (David Solomon) и Алекса Ионеску (Alex Ionescu) (Microsoft Press, 2012), но, если в двух словах, вместо захвата объекта события для каждой критической секции можно было бы использовать единственное событие с ключом для всех критических секций в системе. Это работало, потому что объект события с ключом был именно таков: он полагается на ключ, который является просто идентификатором с размером указателя, естественным образом локальным для адресного пространства.
Конечно, было искушение обновить критические секции так, чтобы они использовали только события с ключом, но из-за того, что многие отладчики и другие инструменты опирались на внутреннее устройство критических секций, к событию с ключом прибегали только как к последнему убежищу, если ядру не удалось создать обычный объект события.
Это может показаться историей, полной противоречий, но в течение цикла разработки Windows Vista работа событий с ключом была значительно улучшена, и это привело к введению совершенно нового блокирующего объекта, который был и проще, и быстрее, но об этом я расскажу немного позже.
Так как объект критической секции теперь защищен от сбоя из-за нехватки памяти, он стал прямолинеен в использовании. Простая оболочка показана на рис. 1.
Рис. 1. Блокировка «критическая секция»
class lock
{
CRITICAL_SECTION h;
lock(lock const &);
lock const & operator=(lock const &);
public:
lock()
{
InitializeCriticalSection(&h);
}
~lock()
{
DeleteCriticalSection(&h);
}
void enter()
{
EnterCriticalSection(&h);
}
bool try_enter()
{
return 0 != TryEnterCriticalSection(&h);
}
void exit()
{
LeaveCriticalSection(&h);
}
CRITICAL_SECTION * handle()
{
return &h;
}
};
Уже упоминавшаяся функция EnterCriticalSection дополнена функцией TryEnterCriticalSection, которая является неблокирующей альтернативой. Функция LeaveCriticalSection освобождает блокировку, а DeleteCriticalSection — любые ресурсы ядра, которые могли быть попутно выделены.
Поэтому критическая секция является вполне разумным выбором. Она работает весьма хорошо, так как старается избегать переходов в режим ядра и выделения ресурсов. Тем не менее, ей приходится сохранять немалый груз, связанный с историей ее развития и необходимый для совместимости с приложениями.
Мьютекс
Мьютекс является истинным синхронизирующим объектом режима ядра. В отличие от критических секций блокировка «мьютекс» всегда использует ресурсы, выделяемые ядром. Его преимущество, конечно же, заключается в том, что ядро в состоянии обеспечить синхронизацию между процессами благодаря своему знанию этой блокировки. Как объект ядра он предоставляет обычные атрибуты (в частности, имя), которые можно использовать для открытия этого объекта из других процессов или просто для идентификации блокировки в отладчике. Кроме того, вы можете указывать маску доступа для ограничения доступа к этому объекту. В качестве внутрипроцессной блокировки данный объект избыточен, несколько сложнее в использовании и намного медленнее. На рис. 2 приведена простая оболочка для безымянного мьютекса, который является локальным для процесса.
Рис. 2. Блокировка «мьютекс»
#ifdef _DEBUG
#include <crtdbg.h>
#define ASSERT(expression) _ASSERTE(expression)
#define VERIFY(expression) ASSERT(expression)
#define VERIFY_(expected, expression) ASSERT(expected == expression)
#else
#define ASSERT(expression) ((void)0)
#define VERIFY(expression) (expression)
#define VERIFY_(expected, expression) (expression)
#endif
class lock
{
HANDLE h;
lock(lock const &);
lock const & operator=(lock const &);
public:
lock() :
h(CreateMutex(nullptr, false, nullptr))
{
ASSERT(h);
}
~lock()
{
VERIFY(CloseHandle(h));
}
void enter()
{
VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
}
bool try_enter()
{
return WAIT_OBJECT_0 == WaitForSingleObject(h, 0);
}
void exit()
{
VERIFY(ReleaseMutex(h));
}
HANDLE handle()
{
return h;
}
};
Функция CreateMutex создает блокировку, а общая функция CloseHandle закрывает описатель процесса, что в конечном счете уменьшает счетчик ссылок блокировки в ядре. Ожидание захвата блокировки осуществляется универсальной функцией WaitForSingleObject, которая проверяет (и при необходимости позволяет ожидать) переход в свободное состояние (signaled state) множества объектов ядра. Ее второй параметр указывает, сколько времени должен блокироваться вызвавший поток, ожидая захвата блокировки. Константа INFINITE (что не удивительно) означает бесконечное ожидание, тогда как нулевое значение вообще исключает ожидание потока, и захват блокировки будет происходить, только если она свободна. Наконец, функция ReleaseMutex освобождает блокировку.
Блокировка «мьютекс» обладает широкими возможностями, но за них приходиться расплачиваться производительностью и усложнением. Оболочка на рис. 2 утыкана выражениями проверки (assertions), указывающими возможные сбойные ситуации, но в большинстве случаев блокировка «мьютекс» отметается прежде всего по соображениям производительности.
Событие
Прежде чем рассказывать о высокопроизводительной блокировке, мне нужно ознакомить вас еще с одним синхронизирующим объектом ядра, который уже был упомянут. Хотя на деле это не блокировка (в том плане, что он не предоставляет механизма для прямой реализации взаимоисключения), объект события крайне важен для координации работы между потоками. По сути, это тот же объект, который на внутреннем уровне используется критической секцией, и, помимо этого, он полезен для эффективной и масштабируемой реализации всех видов шаблонов параллельной обработки.
Функция CreateEvent создает событие, а функция CloseHandle, как и в случае мьютекса, освобождает этот объект в ядре. Поскольку на самом деле это не блокировка, у этого объекта нет семантики захвата и освобождения. Скорее это именно то воплощение функциональности перевода в свободное/занятое состояние, которое предоставляется многими объектами ядра. Чтобы разобраться, как работает эта функциональность, нужно понять, что объект событие может быть создан в одном из двух состояний. Если вы передаете TRUE во второй параметр CreateEvent, то получаете объект события, который называют событием со сбросом вручную (manual-reset event); в ином случае создается событие с автоматическим сбросом (auto-reset event). Событие со сбросом вручную требует от вас самостоятельно устанавливать и сбрасывать состояние объекта (свободен/занят). Для этой цели предназначены функции SetEvent и ResetEvent. Событие с автоматическим сбросом само сбрасывается в исходное состояние (переходит из свободного [signaled] в занятое состояние [nonsignaled]) при освобождении ожидающего потока. Поэтому такое событие полезно, когда один из потоков должен координировать свои действия с еще одним потоком, тогда как событие со сбросом вручную удобно, когда один из потоков должен координировать свои действия с любым количеством других потоков. Вызов SetEvent для события с автоматическим сбросом освободит максимум один поток, а в случае события со сбросом вручную этот вызов освободит все ожидающие потоки. Подобно мьютексу ожидание перехода события в свободное состояние осуществляется через функцию WaitForSingleObject. На рис. 3 приведена простая оболочка безымянного события, которое может быть сконструировано в любом из двух режимов.
Рис. 3. Событие
class event
{
HANDLE h;
event(event const &);
event const & operator=(event const &);
public:
explicit event(bool manual = false) :
h(CreateEvent(nullptr, manual, false, nullptr))
{
ASSERT(h);
}
~event()
{
VERIFY(CloseHandle(h));
}
void set()
{
VERIFY(SetEvent(h));
}
void clear()
{
VERIFY(ResetEvent(h));
}
void wait()
{
VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
}
};
Тонкая блокировка, синхронизирующая доступ потоков-«читателей» и потоков-«писателей»
Тонкая блокировка, синхронизирующая доступ потоков-«читателей» и потоков-«писателей» (Slim Reader/Writer, SRW), может оказаться труднопроизносимым термином, но ключевое слово здесь — «тонкая». Программисты могут недооценивать эту блокировку из-за ее способности различать общих «читателей» (shared readers) и монопольных «писателей» (exclusive writers), полагая, что это перебор, когда им достаточно критической секции. Однако на деле это простейшая в работе блокировка и на данный момент самая быстрая; при этом для ее использования вовсе не обязательно иметь общих «читателей». Свою репутацию в быстродействии она заслужила не только потому, что полагается на эффективный объект события с ключом, но и потому, что она в основном реализована в пользовательском режиме и переключается в режим ядра лишь при достижении такого уровня конкуренции, что потоку лучше «уснуть». И вновь объекты «критическая секция» и «мьютекс» предоставляют дополнительную функциональность, которая может вам потребоваться, например поддержку рекурсии и блокировок между процессами, но гораздо чаще достаточно быстрой и облегченной блокировки для внутреннего использования.
Эта блокировка опирается на уже упомянутые события с ключом и, как таковая, является чрезвычайно облегченной и несмотря на это предоставляет большой объем функциональности. Блокировка SRW требует памяти размером с указатель, которая выделяется вызывающим процессом, а не ядром. По этой причине инициализирующая функция InitializeSRWLock не может завершиться неудачей и просто обеспечивает, чтобы эта блокировка содержала подходящий битовый шаблон перед последующим использованием.
Ожидание захвата блокировки достигается использованием либо функции AcquireSRWLockExclusive для так называемой блокировки «писатель», либо функции AcquireSRWLockShared для блокировки «читатели». Однако термины «монопольный» и «общий» подходят больше. Для обоих режимов (монопольный/общий) имеются соответствующие функции освобождения и попытки захвата. На рис. 4 показана простая оболочка для блокировки SRW монопольного режима. Вам не составит труда самостоятельно добавить функции общего режима, если в том возникнет нужда. Но заметьте, что деструктора нет, так как нет ресурсов, которые нужно было бы освобождать.
Рис. 4. Блокировка SRW
class lock
{
SRWLOCK h;
lock(lock const &);
lock const & operator=(lock const &);
public:
lock()
{
InitializeSRWLock(&h);
}
void enter()
{
AcquireSRWLockExclusive(&h);
}
bool try_enter()
{
return 0 != TryAcquireSRWLockExclusive(&h);
}
void exit()
{
ReleaseSRWLockExclusive(&h);
}
SRWLOCK * handle()
{
return &h;
}
};
Условная переменная
Последний синхронизирующий объект, с которым мне нужно ознакомить вас, — условная переменная (condition variable). Вероятно, это один из тех синхронизирующих объектов, которые неизвестны большинству программистов. Однако в последние месяцы я заметил возрождение интереса к условным переменным. Скорее всего это как-то связано с появлением C++11, но идея не нова и поддержка этой концепции в Windows существует уже некоторое время. Действительно Microsoft .NET Framework поддерживает шаблон условной переменной с момента первого выпуска, правда, эта поддержка включена в класс Monitor, что в некоторых отношениях ограничивает ее полезность. Но возрождение интереса к этому объекту также вызвано тем, что события с ключом позволили ввести условные переменные в Windows Vista, и с тех пор они только совершенствовались. Хотя условная переменная — это просто шаблон параллельной обработки и в связи с этим может быть реализована на основе других примитивов, ее включение в ОС означает, что она способна работать чрезвычайно быстро и освободить программиста от необходимости самому обеспечивать корректность такого кода. Действительно, если вы применяете синхронизирующие примитивы ОС, то почти невозможно гарантировать корректность некоторых шаблонов параллельной обработки без помощи самой ОС.
Если задуматься, шаблон условной переменной довольно распространен. Программе нужно ждать выполнения некоего условия, прежде чем она сможет продолжить работу. Оценка этого условия включает захват блокировки для проверки какого-либо общего состояния. Однако, если условие еще не выполнено, блокировка должна быть освобождена, чтобы другой поток мог выполнить условие. Далее оценивающий поток должен ждать до тех пор, пока условие не будет выполнено, и только потом снова захватывать блокировку. Как только блокировка захвачена повторно, необходимо снова проверить условие, чтобы избежать усиления конкуренции. Реализовать это труднее, чем кажется, поскольку здесь, по сути, требуется позаботиться о других подводных камнях, а реализовать эффективно — еще сложнее. Следующий псевдокод иллюстрирует эту проблему:
lock-enter
while (!condition-eval)
{
lock-exit
condition-wait
lock-enter
}
// Здесь выполняем нужную работу
lock-exit
Но даже в этом псевдокоде скрыта трудноуловимая ошибка. Чтобы функция работала корректно, ожидание на условии должно осуществляться до выхода из блокировки, но в этом случае блокировка никогда не будет освобождена. Возможность атомарного освобождения одного объекта и ожидания на другом настолько важна, что в Windows предусмотрена функция SignalObjectAndWait, которая именно это и делает для определенных объектов ядра. Но, поскольку блокировка SRW, в основном находится в пользовательском режиме, требуется какое-то другое решение. И здесь мы приходим к условным переменным.
Как и блокировка SRW, условная переменная занимает единственный участок памяти размером с указатель и инициализируется функцией InitializeConditionVariable, которая никогда не завершается неудачей. По аналогии с SRW освобождение каких-либо ресурсов не требуется, поэтому, когда условная переменная больше не нужна, память может быть просто возвращена системе.
Поскольку само условие специфично для конкретной программы, написание шаблона в виде цикла while с единственным вызовом функции SleepConditionVariableSRW возлагается на вызывающую сторону. Эта функция автоматически освобождает блокировку SRW, ожидая пробуждения на момент выполнения условия. Также имеется соответствующая функция SleepConditionVariableCS, если вы хотите использовать условные переменные с критической секцией.
Функция WakeConditionVariable вызывается для пробуждения одного ждущего, или спящего, потока. Пробудившийся поток снова захватит блокировку перед возвратом управления. В качестве альтернативы можно использовать функцию WakeAllConditionVariable для пробуждения всех ждущих потоков. На рис. 5 показана простая оболочка с необходимым циклом while. Заметьте, что существует возможность непредсказуемого пробуждения спящего потока, и цикл while гарантирует, что условие всегда будет заново проверяться после повторного захвата блокировки потоком. Также важно отметить, что предикат всегда оценивается при удерживании блокировки.
Рис. 5. Условная переменная
class condition_variable
{
CONDITION_VARIABLE h;
condition_variable(condition_variable const &);
condition_variable const & operator=(condition_variable const &);
public:
condition_variable()
{
InitializeConditionVariable(&h);
}
template <typename T>
void wait_while(lock & x, T predicate)
{
while (predicate())
{
VERIFY(SleepConditionVariableSRW(&h, x.handle(), INFINITE, 0));
}
}
void wake_one()
{
WakeConditionVariable(&h);
}
void wake_all()
{
WakeAllConditionVariable(&h);
}
};
Блокирующая очередь
Для дальнейших пояснений я воспользуюсь блокирующей очередью (blocking queue) как примером. Позвольте подчеркнуть, что я в принципе не советую применять блокирующие очереди. Гораздо эффективнее использовать порт завершения ввода-вывода (I/O completion port) или пул потоков Windows, который является просто абстракцией этого порта, или даже класс concurrent_queue в Concurrency Runtime. В целом, предпочтительнее любые неблокирующие средства. Тем не менее, блокирующая очередь — простая в понимании концепция, и слишком многие разработчики считают ее полезной. Согласен, не в каждой программе требуется масштабирование, но каждая программа должна работать корректно. Блокирующая очередь предоставляет широкие возможности в применении синхронизации как для обеспечения корректности, так и для совершенно неправильного ее использования.
Рассмотрим реализацию блокирующей очереди на основе какой-нибудь блокировки и события. Блокировка защищает общую очередь, а событие уведомляет потребителя о том, что источник что-то поместил в очередь. На рис. 6 дан простой пример с использованием события с автоматическим сбросом. Я задействовал этот режим события потому, что метод push ставит в очередь только один элемент и я хочу, чтобы при этом пробуждался лишь один потребитель для его извлечения из очереди. Метод push захватывает блокировку, ставит элемент в очередь, а затем переводит событие в свободное состояние, чтобы пробудить любого из ждущих потребителей. Метод pop захватывает блокировку, а затем ждет, пока в очереди что-то не появится, прежде чем извлечь элемент и вернуть его. Оба метода используют класс lock_block. Для краткости я не включил его в этот код, но он просто вызывает в конструкторе метод enter блокировки, а в деструкторе — метод exit.
Рис. 6. Блокирующая очередь с автоматическим сбросом
template <typename T>
class blocking_queue
{
std::deque<T> q;
lock x;
event e;
blocking_queue(blocking_queue const &);
blocking_queue const & operator=(blocking_queue const &);
public:
blocking_queue()
{
}
void push(T const & value)
{
lock_block block(x);
q.push_back(value);
e.set();
}
T pop()
{
lock_block block(x);
while (q.empty())
{
x.exit(); e.wait(); // ошибка!
x.enter();
}
T v = q.front();
q.pop_front();
return v;
}
};
Но обратите внимание на весьма вероятную взаимоблокировку (deadlock) из-за того, что вызовы exit и wait не являются атомарными. Если бы блокировка была мьютексом, я мог бы использовать функцию SignalObjectAndWait, но тогда сильно пострадала бы производительность блокирующей очереди.
Другой вариант — применение события со сбросом вручную. Вместо перехода в свободное состояние всякий раз, когда в очередь ставится какой-либо элемент, просто определяем два состояния. Событие может находиться в свободном состоянии, пока в очереди есть элементы, и в занятом состоянии, если она пуста. Этот вариант будет работать гораздо лучше, так как потребует меньше вызовов ядра для перевода события в свободное состояние. Пример показан на рис. 7. Обратите внимание на то, как метод push устанавливает событие, если в очереди есть один элемент. Благодаря этому мы избегаем лишних вызовов функции SetEvent. Метод pop послушно очищает событие, если обнаруживает, что очередь пуста. Пока в очереди имеется несколько элементов, любое количество потребителей может извлекать элементы из очереди без участия объекта события, а это улучшает масштабируемость.
Рис. 7. Блокирующая очередь со сбросом вручную
template <typename T>
class blocking_queue
{
std::deque<T> q;
lock x;
event e;
blocking_queue(blocking_queue const &);
blocking_queue const & operator=(blocking_queue const &);
public:
blocking_queue() :
e(true) // вручную
{
}
void push(T const & value)
{
lock_block block(x);
q.push_back(value);
if (1 == q.size())
{
e.set();
}
}
T pop()
{
lock_block block(x);
while (q.empty())
{
x.exit();
e.wait();
x.enter();
}
T v = q.front();
q.pop_front();
if (q.empty())
{
e.clear();
}
return v;
}
};
В этом случае вероятности потенциальной взаимоблокировки в последовательности exit-wait-enter нет, так как другой потребитель не сможет украсть событие, учитывая, что это событие со сбросом вручную. Этот вариант гораздо производительнее. Тем не менее, альтернативное решение (и, возможно, более естественное) — использовать условную переменную вместо события. Это легко делается с помощью класса condition_variable на рис. 5 и подобно блокирующей очереди со сбросом вручную, но немного проще. Пример представлен на рис. 8. Заметьте, насколько четче стала семантика и ваши намерения в параллельной обработке благодаря применению более высокоуровневых синхронизирующих объектов. Эта четкость помогает избегать ошибок параллельной обработки, которым подвержен более туманный код.
Рис. 8. Блокирующая очередь с условной переменной
template <typename T>
class blocking_queue
{
std::deque<T> q;
lock x;
condition_variable cv;
blocking_queue(blocking_queue const &);
blocking_queue const & operator=(blocking_queue const &);
public:
blocking_queue()
{
}
void push(T const & value)
{
lock_block block(x);
q.push_back(value);
cv.wake_one();
}
T pop()
{
lock_block block(x);
cv.wait_while(x, [&]()
{
return q.empty();
});
T v = q.front();
q.pop_front();
return v;
}
};
Наконец, я должен упомянуть, что C++11 теперь предоставляет блокировку mutex, а также condition_variable. Мьютекс из C++11 не имеет ничего общего с мьютексом из Windows. То же самое относится и к condition_variable. Это хорошие новости в плане портируемости. Их можно использовать где угодно, где есть подходящий компилятор C++. С другой стороны, реализация C++11 в Visual C++ 2012 работает заметно хуже по сравнению с Windows-блокировкой SRW и условной переменной. На рис. 9 показан пример блокирующей очереди, реализованной на основе типов из Standard C++11 Library.
Рис. 9. Блокирующая очередь, реализованная средствами C++11
template <typename T>
class blocking_queue
{
std::deque<T> q;
std::mutex x;
std::condition_variable cv;
blocking_queue(blocking_queue const &);
blocking_queue const & operator=(blocking_queue const &);
public:
blocking_queue()
{
}
void push(T const & value)
{
std::lock_guard<std::mutex> lock(x);
q.push_back(value);
cv.notify_one();
}
T pop()
{
std::unique_lock<std::mutex> lock(x);
cv.wait(lock, [&]()
{
return !q.empty();
});
T v = q.front();
q.pop_front();
return v;
}
};
Реализация на основе Standard C++ Library, несомненно, будет со временем усовершенствована, как и поддержка этой библиотекой параллельной обработки в целом. Комитет по C++ предпринял некоторые шаги довольно консервативного характера в направлении поддержки параллельной обработки, но работа пока не закончена. Как я обсуждал в последних трех статьях, будущее параллельной обработки в C++ все еще находится под вопросом. На данный момент для создания компактных и масштабируемых программ с параллельной обработкой лучше всего сочетать некоторые превосходные синхронизирующие примитивы Windows и средства современного компилятора C++.
Кенни Керр (Kenny Kerr) — высококвалифицированный специалист в области разработки ПО для Windows. С ним можно связаться через kennykerr.ca.
Выражаю благодарность за рецензирование статьи эксперту Мохамеду Амину Ибрагиму (Mohamed Ameen Ibrahim).