Написано Райаном Телином, первоначально размещено на Educative.io

В современном технологическом климате параллелизм стал важным навыком для всех программистов на C ++. По мере того, как программы продолжают усложняться, компьютеры разрабатываются с соответствующим количеством ядер ЦП. Чтобы эффективно проектировать эти программы, разработчики должны писать код, использующий их многоядерные машины. Это достигается за счет параллелизма - метода кодирования, который обеспечивает использование всех ядер и максимизирует возможности машины. Чтобы познакомить вас с параллельным программированием и многопоточностью, я расскажу вам обо всех определениях и реальных примерах, которые вам нужно знать.

Вот что мы рассмотрим сегодня:

Что такое параллелизм?

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

Это позволяет потокам действовать независимо и принимать решения на основе предыдущего потока. В параллелизме могут возникнуть некоторые проблемы, из-за которых его сложно реализовать.

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

История параллелизма C ++

C ++ 11 был первым стандартом C ++, который представил параллелизм, включая потоки, модель памяти C ++, условные переменные, мьютекс и многое другое. Стандарт C ++ 11 кардинально изменился с появлением C ++ 17. Добавление параллельных алгоритмов в стандартную библиотеку шаблонов (STL) значительно улучшило параллельный код.

Параллелизм против параллелизма

Параллелизм и параллелизм часто путают, но важно понимать разницу. В параллельном режиме мы запускаем несколько копий одной и той же программы одновременно, но они выполняются с разными данными. Например, вы можете использовать параллелизм для отправки запросов на разные веб-сайты, но дать каждой копии программы другой набор URL-адресов. Эти копии не обязательно взаимодействуют друг с другом, но они работают одновременно и параллельно. Как мы объясняли выше, параллельное программирование включает в себя общую область памяти, и разные потоки фактически «читают» информацию, предоставленную предыдущими потоками.

Изображение из снимка EdPresso: «Что такое параллельное программирование?»

Методы реализации параллелизма

В C ++ два наиболее распространенных способа реализации параллелизма - это многопоточность и параллелизм. Хотя они могут использоваться в других языках программирования, C ++ выделяется своими параллельными возможностями с меньшими, чем в среднем, накладными расходами, а также способностью выполнять сложные инструкции. Ниже мы рассмотрим параллельное программирование и многопоточность в программировании на C ++.

C ++ Многопоточность

Многопоточность C ++ включает создание и использование объектов потока, которые в коде обозначаются как std::thread, для независимого выполнения делегированных подзадач. После создания потокам передается функция для завершения и, возможно, некоторые параметры для этой функции.

Изображение с носителя, [C ++] Параллелизм от Валентины

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

Чтобы лучше понять пулы потоков, рассмотрим отношение рабочих пчел к королеве улья; королева (программа) имеет более широкую цель (выживание улья), в то время как рабочие (нити) получают от королевы только свои индивидуальные задачи.

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

Параллелизм

Создание различных потоков обычно требует больших затрат как времени, так и памяти для программы; затраты, которые иногда могут не окупаться при работе с короткими и простыми функциями. В такие моменты разработчики могут вместо этого использовать аннотации политики параллельного выполнения, способ пометить определенные функции как кандидатов на параллелизм без явного создания потоков.

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

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

Представьте, что у нас есть две переменные, A и B, и мы создаем функции addA и addB, которые добавляют 2 к их значению. Мы могли бы сделать это с помощью параллелизма, поскольку поведение addA не зависит от поведения другой параллельной функции addB и, следовательно, не имеет проблем с одновременным завершением.

Однако, если обе функции влияют на одну и ту же переменную, мы бы вместо этого захотели использовать последовательное выполнение. Представьте, что вместо этого у нас есть один, который умножает переменную A на два, DoubleA, и другой, который добавляет B к A, addBA. В этом случае мы не хотели бы использовать параллельное выполнение, поскольку результат этого набора функций зависит от того, какая из них выполняется первой, и, следовательно, приведет к состоянию гонки.

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

Примеры многопоточности

В следующих примерах мы рассмотрим несколько простых многопоточных программ, предназначенных для использования функции печати, которую мы объявили в начале.

Простой однопоточный пример

Поскольку всем потокам должна быть предоставлена ​​функция для завершения при их создании, мы сначала должны объявить функцию для ее выполнения. Мы назовем эту функцию print и спроектируем ее так, чтобы при вызове она принимала аргументы int и string. При выполнении этот код просто сообщит о переданных значениях данных.

void print(int n, const std::string &str)  {  
    std::cout << "Printing integer: " << n << std::endl;  
    std::cout << "Printing string: " << str << std::endl;  
}

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

После этого мы используем другую удобную команду многопоточности, join(), приостанавливая поток основной функции до тех пор, пока указанный поток, в данном случае t1, не завершит свою задачу. Без join() здесь главный поток завершит свою задачу до того, как t1 завершит print, что приведет к ошибке.

int main() {
    std::thread t1(print, 10, "Educative.blog");
    t1.join();
return 0;
}

Пример многопоточности

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

Чтобы сделать это с учетом параллелизма, мы вместо этого используем цикл for для инициализации нескольких потоков, передачи им функции print и аргументов, которые они затем выполняют одновременно. Этот вариант многопоточности будет более быстрым, если будет использоваться только основной поток, поскольку используется больше всего ЦП.

Разница во времени выполнения между многопоточными и не многопоточными решениями увеличивается по мере того, как требуется больше print выполнения.

Давайте посмотрим, как будет выглядеть многопоточная версия приведенного выше кода:

Параллелизм C ++ в действии: реальные приложения

Многопоточные программы широко распространены в современных бизнес-системах. Фактически, вы, вероятно, используете более сложные версии вышеуказанных программ в своей повседневной жизни.

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

Как и выше, каждый поток будет выполнять определенную функцию, такую ​​как получение почтового ящика с переданным идентификатором void request_mail (string user_name).

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

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

Шаги вперед и ресурсы

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

Были ли вам незнакомы некоторые термины в этой статье? Хотите освежить свои знания об основах параллелизма? Если да, ознакомьтесь с нашей статьей, в которой резюмируется все, что вам нужно знать, чтобы начать свое путешествие по параллелизму.