Процессы и потоки
Процессы и потоки
Процесс – это исполняемая копия приложения. Например, когда вы открываете приложение MS Word, то запускаете процесс, исполняющий программу MS Word. Поток – отдельное исполняемое задание внутри процесса. Процесс может содержать множество исполняемых потоков. После запуска приложения исполняется главный поток, который далее может порождать другие потоки.
Каждый процесс обладает собственной памятью. Потоки же, которые запущены внутри процесса, разделяют память между собой. Процесс внутри операционной системы обладает собственным идентификатором. Потоки существуют внутри процесса и обладают идентификатором внутри работающего приложения. Каждый из потоков имеет свой собственный стек (он не делит его с другими потоками и другие потоки не могут в него залезть) и собственный набор регистров (поток не изменит значения регистра другого потока во время работы). Часто потоки называют «легковесными» процессами, так как они требуют гораздо меньше ресурсов для работы, чем новый процесс. В зависимости от реализации, обычный настольный компьютер может эффективно использовать от единиц, до десятков тысяч потоков.
Процесс
- Идентификатор процесса
- Окружение
- Рабочая папка
- Регистры
- Стек
- Куча
- Файловый дескриптор
- Общие библиотеки (dll, so)
- Инструменты межпроцессорного взаимодействия (пайпы, очереди сообщений, семафоры или обобщённая память)
- Специфические для операционной системы ресурсы
Поток
- Stack Pointer (указатель на вершину стека, на самом деле «своего» стека, как у процесса, у потока нет)
- Регистры
- Свойства (необходимые для планировщика, такие как приоритет или политики)
- Специфичные для потока данные
- Специфические для операционной системы ресурсы
Многозадачность и параллелизм
Многозадачные системы позволяют запускать несколько задач одновременно. Многозадачность не обязательно обозначает истинную параллельность выполнения задач: такие системы существуют достаточно давно и появились тогда, когда процессоры были одноядерными. Все задачи получают от планировщика временной промежуток, в течение которого выполнять работу. После чего задача переходит в состояние ожидания. Все задачи имеют свой приоритет, соответственно, чем выше приоритет, тем больше времени задача может работать.
Многозадачные системы на однопоточном процессоре создают иллюзию синхронного выполнения нескольких процессов. Пусть у нас есть три процесса. Если каждый из них работает время t1, t2 и t3, то общее время выполнения будет равно t1+ t2 + t3.
Если теперь мы разобьём каждую из задач на N частей, то общее время выполнения будет dt1*N+dt2*N+dt3*N+dts*N*N, где dts – это время, затрачиваемое на восстановление контекста выполнения задачи (на работу планировщика).
С одной стороны, последовательное выполнение трёх задач без накладных расходов на переключение между задачами должно быть гораздо быстрее. Однако на практике часто бывает иначе. Если процесс выполняет много операция ввода-вывода или работает с внешними ресурсами, то большую часть времени он простаивает, ожидая данные. Это время простоя занимает другая задача. Таким образом, общее время выполнения становится меньше.
Если у нас имеется одна «числодробительная» задача, то никакого преимущества не будет. Но стоит помнить, что в ряде случаев даже на одноядерном процессоре такая искусственная параллелизация может существенно ускорить выполнение.
Для многоядерных систем всё яснее: если задача разбита на несколько потоков, то каждый из них может выполняться реально параллельно. То есть, если решать задачу в 4 потока вместо одного, то потенциально она станет работать в 4 раза быстрее. Очевидно, что где-то есть подвох…
Во-первых, ускорение работы с увеличением числа процессоров и ядер растёт нелинейно и имеет для данной задачи какой-то потолок (см. закон Амдала). А во-вторых, задача сильно усложняется при наличии общих ресурсов.
Совместный доступ к ресурсам
Когда несколько потоков делают каждый своё дело, не разделяя память, то они могут сильно ускорить работу. Дополнительные издержки потребуются только для выделения ресурсов под эти потоки и для передачи им необходимых данных. Когда несколько потоков должны общаться друг с другом, передавать данные, обрабатывать один объект, то есть совместно обращаться к одному ресурсу (обычно это общий участок памяти), то возникают так называемые race conditions – состояния гонки – когда результат работы зависит от порядка доступа к ресурсам.
Например, нам нужно сложить два массива a и b одинаковой длины и поместить результат в массив c. Каждое значение c[i] зависит от a[i] и b[i] и не зависит от остальных. Мы можем разделить массивы на несколько участков, и каждый из потоков будет заниматься сложением только этих участков, не пересекаясь с остальными потоками. У них всех будут общие переменные a, b и c, но они будут всегда независимо обращаться только к отдельным областям памяти.
Второй типичный пример: банковский счёт. Пусть два человека имеют доступ до одного счёта. На счету 10000. Пользователь A снимает со счёта 8000. Второй пользователь запрашивает остаток. Операция первого пользователя не успела завершиться и на счету указано 10000. Второй пользователь снимает 5000. В тот момент, когда он отправил заявку на снятие денег со счёта, деньги уже снялись, и на счету осталось 2000. В данном случае возможно несколько исходов. Самый лучший, когда у второго пользователя выпадет ошибка, и он ничего не получит. Ситуация, когда второй пользователь снимет деньги и счёт станет -3000. А также ситуация, когда оба снимут деньги и на счету останется 2000 или 5000.
POSIX threads
Исторически сложилось, что каждый производитель железа реализовывал свою проприетарную версию потоков. Эти реализации сильно отличались друг от друга, создавая большие проблемы для программистов и не давая возможности писать переносимое программное обеспечение.
В связи с этим, появилась необходимость в стандарте для потоков. Для UNIX-подобных операционных систем был принят стандарт IEEE POSIX 1003.1c (1995). Реализация библиотеки для работы с потоками в соответствии с этим стандартом и называется POSIX threads, или pthreads.
В настоящее время большинство производителей совместно со своими собственными интерфейсами для работы с потоками предлагают Pthreads. Pthreads обычно представляет собой набор типов и функций на языке си, описанных в файле pthread.h и реализованных в .h, .lib, .dll и т.д. файлах, поставляемых с библиотекой. Иногда pthread входит в состав другой библиотеки (например, libc).
