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

В вычислениях есть два типа параллельного программирования: многопоточность и многопроцессорность.

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

Процессы

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

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

Потоки

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

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

Связь между потоками также легко достичь, поскольку они используют одно и то же адресное пространство, когда находятся в одном процессе.

Однако не все в нитках идеально. Когда вы имеете дело с потоками, вам нужно уделять особое внимание тому, чтобы не наступать друг на друга, что может привести к разного рода проблемам, таким как состояние гонки.

Чтобы избежать подобных проблем, при необходимости следует использовать блокировку потоков.

Применение процессов и потоков

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

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

фильтр нижних частот: фильтр нижних частот (ФНЧ) - это фильтр, который пропускает сигналы с частотой ниже выбранной частоты среза и ослабляет сигналы с частотами выше частоты среза.

Теперь, когда вы уже знаете, что такое LPF, позвольте мне объяснить проблему, которую мы решим.

На приведенном ниже графике показано 1000 образцов для данного знака. Наша цель - убрать весь шум с этого знака, чтобы конечный результат был исходным, практически без шума. Чтобы получить эти данные, вы должны загрузить файл input.dat с моего github:



Если присмотреться, можно заметить, что этот знак является синусоидальной функцией.

В цифровых системах мы используем уравнения для обработки цифровых знаков. В нашем случае уравнение ФНЧ, которое мы будем использовать, представляет собой среднее арифметическое значение, определенное ниже:

Эту формулу нетрудно понять. Он в основном вычисляет общее среднее значение данной выборки, где N - общее количество выборок, а y (n) - это отфильтрованный знак. Нам нужно отфильтровать этот знак для следующих значений: N = 3, N = 6, N = 10 и N = 20 выборок.

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

Решение с использованием процессов

Что мы сейчас сделаем, так это решим эту проблему с помощью процессов. Мы создадим 4 процесса, которые будут отвечать за обработку заданного количества образцов. Каждый из этих процессов сохранит свои результаты в файле с именем «saida [N] .txt». Выходные данные и входные данные будут обрабатываться как векторы, как показано ниже:

std::vector<float> sinal_entrada;
std::vector<float> saida;

Обратите внимание, что к тому времени, когда все дочерние процессы закончат обработку, у нас будет всего 4 файла, содержащих все данные, которые нам понадобятся для создания исходного знака.

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

Обратите внимание, что мы записываем каждый отфильтрованный знак только после завершения каждого цикла обработки, который зависит от количества N. Я также решил использовать функцию push_back (), чтобы протолкнуть элементы в вектор saida с его задней стороны.

Когда дело доходит до написания вывода, все довольно просто. Вы можете перейти к исходному коду, чтобы увидеть, как это делается.

Теперь нам нужно создать 4 дочерних процесса, каждый из которых получит разное количество N. Мы начнем с чтения файла «input.dat» и сохранения его в векторе. После этого нам нужно создать и инициализировать все 4 дочерних процесса, как показано ниже.

Как видите, вы используете pid_t для представления PID каждого процесса.

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

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

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

Наконец, нам нужно измерить время выполнения нашей программы. Для этого я использую high_resolution_clock из библиотеки chrono.

auto t2 = std::chrono::high_resolution_clock::now();

Я запускаю этот код 5 раз на процессоре Intel Core i5 с 4 ядрами @ 2,5 ГГц.

Среднее время выполнения нашего приложения на основе процессов было 22,95 мс.

Затем я использовал выходные данные и с помощью Excel построил все отфильтрованные знаки.

Красный график - лучший результат, который мы получили. Из графика совершенно ясно, что чем выше значение N, тем лучше будет наш процесс реконструкции. Конечно, есть ограничение на то, что здесь не обсуждается. Если вам интересно узнать больше о цифровой обработке знаков, ознакомьтесь с теорией Найквиста – Шеннона.

Решение с использованием потоков

Теперь, когда мы решили нашу задачу с использованием процессов, пришло время решить ее с помощью потоков.

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

Мы будем использовать pthread_t из библиотеки pthread.

Наша функция doFiltering () получит указатель для своего ввода N. Обратите внимание, что мы будем отправлять адрес памяти наших переменных для каждого потока.

Когда дело доходит до инициализации каждого потока, мы будем использовать pthread_create следующим образом:

pthread_create(&tid_2, NULL, doFiltering2, (void *)N);

После выполнения каждого вторичного потока основной процесс присоединится к выходу и запишет результат в файл с именем «saida_TOTAL.txt». Выполнение ниже показывает каждый шаг, сделанный нашей программой.

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

Заключение

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

Эта четырехкратная разница связана с накладными расходами, связанными с процессами.

Я надеюсь, что этот пост был вам чем-то полезен, и если у вас возникнут какие-либо вопросы, не стесняйтесь обращаться ко мне по электронной почте.

Jaimedantas.com