В чем разница между потоком и процессом?
Процессы и потоки связаны друг с другом, но при этом имеют существенные различия.
Процесс — экземпляр программы во время выполнения, независимый объект, которому выделены системные ресурсы (например, процессорное время и память). Каждый процесс выполняется в отдельном адресном пространстве: один процесс не может получить доступ к переменным и структурам данных другого. Если процесс хочет получить доступ к чужим ресурсам, необходимо использовать межпроцессное взаимодействие. Это могут быть конвейеры, файлы, каналы связи между компьютерами и многое другое.
Поток использует то же самое пространства стека, что и процесс, а множество потоков совместно используют данные своих состояний. Как правило, каждый поток может работать (читать и писать) с одной и той же областью памяти, в отличие от процессов, которые не могут просто так получить доступ к памяти другого процесса. У каждого потока есть собственные регистры и собственный стек, но другие потоки могут их использовать.
Поток — определенный способ выполнения процесса. Когда один поток изменяет ресурс процесса, это изменение сразу же становится видно другим потокам этого процесса.
Разбор взят из книги Гейл Л. Макдауэлл «Cracking the Coding Interview» (есть в переводе).
Чем процесс отличается от потока
Next: Преимущества многопоточности Up: Потоки (threads) Previous: Потоки (threads) Contents
- Процесс располагает определенными ресурсами. Он размещен в некотором виртуальном адресном пространстве, содержащем образ этого процесса. Кроме того, процесс управляет другими ресурсами (файлы, устройства ввода / вывода и т.д.).
- Процесс подвержен диспетчеризации. Он определяет порядок выполнения одной или нескольких программ, при этом выполнение может перекрываться другими процессами. Каждый процесс имеет состояние выполнения и приоритет диспетчеризации.
- владельцу ресурса, обычно называемому процессом или задачей, присущи:
- виртуальное адресное пространство;
- индивидуальный доступ к процессору, другим процессам, файлам, и ресурсам ввода — вывода.
- состояние выполнения (активное, готовность и т.д.);
- сохранение контекста потока в неактивном состоянии;
- стек выполнения и некоторая статическая память для локальных переменных;
- доступ к пространству памяти и ресурсам своего процесса.
При корректной реализации потоки имеют определенные преимущества перед процессами. Им требуется:
- меньше времени для создания нового потока, поскольку создаваемый поток использует адресное пространство текущего процесса;
- меньше времени для завершения потока;
- меньше времени для переключения между двумя потоками в пределах процесса;
- меньше коммуникационных расходов, поскольку потоки разделяют все ресурсы, и в частности адресное пространство. Данные, продуцируемые одним из потоков, немедленно становятся доступными всем другим потокам.
Процессы и потоки
П роцесс – это исполняемая копия приложения. Например, когда вы открываете приложение 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).
ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students
Всё ещё не понятно? – пиши вопросы на ящик
Процессы и потоки in-depth. Обзор различных потоковых моделей
Здравствуйте дорогие читатели. В данной статье мы рассмотрим различные потоковые модели, которые реализованы в современных ОС (preemptive, cooperative threads). Также кратко рассмотрим как потоки и средства синхронизации реализованы в Win32 API и Posix Threads. Хотя на Хабре больше популярны скриптовые языки, однако основы — должны знать все 😉
Потоки, процессы, контексты.
Системный вызов (syscall). Данное понятие, вы будете встречать достаточно часто в данной статье, однако несмотря на всю мощь звучания, его определение достаточно простое 🙂 Системный вызов — это процесс вызова функции ядра, из приложение пользователя. Режим ядра — код, который выполняется в нулевом кольце защиты процессора (ring0) с максимальными привилегиями. Режим пользователя — код, исполняемый в третьем кольце защиты процессора (ring3), обладает пониженными привилегиями. Если код в ring3 будет использовать одну из запрещенных инструкций (к примеру rdmsr/wrmsr, in/out, попытку чтения регистра cr3, cr4 и т.д.), сработает аппаратное исключение и пользовательский процесс, чей код исполнял процессор в большинстве случаях будет прерван. Системный вызов осуществляет переход из режима ядра в режим пользователя с помощью вызова инструкции syscall/sysenter, int2eh в Win2k, int80h в Linux и т.д.
И так, что же такое поток? Поток (thread) — это, сущность операционной системы, процесс выполнения на процессоре набора инструкций, точнее говоря программного кода. Общее назначение потоков — параллельное выполнение на процессоре двух или более различных задач. Как можно догадаться, потоки были первым шагом на пути к многозадачным ОС. Планировщик ОС, руководствуясь приоритетом потока, распределяет кванты времени между разными потоками и ставит потоки на выполнение.
На ряду с потоком, существует также такая сущность, как процесс. Процесс (process) — не что более иное, как некая абстракция, которая инкапсулирует в себе все ресурсы процесса (открытые файлы, файлы отображенные в память. ) и их дескрипторы, потоки и т.д. Каждый процесс имеет как минимум один поток. Также каждый процесс имеет свое собственное виртуальное адресное пространство и контекст выполнения, а потоки одного процесса разделяют адресное пространство процесса.
- Регистры процессора.
- Указатель на стек потока/процесса.
- Если ваша задача требует интенсивного распараллеливания, используйте потоки одного процесса, вместо нескольких процессов. Все потому, что переключение контекста процесса происходит гораздо медленнее, чем контекста потока.
- При использовании потока, старайтесь не злоупотреблять средствами синхронизации, которые требуют системных вызовов ядра (например мьютексы). Переключение в редим ядра — дорогостоящая операция!
- Если вы пишете код, исполняемый в ring0 (к примеру драйвер), старайтесь обойтись без использования дополнительных потоков, так как смена контекста потока — дорогостоящая операция.
Классификация потоков
- По отображению в ядро: 1:1, N:M, N:1
- По многозадачной модели: вытесняющая многозадачность (preemptive multitasking), кооперативная многозадачность (cooperative multitasking).
- По уровню реализации: режим ядра, режим польователя, гибридная реализация.
Классификация потоков по отображению в режим ядра
- Центральный планировщик ОС режима ядра, который распределяет время между любым потоком в системе.
- Планировщик библиотеки потоков. У библиотеки потоков режима пользователя может быть свой планировщик, который распределяет время между потоками различных процессов режима пользователя.
- Планировщик потоков процесса. Уже рассмотренные нами волокна, ставятся на выполнение именно таким способом. К примеру свой Thread Manager есть у каждого процесса Mac OS X, написанного с использованием библиотеки Carbon.
Модель N:M отображает некоторое число потоков пользовательских процессов N на M потоков режима ядра. Проще говоря имеем некую гибридную систему, когда часть потоков ставится на выполнение в планировщике ОС, а большая их часть в планировщике потоков процесса или библиотеки потоков. Как пример можно привести GNU Portable Threads. Данная модель достаточно трудно реализуема, но обладает большей производительностью, так как можно избежать значительного количества системных вызовов.
Модель N:1. Как вы наверное догадались — множество потоков пользовательского процесса отображаются на один поток ядра ОС. Например волокна.
Классификация потоков по многозадачной модели
Во времена DOS, когда однозадачные ОС перестали удовлетворять потребителя, программисты и архитекторы задумали реализовать многозадачную ОС. Самое простое решение было следующим: взять общее количество потоков, определить какой-нибудь минимальный интервал выполнения одного потока, да взять и разделить между всеми -братьями- потоками время выполнения поровну. Так и появилось понятие кооперативной многозадачности (cooperative multitasking), т.е. все потоки выполняются поочередно, с равным временем выполнения. Никакой другой поток, не может вытеснить текущий выполняющийся поток. Такой очень простой и очевидный подход нашел свое применение во всех версиях Mac OS вплоть до Mac OS X, также в Windows до Windows 95, и Windows NT. До сих пор кооперативная многозадачность используется в Win32 для запуска 16 битных приложений. Также для обеспечения совместимости, cooperative multitasking используется менеджером потоков в Carbon приложениях для Mac OS X.
Однако, кооперативная многозадачность со временем показала свою несостоятельность. Росли объемы данных хранимых на винчестерах, росла также скорость передачи данных в сетях. Стало понятно, что некоторые потоки должны иметь больший приоритет, как-то потоки обслуживания прерываний устройств, обработки синхронных IO операций и т.д. В это время каждый поток и процесс в системе обзавелся таким свойством, как приоритет. Подробнее о приоритетах потоков и процессов в Win32 API вы можете прочесть в книге Джефри Рихтера, мы на этом останавливатся не будем 😉 Таким образом поток с большим приоритетом, может вытеснить поток с меньшим. Такой прицип лег в основу вытесняющей многозадачности (preemptive multitasking). Сейчас все современные ОС используют данный подход, за исключением реализации волокон в пользовательском режиме.
Классификация потоков по уровню реализации
- Реализация потоков на уровне ядра. Проще говоря, это классическая 1:1 модель. Под эту категорию подпадают:
- Потоки Win32.
- Реализация Posix Threads в Linux — Native Posix Threads Library (NPTL). Дело в том, что до версии ядра 2.6 pthreads в Linux был целиком и полностью реализован в режиме пользователя (LinuxThreads). LinuxThreads реализовывалf модель 1:1 следующим образом: при создании нового потока, библиотека осуществляла системный вызов clone, и создавало новый процесс, который тем не менее разделял единое адресное пространство с родительским. Это породило множество проблем, к примеру потоки имели разные идентификаторы процесса, что противоречило некоторым аспектам стандарта Posix, которые касаются планировщика, сигналов, примитивов синхронизации. Также модель вытеснения потоков, работала во многих случаях с ошибками, по этому поддержку pthread решено было положить на плечи ядра. Сразу две разработки велись в данном направлении компаниями IBM и Red Hat. Однако, реализация IBM не снискала должной популярности, и не была включена ни в один из дистрибутивов, потому IBM приостановила дальнейшую разработку и поддержку библиотеки (NGPT). Позднее NPTL вошли в библиотеку glibc.
- Легковесные ядерны потоки (Leight Weight Kernel Threads — LWKT), например в DragonFlyBSD. Отличие этих потоков, от других потоков режима ядра в том, что легковесные ядерные потоки могут вытеснять другие ядерные потоки. В DragonFlyBSD существует множество ядерных потоков, например поток обслуживания аппаратных прерываний, поток обслуживания программных прерываний и т.д. Все они работают с фиксированным приоритетом, так вот LWKT могут вытеснять эти потоки (preempt). Конечно это уже более специфические вещи, про которые можно говорить бесконечно, но приведу еще два примера. В Windows все потоки ядра выполняются либо в контексте потока инициировавшего системный вызов/IO операцию, либо в контексте потока системного процесса system. В Mac OS X существует еще более интересная система. В ядре есть лишь понятие task, т.е. задачи. Все операции ядра выполняются в контексте kernel_task. Обработка аппаратного прерывания, к примеру, происходит в контексте потока драйвера, который обслуживает данное прерывание.
- Реализация потоков в пользовательском режиме. Так как, системный вызов и смена контекста — достаточно тяжелые операции, идея реализовать поддержку потоков в режиме пользователя витает в воздухе давно. Множество попыток было сделано, однако данная методика популярности не обрела:
- GNU Portable Threads — реализация Posix Threads в пользовательском режиме. Основное преимущество — высокая портабельность данной библиотеки, проще говоря она может быть легко перенесена на другие ОС. Проблему вытиснения потоков в данной библиотеке решили очень просто — потоки в ней не вытесняются 🙂 Ну и конечно ни о какой мультмпроцессорности речь идти не может. Данная библиотека реализует модель N:1.
- Carbon Threads, которые я упоминал уже не раз, и RealBasic Threads.
- Гибридная реализация. Попытка использовать все преимущества первого и второго подхода, но как правило подобные мутанты обладают гораздо бОльшими недостатками, нежели достоинствами. Один из примеров: реализация Posix Threads в NetBSD по модели N:M, которая была посже заменена на систему 1:1. Более подробно вы можете прочесть в публикации Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism.
Win32 API Threads
Если вы все еще не устали, предлагаю небольшой обзор API для работы с потоками и средствами синхронизации в win32 API. Если вы уже знакомы с материалом, можете смело пропускать этот раздел 😉
Потоки в Win32 создаются с помощью функции CreateThread, куда передается указатель на функцию (назовем ее функцией потока), которая будет выполнятся в созданом потоке. Поток считается завершенным, когда выполнится функция потока. Если же вы хотите гарантировать, что поток завершен, то можно воспользоватся функцией TerminateThread, однако не злоупотребляйте ею! Данная функция «убивает» поток, и отнюдь не всегда делает это корректно. Функция ExitThread будет вызвана неявно, когда завершится функция потока, или же вы можете вызвать данную функцию самостоятельно. Главная ее задача — освободить стек потока и его хендл, т.е. структуры ядра, которые обслуживают данный поток.
Поток в Win32 может пребывать в состоянии сна (suspend). Можно «усыпить поток» с помощью вызова функции SuspendThread, и «разбудить» его с помощью вызова ResumeThread, также поток можно перевести в состояние сна при создании, установив значение параметра СreateSuspended функции CreateThread. Не стоит удивлятся, если вы не увидите подобной функциональности в кроссплатформенных библиотеках, типа boost::threads и QT. Все очень просто, pthreads просто не поддерживают подобную функциональность.
Средства синхронихации в Win32 есть двух типов: реализованные на уровне пользователя, и на уровне ядра. Первые — это критические секции (critical section), к второму набору относят мьютексы (mutex), события (event) и семафоры (semaphore).
Критические секции — легковесный механизм синхронизации, который работает на уровне пользовательского процесса и не использует тяжелых системных вызовов. Он основан на механизме взаимных блокировок или спин локов (spin lock). Поток, который желает обезопасить определенные данные от race conditions вызывает функцию EnterCliticalSection/TryEnterCriticalSection. Если критическая секция свободна — поток занимает ее, если же нет — поток блокируется (т.е. не выполняется и не отъедает процессорное время) до тех пор, пока секция не будет освобождена другим потоком с помощью вызова функции LeaveCriticalSection. Данные функции — атомарные, т.е. вы можете не переживать за целостность ваших данных 😉
- Они использует примитивы ядра при выполнении, т.е. системные вызовы, что сказывается не производительности.
- Могут быть именованными и не именованными, т.е. каждому такому объекту синхронизации можно присвоить имя.
- Работают на уровне системы, а не на уровне процесса, т.е. могут служить механизмом межпроцессного взаимодействия (IPC).
- Используют для ожидания и захвата примитива единую функцию: WaitForSingleObject/WaitForMultipleObjects.
Posix Threads или pthreads
Сложно представить, какая из *nix подобных операционных систем, не реализует этот стандарт. Стоит отметить, что pthreads также используется в различных операционных системах реального времени (RTOS), потому требование к этой библиотеке (вернее стандарту) — жестче. К примеру, поток pthread не может пребывать в состоянии сна. Также в pthread нет событий, но есть гораздо более мощный механизм — условных переменных (conditional variables), который с лихвой покрывает все необходимые нужды.
Поговорим об отличиях. К примеру, поток в pthreads может быть отменен (cancel), т.е. просто снят с выполнения посредством системного вызова pthread_cancel в момент ожидания освобождения какого-нибудь мьютекса или условной переменной, в момент выполнения вызова pthread_join (вызывающий поток блокируется до тех пор, пока не закончит свое выполнение поток, приминительно к которому была вызвана функция) и т.д. Для работы с мьютексами и семафорами существует отдельные вызовы, как-то pthread_mutex_lock/pthread_mutex_unlock и т.д.
Conditional variables (cv) обычно используется в паре с мьютексами в более сложных случаях. Если мьютекс просто блокирует поток, до тех пор, пока другой поток не освободит его, то cv создают условия, когда поток может заблокировать сам себя до тех пор, пока не произойдет какое-либо условия разблокировки. Например, механизм cv помогает эмулировать события в среде pthreads. Итак, системный вызов pthread_cond_wait ждет, пока поток не будет уведомлен о том, что случилось определенное событие. pthread_cond_signal уведомляет один поток из очереди, что cv сработала. pthread_cond_broadcast уведомляет все потоки, которые вызывали pthread_cond_wait, что сработала cv.
Прощальное слово
На сегодня пожалуй все, иначе информации станет слишком много. Для интересующихся, есть несколько полезных ссылок и книг внизу 😉 Также высказывайте свое мнение, интересны ли вам статьи по данной теме.
UPD: дополнил статью небольшой информацией о режиме ядра и режиме пользователя.
UPD2: исправил досадные промахи и ошибки. Спасибо комментаторам 😉