Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Определившись со структурой кода, мы должны постараться, чтобы все работало в соответствии с планом. Один из путей - воспользоваться набором объектов std::promise
, обозначающих, что все готово. Каждый поток устанавливает обещание, сообщая, что он готов, а затем ждет (копии) будущего результата std::shared_future
, полученного из третьего объекта s td::promise
; главный поток ждет обещаний от всех потоков, а затем запускает потоки, устанавливая go
. Тем самым гарантируется, что каждый поток запущен и находится в точке, непосредственно предшествующей коду, который должен выполняться параллельно; весь потоковый код настройки должен завершиться до установки обещания go
. Наконец, главный поток ждет завершения других потоков и проверяет получившееся состояние. Мы также должны принять во внимание исключения и гарантировать, что ни один поток не будет ждать сигнала go
, который никогда не поступит. В листинге ниже приведён один из возможных способов структурирования этого теста.
Листинг 10.1.Пример теста, проверяющего параллельное выполнение функций очереди push()
и pop()
void test_concurrent_push_and_pop_on_empty_queue() {
threadsafe_queue q; ←
(1)
std::promise go, push_ready, pop_ready;←
(2)
std::shared_future
ready(go.get_future()); ←
(3)
std: :future push_done; ←
(4)
std::future pop_done;
try {
push_done = std::async(std::launch::async, ←
(5)
[&q, ready, &push_ready]() {
push_ready.set_value();
ready.wait();
q.push(42);
}
);
pop_done = std::async(std::launch::async, ←
(6)
[&q, ready, &pop_ready]() {
pop_ready.set_value();
ready.wait();
return q.pop(); ←
(7)
}
);
push_ready.get_future().wait(); ←
(8)
pop_ready.get_future().wait();
go.set_value(); ←
(9)
push_done.get(); ←
(10)
assert(pop_done.get() == 42); ←
(11)
assert(q.empty());
} catch (...) {
go.set_value(); ←
(12)
throw;
}
}
Структура кода в точности соответствует описанной выше. Сначала, в коде общей настройки, мы создаем пустую очередь (1). Затем создаем все объекты-обещания для сигналов ready
(готово) (2)и получаем std::shared_future
для сигнала go
(3). После этого создаются будущие результаты, означающие, что потоки завершили исполнение (4). Они должны быть созданы вне блока try
, чтобы сигнал go
можно было установить в случае исключения, не ожидая завершения потоков (что привело бы к взаимоблокировке — вещь, абсолютно недопустимая в тесте).
Внутри блока try
мы затем можем создать потоки (5), (6)— использование std::launch::async
гарантирует, что каждая задача работает в отдельном потоке. Отметим, что благодаря использованию std::async
обеспечить безопасность относительно исключений проще, чем в случае простого std::thread
, потому что деструктор будущего результата присоединит поток. В переменных, захваченных лямбда-функцией, хранится ссылка на очередь, соответствующее обещание для подачи сигнала о готовности, а также копия будущего результата ready
, полученного из обещания go
.
Как было описано выше, каждая задача устанавливает свой сигнал ready
, а затем ждет общего сигнала ready
, прежде чем начать исполнение тестируемого кода. Главный поток делает всё наоборот — ждет сигналов от обоих потоков (8), а затем сигнализирует им о том, что можно переходить к исполнению тестируемого кода (9).
Напоследок главный поток вызывает функцию get()
обоих будущих результатов, возвращенных асинхронными вызовами, чтобы дождаться завершения задач (10), (11)и проверить получившееся состояние. Отметим, что задача pop возвращает извлеченное из очереди значение в будущем результате (7), чтобы мы могли проверить его в утверждении (11).
В случае исключения мы устанавливаем сигнал go
, чтобы не оказалось висячего потока, и возбуждаем исключение повторно (12). Будущие результаты, соответствующие обеим задачам (4), были объявлены последними, поэтому уничтожаются первыми, и их деструкторы ждут завершения задач, если они еще не завершились.
Хотя служебного кода многовато для тестирования двух простых вызовов, что-то в этом роде все равно необходимо, чтобы проверить именно то, что мы хотим проверить. Например, запуск потока занимает довольно много времени, поэтому если бы мы не заставили потоки ждать сигнала go
, то поток, помещающий данные, вполне мог бы завершиться еще до запуска потока, извлекающего данные, а это шло бы вразрез с целью данного теста. Благодаря использованию будущих результатов мы можем быть уверены, что оба потока запущены и блокированы в ожидании одного и того же будущего. Как только это будущее наступит, оба потока начнут работать. Привыкнув к этой структуре, вы без труда напишете и другие тесты. Продемонстрированный принцип без труда обобщается на случай, когда в каком-то тесте требуется более двух потоков.
До сих пор мы говорили о корректности многопоточного кода. Это, конечно, самая важная, но не единственная цель тестирования. Существенна также его производительность , и далее мы займемся этим вопросом.
10.2.6. Тестирование производительности многопоточного кода
Одна из основных причин распараллеливания кода — задействовать многоядерные процессоры для повышения производительности программы. Поэтому проверять, что производительность действительно возросла, так же важно, как при использовании других методов оптимизации.
Когда распараллеливание применяется ради повышения производительности, особый интерес представляет масштабируемость — мы хотим, чтобы на машине с 24 ядрами код выполнялся примерно в 24 раза быстрее или обрабатывал в 24 раза больше данных, чем на машине с одним ядром, — при прочих равных условиях. Мы не хотим, чтобы на двухъядерной машине код исполнялся в два раза быстрее, а на 24-ядерной — медленнее. В разделе 8.4.2 мы видели, что если значительная часть программы работает в одном потоке, то выигрыш от распараллеливания надает. Поэтому еще до начала тестирования стоит критически проанализировать общую структуру программы, чтобы понять, можно ли рассчитывать на 24-кратное ускорение или вследствие того, что большая часть программы работает последовательно, коэффициент ускорения вряд ли превысит 3.
В предыдущих главах было показано, что конкуренция между процессорами за доступ к структуре данных может весьма негативно сказаться на производительности. Программа, которая хорошо масштабируется, когда число процессоров мало, может повести себя никуда не годно при увеличении их числа из-за возрастания конкуренции.
Читать дальшеИнтервал:
Закладка: