Сергей Яковлев
Мой блокнот, мастерская, место где я делюсь своим опытом и мыслями

Процессы, потоки и конкурентное выполнение программ

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

Все нижесказанное в первую очередь относится к POSIX-совестимым системам. В Windows всё чуть иначе и я не буду останавливаться на различиях. Если вы заметили неточность или хотели бы, чтобы я добавил что-то интересное, напишите мне.

Структура процесса

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

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

  • Идентификация двоичного формата (a.out, coff, elf)
  • Машинный код (алгоритм и логика программы)
  • Адрес точки входа (например функции main)
  • Данные (значения переменных, строки)
  • Таблицы имен для однозначной идентификации принадлежности символов
  • Информация о совместно используемых библиотеках и динамической компоновке, включая название компоновщика

С позиции ядра процесс является абстрактной сущностью, состоящей из памяти пользовательского пространства:

  • код программы
  • переменные

и ряда структур данных ядра:

  • состояние процесса
  • таблицы виртуальной памяти
  • обработка сигналов
  • ресурсы процессора
  • текущий рабочий каталог

Ядро изолирует процессы от взаимного влияния. Каждый процесс владеет изолированным адресным пространством. Процессы, нуждающиеся в обмене данными, должны явно организовать такой обмен посредством межпроцессного взаимодействия (IPC): каналов, файлов, базы данных и т.п.

Идентификация процессов

Каждый процесс имеет идентификатор (PID), за уникальностью которого следит операционная система. Кроме того, каждый процесс (за исключением init, PID которого равен 1) содержит идентификатор родительского процесса (PPID). Родительский процесс, в свою очередь содержит идентификатор своего родителя. Таким образом, ядро операционной системы обслуживает дерево процессов (см. команду pstree). Если процесс становится сиротой, ядро делает его родителем процесса init. В UNIX-подобных системах с интерфейсом коммуникации с ядром основанным на procfs (например BSD, Linux), родитель любого процесса может быть найден следующей командой:

# <PID> -- реальный идентификатор
grep PPid /proc/<PID>/status | awk '{print $2}'

Параллельное выполнение задач

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

Порождение каждого нового процесса производится при помощи системного вызова fork(). Однако данная операция является дорогой с т.з. ресурсов и времени инициализации процесса. Для обеспечения более легковесной работы параллельных сопрограмм принято использовать потоки вместо процессов. Каждый процесс создается как минимум с одним потоком. Существует техническая возможность создавать дополнительные потоки внутри процесса.

Можно определить следующие ограничения создания дополнительных процессов:

  • Каждый процесс выполняется в отдельном адресном пространстве. Это в свою очередь приводит к тому, что процессы не разделяют общую память
  • Создание процесса с помощью fork() занимает существенное время
  • Усложненный обмен данным между процессами (конвейеры, файлы, каналы и т.д.)

Модель конкурентного выполнения сопрограмм (concurrency) - это немного более широкий термин, чем параллелизм (parallelism). Данная модель предполагает, что несколько задач могут выполняться одновременно, но не говорит как это должно быть достигнуто. В англоязычном мире есть поговорка, “Concurrency does not imply parallelism”. Асинхронный ввод-вывод, не являющийся ни многопроцессным, ни многопоточным, тем не менее также подпадает под формулировку конкурентного выполнения кода. Резюмируя вышесказанное, многопроцессность это форма параллелизма (которого также можно добиться многопоточностью), а параллелизм в свою очередь, это подмножество конкурентного выполнения сопрограмм.

Схематично вышесказанное можно изобразить следующим образом:

Concurrency vs Threading vs Parallelism vs Multiprocessing

О потоках

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

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

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

В Linux потоки реализованы с помощью системного вызова clone(), который как минимум в 10 раз меньше занимает времени для создания еще одного потока, чем создание еще одного процесса при помощи fork(). Такая скорость достигается за счет того, что многие атрибуты процесса разделяются между потоками.

Что важно знать о потоках, так это то, что они лучше подходят для задач, связанных с вводом-выводом. В то время как задачи ориентированные процессорные вычисления (CPU-bound), характеризуется постоянной интенсивной работой ядер компьютера от начала до конца, в задачах ориентированных на ввод-вывод (IO-bound) преобладает длительное ожидание завершения ввода-вывода.

Python

В Python существует модуль multiprocessing, позволяющий запускать дополнительные процессы и распределять вычислительную нагрузку между процессами параллельно, concurrent.futures предоставляющий высокоуровневый интерфейс к параллелизации сопрограмм, а также threading, предоставляющий высокоуровневый интерфейс к реализации параллельного выполнения кода с использованием многопоточности.

Литература

  • Майкл Керриск. «Linux API. Исчерпывающее руководство». Питер, 2019.
  • Роберт Лав. «Ядро Linux. Описание процесса разработки», 3-е издание. Вильямс, 2013.
Вернуться в начало