Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Один из распространенных способов разрешить такую ситуацию — сделать функцию потока замкнутой, то есть копировать в поток данные, а не разделять их. Если функция потока реализовала в виде вызываемого объекта, то сам этот объект копируется в поток, поэтому исходный объект можно сразу же уничтожить. Однако по-прежнему необходимо следить за тем, чтобы объект не содержал ссылок или указателей, как в листинге 2.1. В частности, не стоит создавать внутри функции поток, имеющий доступ к локальным переменным этой функции, если нет гарантии, что поток завершится до выхода из функции.
Есть и другой способ — явно гарантировать, что поток завершит исполнение до выхода из функции, присоединившись к нему.
2.1.2. Ожидание завершения потока
Чтобы дождаться завершения потока, следует вызвать функцию join()
ассоциированного объекта std::thread
. В листинге 2.1 мы можем заменить вызов my_thread.detach()
перед закрывающей скобкой тела функции вызовом my_thread.join()
, и тем самым гарантировать, что поток завершится до выхода из функции, то есть раньше, чем будут уничтожены локальные переменные. В данном случае это означает, что запускать функцию в отдельном потоке не имело смысла, так как первый поток в это время ничего не делает, по в реальной программе исходный поток мог бы либо сам делать что-то полезное, либо запустить несколько потоков параллельно, а потом дождаться их всех.
Функция join()
дает очень простую и прямолинейную альтернативу — либо мы ждем завершения потока, либо нет. Если необходим более точный контроль над ожиданием потока, например если необходимо проверить, завершился ли поток, или ждать только ограниченное время, то следует прибегнуть к другим механизмам, таким, как условные переменные и будущие результаты, которые мы будем рассматривать в главе 4. Кроме тот, при вызове join()
очищается вся ассоциированная с потоком память, так что объект std::thread
более не связан с завершившимся потоком — он вообще не связан ни с каким потоком. Это значит, что для каждого потока вызвать функцию join()
можно только один раз; после первого вызова объект std::thread
уже не допускает присоединения, и функция joinable()
возвращает false
.
2.1.3. Ожидание в случае исключения
Выше уже отмечалось, что функцию join()
или detach()
необходимо вызвать до уничтожения объекта std::thread
. Если вы хотите отсоединить поток, то обычно достаточно вызвать detach()
сразу после его запуска, так что здесь проблемы не возникает. Но если вы собираетесь дождаться завершения потока, то надо тщательно выбирать место, куда поместить вызов join()
. Важно, чтобы из-за исключения, произошедшего между запуском потока и вызовом join()
, не оказалось, что обращение к join()
вообще окажется пропущенным.
Чтобы приложение не завершилось аварийно при возникновении исключения, необходимо решить, что делать в этом случае. Вообще говоря, если вы намеревались вызвать функцию join()
при нормальном выполнении программы, то следует вызывать ее и в случае исключения, чтобы избежать проблем, связанных с временем жизни. В листинге 2.2 приведен простой способ решения этой задачи.
Листинг 2.2.Ожидание завершения потока
struct func; ←┐
см. определение
│
в листинге 2.1
void f() {
int some_local_state = 0;
func my_func(some_local_state)
std::thread t(my_func);
try {
do_something_in_current_thread()
}
catch(...) {
t.join(); ←
(1)
throw;
}
t.join(); ←
(2)
}
В листинге 2.2 блок try
/ catch
используется для того, чтобы поток, имеющий доступ к локальному состоянию, гарантированно завершился до выхода из функции вне зависимости оттого, происходит выход нормально (2)или вследствие исключения (1). Записывать блоки try
/ catch
очень долго и при этом легко допустить ошибку, поэтому такой способ не идеален. Если необходимо гарантировать, что поток завершается до выхода из функции потому ли, что он хранит ссылки на локальные переменные, или по какой-то иной причине то важно обеспечить это на всех возможных путях выхода, как нормальных, так и в результате исключения, и хотелось бы иметь для этого простой и лаконичный механизм.
Один из способов решить эту задачу воспользоваться стандартной идиомой захват ресурса есть инициализация (RAII) и написать класс, который вызывает join()
в деструкторе, например, такой, как в листинге 2.3. Обратите внимание, насколько проще стала функция f()
.
Листинг 2.3.Использование идиомы RAII для ожидания завершения потока
class thread_guard {
std::threads t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if (t.joinable()) ←
(1)
{
t.join(); ←
(2)
}
}
thread_guard(thread_guard const&)=delete; ←
(3)
thread_guard& operator=(thread_guard const&)=delete;
};
struct func; ←┐
см.определение
│
в листинге 2.1
void f() {
int some_local_state;
std::thread t(func(some_local_state));
thread_guard g(t);
do_something_in_current_thread();
} ←
(4)
Когда текущий поток доходит до конца f
(4), локальные объекты уничтожаются в порядке, обратном тому, в котором были сконструированы. Следовательно, сначала уничтожается объект g
типа thread_guard
, и в его деструкторе (2)происходит присоединение к потоку Это справедливо даже в том случае, когда выход из функции f
произошел в результате исключения внутри функции do_something_in_current_thread
.
Деструктор класса thread_guard
в листинге 2.3 сначала проверяет, что объект std::thread
находится в состоянии joinable()
(1)и, лишь если это так, вызывает join()
(2). Это существенно, потому что функцию join()
можно вызывать только один раз для данного потока, так что если он уже присоединился, то делать это вторично было бы ошибкой.
Копирующий конструктор и копирующий оператор присваивания помечены признаком =delete
(3), чтобы компилятор не генерировал их автоматически: копирование или присваивание такого объекта таит в себе опасность, поскольку время жизни копии может оказаться дольше, чем время жизни присоединяемого потока. Но раз эти функции объявлены как «удаленные», то любая попытка скопировать объект типа thread_guard
приведет к ошибке компиляции. Дополнительные сведения об удаленных функциях см. в приложении А, раздел А.2.
Если ждать завершения потока не требуется, то от проблемы безопасности относительно исключений можно вообще уйти, отсоединив поток. Тем самым связь потока с объектом std::thread
разрывается, и при уничтожении объекта std::thread
функция std::terminate()
не будет вызвана. Но отсоединенный поток по-прежнему работает — в фоновом режиме.
Интервал:
Закладка: