Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ стр 5.

Шрифт
Фон

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

Алгоритмы, легко поддающиеся такому распараллеливанию, часто называют естественно параллельными (embarrassingly parallel, naturally parallel, conveniently concurrent.). Они очень хорошо масштабируются  если число располагаемых аппаратных потоков увеличивается, то и степень параллелизма алгоритма возрастает. Такой алгоритмидеальная иллюстрации пословицы «берись дружно, не будет грузно». Те части алгоритма, которые не являются естественно параллельными, можно разбить на фиксированное (и потому не масштабируемое) число параллельных задач. Техника распределения задач по потокам рассматривается в главе 8.

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

1.2.3. Когда параллелизм вреден?

Понимать, когда параллелизмом пользоваться не следует, не менее важно. Принцип простой: единственная причина не использовать параллелизмситуация, когда затраты перевешивают выигрыш. Часто параллельная программа сложнее для понимания, поэтому для написания и сопровождения многопоточного кода требуются дополнительные интеллектуальные усилия, а, стало быть, возрастает и количество ошибок. Если потенциальный прирост производительности недостаточно велик или достигаемое разделение обязанностей не настолько очевидно, чтобы оправдать дополнительные затраты времени на разработку, отладку и сопровождение многопоточной программы, то не используйте параллелизм.

Кроме того, прирост производительности может оказаться меньше ожидаемого: с запуском потоков связаны неустранимые накладные расходы, потому что ОС должна выделить ресурсы ядра и память для стека и сообщить о новом потоке планировщику, а на все это требуется время. Если задача, исполняемая в отдельном потоке, завершается быстро, то может оказаться, что в общем времени ее работы доминируют именно накладные расходы на запуск потока, поэтому производительность приложения в целом может оказаться хуже, чем если бы задача исполнялась в уже имеющемся потоке.

Далее, потокиэто ограниченный ресурс. Если одновременно работает слишком много потоков, то ресурсы ОС истощаются, что может привести к замедлению работы всей системы. Более того, при чрезмерно большом количестве потоков может исчерпаться память или адресное пространство, выделенное процессу, так как каждому потоку необходим собственный стек. Особенно часто эта проблема возникает в 32-разрядных процессах с «плоской» структурой памяти, где на размер адресного пространства налагается ограничение 4 ГБ: если у каждого потока есть стек размером 1 МБ (типичное соглашение во многих системах), то 4096 потоков займут все адресное пространство, не оставив места для кода, статических данных и кучи. В 64-разрядных системах (и системах с большей разрядностью слова) такого ограничения на размер адресного пространства нет, но ресурсы все равно конечны: если запустить слишком много потоков, то рано или поздно возникнут проблемы. Для ограничения количества потоков можно воспользоваться пулами потоков (см. главу 9), но и это не панацея  у пулов есть и свои проблемы.

Если на серверной стороне клиент-серверного приложения создается по одному потоку для каждого соединения, то при небольшом количестве соединений все будет работать прекрасно, но когда нагрузка на сервер возрастает и ему приходится обрабатывать очень много соединений, такая техника быстро приведет к истощению системных ресурсов. В такой ситуации оптимальную производительность может дать обдуманное применение пулов потоков (см. главу 9).

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

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

Но предположим, что вы уже решили, что хотите распараллелить приложение, будь то для повышения производительности, ради разделения обязанностей или просто потому, что сегодня «День многопоточности». Что это означает для программиста на С++?

1.3. Параллелизм и многопоточность в С++

Стандартизованная поддержка параллелизма за счет многопоточностивещь новая для С++. Только новый стандарт С++11 позволит писать многопоточный код, не прибегая к платформенно-зависимым расширениям. Чтобы разобраться в подоплёке многочисленных решений, принятых в новой стандартной библиотеке С++ Thread Library, необходимо вспомнить историю.

1.3.1. История многопоточности в С++

Стандарт С++ 1998 года не признавал существования потоков, поэтому результаты работы различных языковых конструкций описывались в терминах последовательной абстрактной машины. Более того, модель памяти не была формально определена, поэтому без поддержки со стороны расширений стандарта С++ 1998 года писать многопоточные приложения вообще было невозможно.

Разумеется, производители компиляторов вправе добавлять в язык любые расширения, а наличие различных API для поддержки многопоточности в языке С, например, в стандарте POSIX С Standard и в Microsoft Windows API, заставило многих производителей компиляторов С++ поддержать многопоточность с помощью платформенных расширений. Как правило, эта поддержка ограничивается разрешением использовать соответствующий платформе С API с гарантией, что библиотека времени исполнения С++ (в частности, механизм обработки исключений) будет корректно работать при наличии нескольких потоков. Хотя лишь очень немногие производители компиляторов предложили формальную модель памяти с поддержкой многопоточности, практическое поведение компиляторов и процессоров оказалось достаточно приемлемым для создания большого числа многопоточных программ на С++.

Не удовлетворившись использованием платформенно-зависимых С API для работы с многопоточностью, программисты на С++ пожелали, чтобы в используемых ими библиотеках классов были реализованы объектно-ориентированные средства для написания многопоточных программ. В различные программные каркасы типа MFC и в универсальные библиотеки на С++ типа Boost и АСЕ были включены наборы классов С++, которые обертывали платформенно-зависимые API и предоставляли высокоуровневые средства для работы с многопоточностью, призванные упростить программирование. Детали реализации в этих библиотеках существенно различаются, особенно в части запуска новых потоков, но общая структура классов очень похожа. В частности, во многих библиотеках классов С++ применяется крайне полезная идиома захват ресурса есть инициализация (RAII), которая материализуется в виде блокировок, гарантирующих освобождение мьютекса при выходе из соответствующей области видимости.

Ваша оценка очень важна

0
Шрифт
Фон

Помогите Вашим друзьям узнать о библиотеке