5. МНОГОЗАДАЧНОСТЬ И МНОГОПОТОЧНОСТЬ
5.1. Цель работы Ознакомление с концепцией многозадачности и многопоточности современных операционных систем, получение практических навыков по составлению, написанию и отладке программ, содержащих параллельно функционирующие процедуры и функции.
5.2. Указания по подготовке к выполнению лабораторной работы Многозадачность на сегодняшний день – одна из определяющих особенностей операционных систем. При подготовке к лабораторной работе необходимо ознакомится с теоретическим описанием принципа многозадачности и особенностями ее аппаратной реализации. Внимательно проработать вопросы обеспечения многозадачности (multitasking) и многопоточности (multithreading) при- ложений Windows. При подготовке к работе необходимо изучить конспект лекций по указанной теме, методические указания, а также разделы, указанные в [16, c.763-809], [17, c.59-87]. 5.3. Обзор темы работы Многозадачность (multitasking) – это способность операционной системы выполнять несколько программ одновременно. В основе этого принципа лежит использование операционной системой аппаратного таймера для выделения отрезков времени для каждого из одновременно выполняемых процессов. Если эти отрезки времени достаточно малы, и машина не перегружена слишком большим числом программ, то пользователю кажется, что все эти программы выполняются параллельно. Многопоточность – это возможность программы самой быть многозадачной. Программа может быть разделена на отдельные потоки выполнения, которые, как кажется, выполняются параллельно. В лабораторной работе изучаются функции порождения и завершения процесса CreateProcess, ExitProcess, TerminateProcess, создания и завершения потока CreateThread, ExitThread. Особое внимание в лабораторной работе уделяется исследованию возможностей синхронизации процессов и потоков. Существует большой класс задач (например, в управлении базами данных, параллельных вычислениях), в которых параллельно функционирующие программы (или их модули) нуждаются в обмене информации или порядок выполнения одних из программных модулей зависит от выполнения других. Windows предоставляет две специальные возможности синхронизировать параллельно выполняемые задачи – это семафоры и события. Семафор действует как обычных флаг, и используется для того, чтобы определить свободен или нет в настоящее время требующийся потоку или процессу ресурс. Пользователь может определять для семафора количество ресурсов, доступных для использования параллельными задачами. При занятии потоком какого-либо количества свободных ресурсов происходит декрементация количества ресурсов, и если, оставшееся число ресурсов недостаточно следующему потоку, он приостанавливается до момента освобождения необходимого числа ресурсов. Для управления семафорами используются функции CreateSemaphore, ReleaseSemaphore, OpenSemaphore, WaitForSingleObject. События являются самой примитивной разновидностью объектов синхронизации. Они используются для того, чтобы уведомить поток о том, что наступило ожидаемое событие. Эти объекты обычно используются для того, чтобы
синхронизировать потоки, которые работают по принципу конвейера. Например, один поток опрашивает датчики и загружает считанные значения в буфер. Другой поток считывает эти данные из буфера и производит их обработку. Первый поток может сигнализировать второму о том, что событие – заполнение буфера – наступило. Второй поток может сигнализировать первому о том, что наступило другое событие – данные из буфера считаны, ожидается новая порция данных. Событие может иметь два состояния – занятое и свободное. Работа с событиями осуществляется посредством следующих функций: CreateEvent, ResetEvent, PulseEvent, SetEvent. 5.4. Задание на лабораторную работу Вариант 1 Написать программу, порождающую четыре потока, каждому из которых выделяется четвертая часть окна приложения. Первый поток выводит в свою область возрастающую числовую последовательность 0,1,2,…, второй – последовательность чисел Фибоначчи. Третий поток заполняет свой участок окна прямоугольниками случайного размера и цвета, четвертый поток фиксирует в трех переменных и выводит их в своей области окна число запусков каждого из предыдущих трех потоков. Вариант 2 Написать программу, порождающую поток по нажатию одной из клавиш клавиатуры. Каждому созданному таким образом потоку соответствует окружность в окне приложения, которая появляется в случайном месте окна приложения и движется либо во вертикали, либо по горизонтали. При достижении границы окна, окружность меняет направление своего движения на противоположное. Вариант 3 В программе создать два потока. Назначение одного из них – периодиче- ское чтение системного времени и заполнение глобальной структуры (часы, минуты, секунды), второго – вывод данной структуры на экран. При помощи критического раздела организовать раздельный доступ потоков к структуре данных. Вариант 4 Написать программу, содержащую два потока, каждый из которых управ- ляет движением одного из двух шаров. Первый шар двигается горизонтально, второй – вертикально. Скорость шаров различна. При достижении границы клиентской области окна, шар меняет направление движения на противоположное. При помощи объектов синхронизации (семафоров или событий) реализовать алгоритм движения шаров без столкновений. Вариант 5 Написать программу, которая запускает новый поток при нажатии левой клавиши мыши. Поток начинает выводить возрастающую числовую последовательность в текущую позицию курсора мыши. При нажатии левой клавиши мыши программа удаляет поток, координаты которого ближе всего к положению мыши.
Вариант 6 Создать многопоточную программу, формирующую потоки трех типов. Каждый из потоков запускается соответствующим пунктом меню и захватывает соответственно 1,2,3 ресурса (максимальное число ресурсов по умолчанию – 8 и может меняться пользователем в окне диалога, вызываемом через меню). Количество, вид потоков, а также их состояние выводится на экран. Если число ресурсов не позволяет работать потоку, он находится в состоянии ожидания. Удаление потоков осуществляется через меню в порядке запуска (первым удаляется поток, запущенный первым). Вариант 7 Написать программу, которая позволяет запускать процессы, используя для этого выбранные на диске файлы. Пользователь может задавать имя запускаемого файла и командную строку. Программа следит за всеми запущенными ею процессами и выводит по требованию пользователя следующую информацию: имя процесса, значение указателя и идентификатора процесса, время выполнения процесса. Вариант 8 Написать программу, которая читает с диска *.bmp файл и выводит его в окно приложения. При помощи потока организовать поворот изображения на 90 градусов. Операцию можно прервать при помощи диалогового окна, возникающего на время выполнения операции. Вариант 9 Написать программу, которая по нажатию мыши создает потоки: по нажа- тию правой клавиши – поток, производящий вывод возрастающего ряда в позицию курсора, левой – поток с убывающим рядом. Поток выгружается из памяти по окончанию счета. Число потоков ограничивается пользователем через контекстное меню и находится в диапазоне [4,8]. Вариант 10 Написать три программы. Первая из них формирует метофайл, содержа- щий 10 прямоугольников, вторая – открывает созданный метофайл и дорисовывает в него 10 эллипсов. Третья программа – отображает конечный метофайл в клиентской области окна. Все три программы работают циклически, в одно и то же время может работать только одна программа. 5.5. Контрольные вопросы и задания 1. Поясните принцип многозадачности современных операционных систем. 2. Чем отличается многозадачность от многопоточности? 3. Каким образом аппаратно решаются задачи обеспечения многозадачности? 4. В чем заключаются особенности невытесняющей многозадачности? 5. Приведите примеры использования многопоточности в прикладных программах. 6. Каким образом осуществляется синхронизация потоков?
Многозадачность и многопоточность — распространенные заблуждения и недопонимания
Когда я предложил перевести на русский мою последнюю статью Easy Concurrency with Python Shared Objects на английском, поступило предложение «написать в несколько раз короче и понятнее». Просьба более чем обоснована. Поскольку я уже порядка десяти лет пишу многопоточку и БД, то описываемые мной логические связи выглядели самоочевидно, и я ошибочно расчитывал на аудиторию из трех с половиной человек, которые сидят сейчас где-то в яндексе или гугле. Судя по всему, они там и сидят, но тема им не интересна, поскольку в питоне нет настоящих потоков, а значит для этих людей такого языка программирования не существует. Потому я немножко снижаю планку и делаю общий обзор проблематики параллельных вычислений для людей, которые в них разбираются, но не являются экспертами в области.
Из-за чего весь сыр-бор?
a := 1 b := a + 1 print(a) print(b)
Цикл
Два процесса выполняются параллельно и независимо. Если мы возьмем первую инструкцию процесса 1, то параллельно с ней может выполниться любая из четырех команд. Вторая команда процесса 1 аналогично не ограничивает выполнение в процессе 2, потому она может выполняться параллельно с любой из четырех команд. Для простоты допускаем, что одна команда атомарна. Всего число возможных сценариев выполнения первого процесса 4^4 = 256. Если в аналогичных первом и втором процессах по десять инструкций, то число различных вариантов выполнения равно 10 10 , то есть, 10 миллиардов. За пару минут мы можем написать 10 миллиардов программ! Вау, мы крутые! А если серьезно, то нам не хватит никакого времени, чтобы отладить все эти 10 миллиардов сценариев выполнения.
По этой причине для успешной реализации параллельных асинхронных задач необходимо беспощадно ограничивать эту параллельность на пересечении задач, в местах согласования и координации.
Что там сложного? Есть конкуренция за ресурс — повесь мьютекс. Я так всегда делаю.
Блокировки, они же семафоры или мьютексы. Если вы только начали осваивать многопоточность в классических императивных языках, то вы знаете про задачу «обедающих философов», и в качестве простейшего решения предложите «официанта», то есть, простую блокировку. Ведь отдельная блокировка на каждую вилку быстро приводит вас к дедлокам.

А еще лучше вообще иметь только одну блокировку на все вилки, ложки, философов, и стол одновременно — создатели питона примерно так и подумали.
Если же вы решили использовать множественные блокировки, то неявные и допускающие вложенность блокировки (Rust std::sync::Mutex или Java synchronized) — путь в никуда, поскольку становится очень легко создать неочевидно дедлочащуюся программу. На самом деле, даже без дедлоков детальные блокировки не являются панацеей, поскольку простые блокировки, они же «семафоры Дейкстры», ограничены в своих способностях:
Хотя задачу курильщиков и возможно решить семафорами Дейкстры, такие «семафоры курильщика» дают сложный в понимании и поддержке алгоритм, при том, что задача, казалось бы, элементарна. Потому вторым по популярности механизмом блокировок стали условные переменные и более общий аспект оных — мониторы (в том числе уже упомянутое Java synchronized). Правда, проблемы дедлоков при множественных блокировках они не решают.
Что же делать? Мы все умрем? Да, но проблему дедлоков можно решить. Одно из остроумных решений для задачи обедающих философов предложил сам Дейкстра — брать детальные блокировки только в одном заранее заданном порядке. Следующий забавный прием — брать блокировки всем скопом одной командой либо отменять взятие блокировки. Такое можно реализовать на любой абстракции с примитивом try-lock (например, compare-and-swap), при помощи бесконечного цикла (например, std::lock из библиотеки C++).

Одна из самых последних и перспективных альтернатив блокировкам — это транзакционная память, по сути атомарный compare-and-swap на большом числе ячеек. Идею транзакционной памяти развил в конкретную программную реализацию, STM, мой любимый автор книг и статей по многопоточности — Нир Шавит. Главное преимущество STM — отсутствие дедлоков. В каком-то смысле транзакционная память достаточно стара, если вспомнить, что реляционные СУБД давно умели в транзакции. Программная транзакционная память, как правило, берет блокировки всем скопом, как в описанном выше алгоритме предварительной блокировки, но делает это не до выполнения операций изменения ячеек памяти, а после — таким образом нам не обязательно до начала работы алгоритма знать список нужных нам блокировок и длительность этих блокировок минимальна. Из популярных готовых решений на эту тему можно вспомнить Clojure и GHC.

У наивной реализации STM есть проблема — при интенсивных конфликтах меж потоками приложение большую часть времени крутит откаты транзакций вхолостую: подробнее.
Решение этой проблемы уже придумано за нас — это алгоритм раннего разрешения конфликтов, при котором реализация не ждет окончания транзакции для того, чтобы осознать невозможность применения ее результатов, а сразу прерывает одну из конфликтующих транзакций. По сути это способ организации детальных блокировок с разрешением дедлоков через откат изменений. Такой подход я и перенял при реализации Python Shared Object.

Коротко: взаимоисключающая блокировка не является основным средством организации доступа к разделяемой памяти и вообще разделяемым ресурсам, но именно этими блокировками чаще всего злоупотребляют.
Разделяемая память не нужна! Только акторная модель и сообщения (Erlang, Tcl)
Подход, известный также как «у меня болит палец — отрежу себе руку!».
Начну издалека. Если мы попытаемся разделить все-все-все алгоритмы и механизмы координации на самые простые винтики и гайки, то получим два основных примитива:
- состояние с последовательными переходами, синхронными и заранее предсказуемыми, потенциально в виде большого вектора, как то SIMD, GPGPU, компьютерные и даже живые нейронные сети. Да, живые организмы чутко воспринимают задержки входных сигналов, потому могут ощущать время и движение;
- асинхронное взаимодействие, как то сообщение в очереди или состояние-флаг, запись которого и чтение условно не зависят от задержек.
На первый взгляд очевидно, что асинхронная координация, то есть, асинхронная акторная модель и асинхронные сообщения, накладывает минимум ограничений на параллельное выполнение, при этом позволяют одинаково успешно передавать сообщения как между потоками одного процесса, так по сети между сильно удаленными компьютерами. Почему же столько разработчиков хотят вернуться в синхронность и детерминированность? Почему живые нейронные сети в значительной степени синхронны?
Асинхронная передача сообщений сама по себе не способна обеспечить строгую согласованность состояния/данных на разрозненных узлах. Если быть точным, то в асинхронной системе с отказами узла (или непредсказуемыми задержками ответа) невозможно детерминированное достижение консенсуса (достижение единого согласованного решения между всеми узлами) в соответствии с FLP-недостижимостью, а также производной теоремой CAP. В случае, если мы имеем некую гарантию времени ответа от узла и отсутствие отказов, мы можем достигнуть некоего условного консенсуса за время порядка нескольких круговых задержек передачи сообщения (то есть, периода выполнения запроса и обратного ответа).
Недетерминированность достижения консенсуса в асинхронной системе с отказами значит, что прийти к единому решению в такой системе — это рулетка, полнейшая случайность, хотя шанс «выиграть» в ней и повышается с течением времени, но он никогда не может быть меньше двух круговых задержек сети. По этой причине в реальности передача сообщений не так уж прозрачна и не так бесплатна при увеличении дистанции, например, при переходе от передачи сообщений между потоками одного процесса ОС к передаче сообщений между удаленными датацентрами. В случае большой круговой задержки протоколы организации распределенных БД, как то Raft, Zab, MultiPaxos, минимизируют проблемы координации, ограничивая ее выборами лидера, и применяют случайную задержку при выборах лидера. Аналогично Ethernet использует случайную задержку при конфликте использования канала.
Забавно, что создатель величайшей платформы Erlang для построения распределенных систем, Джо Армстронг, не строит иллюзий и осознает принципиальную неразрешимость проблемы. Вот диаграмма ситуации, которую Армстронг пытался описать на пальцах:

В любой момент времени любой из двух узлов знает о предыдущем состоянии другого узла, но не знает, получил ли другой узел последнее сообщение, каково сейчас состояние другого узла, и жив ли вообще этот второй узел. Как правило, все системы с распределенным состоянием решают эту проблему одинаково — полагаются на единственное разделяемое состояние, как на эталонное во всей распределенной системе.
По итогу мы пришли к тому, от чего уходили — к разделяемому состоянию. Вся трудность и неопределенность согласования системы, построенной на базе асинхронных сообщений, решается минимальным координатором, внутри себя исполняющим синхронный алгоритм, то есть, механизмом последовательного применения операций к одному-единственному эталонному состоянию.
YandexDB, она же Calvin, использует Zookeeper для организации глобально согласованной истории изменений. Сам Zookeeper использует асинхронные сообщения только для выбора лидера — работа с данными происходит синхронно на выбранном эталонном узле (лидере). Создатели экспериментальной СУБД Tango заявляют, что реализовывает ZooKeeper в 300 строчек кода. Однако, сам оригинальный Tango использует нереплицируемый счетчик в качестве опорного средства координации, потому не обеспечивает гарантий отказоустойчивости ZooKeeper. И по итогу для организации сравнимых гарантий в этой «реализации ZooKeeper» на Tango вам нужно реализовать координацию Tango на… ZooKeeper. Это похоже на игру «горячий пирожок».
Почему в этот горячий пирожок играют? Потому что идеальное решение невозможно чисто теоретически (FLP-недостижимость) — все практически реализации являются тем или иным компромиссом. Но есть желающие «идеальное решение» купить, потому используется популярный маркетинговый прием: недостатки существующих решений уже хорошо известны, а недостатки нашей новой системы еще не известны… значит можно сказать, что их нет. Так и развиваются многие Big Data проекты.
Когда же у вас есть хотя бы общий атомарный счетчик, а еще лучше — общий атомарный список транзакций, дальше вы можете наращивать объем «нагрузочных данных» как угодно, реплицировать их в eventual consistency хранилищах, придавать им произвольную форму (как это сделало в том же Tango, давшем пользователю свободу выбора формы данных) — это всё имеет второстепенное значение и легко меняется, хотя многие люди по прежнему обращают внимание на обёртку, а не на суть. То есть, ходовые качества машины определяются двигателем, трансмиссией, рулевой системой, но большинство видит лишь красивый кузов и отделку салона, к сожалению. Я хочу здесь подчеркнуть, что ZooKeeper в случае Tango, YandexDB, Calvin, ClickHouse и прочих аналогичных проектов — это не «просто вспомогательная штука», как его рисуют, а ключевой компонент и большая часть сложности реализации всей СУБД, и этот компонент, к тому же, полностью определяет число транзакций в секунду, которые сможет обработать весь кластер.
Проблем неопределенности в том числе избегает реализация синхронной передачи сообщений, иначе известная как «взаимодействующие последовательные процессы» (Communicating sequential processes, CSP), которые являются давно известным подходом. В частности, этот подход выбрал в качестве своего фундамента популярный язык Go и сильно менее популярные предшественники оного, языки Limbo и Newsqueak. Атомарная операция отправки сообщения через канал гарантирует, что либо сообщение отправлено и отправка одновременно подтверждена, либо оно не отправлено — для обеспечения этой синхронизации используется разделяемое состояние канала скрытое от программиста на Go.
Коротко: злоупотребление передачей сообщений в архитектуре приложения так же плохо, как злоупотребление разделяемым состоянием. Бездумное следование любой идеологии до добра не доводит.
Золотая середина
Асинхронные алгоритмы и синхронные алгоритмы, передача сообщений и разделяемое состояние дополняют друг друга. Проблемы возникают только когда одним из подходов злоупотребляют, а второй — игнорируют. Невозможность организации простого эффективного разделяемого состояния на питоне привела к появлению массы многочисленных «костылей» — это и простые РСУБД, как то PostgreSQL и MySQL, и NoSQL вроде Redis/MemCached, и очереди сообщений (RabbitMQ). Примерно на этой волне я и решил написать Python Shared Objects. В упомянутом Erlang уже давно есть специальный модуль для организации разделяемой памяти: подробнее.
Также, сама реализация Erlang/OTP неявно использует разделяемую память. Даже если из питона получится жалкое подобие Erlang — это все равно намного лучше, чем тот безнадежно однозадачный (не путать с зелеными потоками/concurrent IO) интерпретатор питона, который мы имеем сейчас.
Состояние/ячейки данных, используемое для координации, должно быть разделяемым и изменяться строго последовательно. Независимые состояния должны оставаться независимыми, асинхронно выполняющиеся задачи должны выполняться асинхронно. Если лишь часть задач требуют координации, то они и должны разделять некоторую часть состояния для своей координации. Если возможно выполнить одинаковый набор инструкций для большого числа в ячеек в векторе — выполните его при помощи SIMD/CUDA/OpenCL, а не созданием асинхронно выполняющихся потоков, которые потом придется обратно синхронизировать.
Например, оригинальная STM от Нир Шавита использует глобальный атомарный счетчик для координации всех задача, но в остальном «общими» ячейки данных становятся только когда они по-настоящему общие для нескольких задач. Аналогичный подход с общим атомарным счетчиком номера/приоритета транзакции и детальными блокировками также применяет моя реализация STM в виде Python Shared Objects.
Тесты, еще тесты, нужно больше тестов
Как же тестировать и отлаживать многозадачные и распределенные системы? Основная проблема многопоточных, многозадачных, распределенных систем — это недетерминированность их поведения. То есть, ваша система может работать пять лет, а потом резко начать падать из-за какого-то незначительного изменения алгоритма.
В общем случае ответ на вопрос «как тестировать?» — «никак», вы подходите к проблемы с неправильной стороны. Но не расстраивайтесь — это нормально, так делает много кто. По этой причине, например, большинство распределенных СУБД, заявлявших про строгую согласованность данных и отказоустойчивость, в жестких тестах на Jepsen показывают, что на самом деле никогда не обеспечивали как минимум одну из этих гарантий: подробнее.
Тестирование — это способ обнаружить наличие проблемы, но тестированием невозможно доказать отсутствие проблем. Если в качестве отказоустойчивого хранилища согласованных данных вы используете не ZooKeeper и не Etcd, то с большой вероятностью ваша система потеряет/испортит данные или вовсе упадет при потере лидера. Если вы уделите пару часов чтению серии статей по ссылке, то вы узнаете, что какой-нибудь Galera Cluster способен нарушать согласованность даже на полностью исправном кластере, а MongoDB не дает даже «eventual consistency» гарантий при отказах (то есть, в MongoDB ваши данные из подтвержденных транзакций может быть сохранятся, а может быть не сохранятся). Однако, даже если вы построили систему на базе условно надежных ZooKeeper или Etcd, вы не знаете, сколько еще проблем возникло из-за некорректного использования оных в самописном коде.
Единственный более-менее гарантированный способ построить работающую систему — это прежде всего грамотно подойти к ее проектированию и реализации, доказать корректность алгоритма, а не полагаться на популярный нынче метод тестирования и «авось». Вторая линия обороны — это тестирование, но не простое. Разработчики должны понимать, что если ошибка есть, то ее необходимо обнаружить, а не замести под ковер и подпереть костылем. То есть, без участия и сотрудничества самих разработчиков невозможно эффективно произвести тестирование. Необходимо тестировать систему в самых неудобных и опасных режимах, предусмотреть в исходном коде отладочные опции для более частого срабатывания редких фрагментов кода — в том числе такой подход я задействовал для своего проекта Python Shared Objects. В случае отказоустойчивых кластеров обязательно необходимо периодически симулировать отказы, как это делает, например, Яндекс.
Поскольку очень часто проблема происходит только один раз и больше никогда не повторяется, то большую ценность имеет логирование, создание снимков состояния системы во время проблемы. Для последнего, например, есть замечательная утилита rr от Mozilla, которая была разработана специально для отладки многопоточного кода в Firefox. Эта утилита позволяет выполнить код в обратном направлении, точно восстанавливая последовательность выполнения асинхронных потоков, приведшую к ошибке. И хотя облака нынче стали популярным хайпом, выбор инструментов для отладки многозадачного кода и распределенных систем на удивление скудный. Для распределенных систем есть упомянутый Jepsen, для многопоточных есть, например, ThreadSanitizer.
К сожалению, для тестирования моего Python Shared Objects мне так и не удалось найти готового решения для неинтрузивного логирования. Неинтрузивного — поскольку для многопоточного кода имеет место так «эффект наблюдателя» — когда при логировании программа ведет себя совсем не так, как без него. Когда-то давно для закрытой разработки я сам реализовывал неинтрузивное логирование на Delphi, но это было давно и неправда, а на сишные программы этот код довольно плохо налазит.
Коротко: при создании сложных многопоточных, многозадачных, и распределенных систем грамотный подход и такие люди, как я, с большим трудом заменяются тестами и «большой дружной командой».
- Python
- multithreading
- multitasking
- distributed computing
- многопоточность
- многозадачность
- распределённые вычисления
Android-Robot
Вы здесь: Главная Статьи Что такое многопоточность?
Что такое многопоточность?
Добавлено: 23.03.2023 | Рубрика: Статьи | Автор: Сергей
Прочитано: 122 раз(а)
Многопоточность — это способность программы или операционной системы поддерживать более одного пользователя одновременно, не требуя наличия нескольких копий программы, работающей на компьютере. Многопоточность также может обрабатывать несколько запросов от одного пользователя.
Каждый пользовательский запрос на программу или системную службу отслеживается как поток с отдельным идентификатором. Поскольку программы работают от имени начального запроса потока и прерываются другими запросами, рабочий статус начального запроса отслеживается до тех пор, пока работа не будет завершена. В этом контексте пользователем также может быть другая программа.
Для многопоточности необходимы высокая скорость центрального процессора ( ЦП ) и большой объем памяти . Один процессор выполняет фрагменты или потоки различных программ так быстро, что кажется, что компьютер одновременно обрабатывает несколько запросов.
Как работает многопоточность?
Чрезвычайно высокая скорость обработки современных микропроцессоров делает возможной многопоточность. Несмотря на то, что процессор выполняет только одну инструкцию за раз, потоки из нескольких программ выполняются так быстро, что кажется, что несколько программ выполняются одновременно.
Каждый цикл ЦП выполняет один поток, который связан со всеми другими потоками в своем потоке. Этот процесс синхронизации происходит так быстро, что кажется, что все потоки выполняются одновременно. Это можно описать как многопоточную программу, так как она может выполнять много потоков во время обработки.
Каждый поток содержит информацию о том, как он относится к программе в целом. В потоке асинхронной обработки одни потоки выполняются, а другие ждут своей очереди. Многопоточность требует, чтобы программисты обращали особое внимание на предотвращение условий гонки и взаимоблокировок.
Пример многопоточности
Многопоточность используется во многих различных контекстах. Один пример возникает, когда данные вводятся в электронную таблицу и используются для приложения реального времени.
При работе с электронной таблицей пользователь вводит данные в ячейку, и может произойти следующее:
- ширину столбцов можно регулировать;
- повторяющиеся клеточные элементы могут быть реплицированы; и
- электронные таблицы могут быть сохранены несколько раз по мере дальнейшей разработки файла.
Каждое действие происходит потому, что для каждого действия генерируется и обрабатывается несколько потоков, не замедляя общую работу приложения для работы с электронными таблицами.
Многопоточность против многозадачности против многопроцессорности
Многопоточность отличается от многозадачности и многопроцессорности . Однако многозадачность и многопроцессорность связаны с многопоточностью следующими способами:
- Многозадачность — это способность компьютера одновременно выполнять две или более программы. Многопоточность делает возможной многозадачность, когда программа разбивается на более мелкие исполняемые потоки. Каждый поток имеет программные элементы, необходимые для выполнения основной программы, и компьютер выполняет каждый поток по одному.
- Многопроцессорная обработка использует более одного ЦП для ускорения общей обработки и поддерживает многозадачность.
Многопоточность против параллельной обработки и многоядерных процессоров
Параллельная обработка — это когда два или более ЦП используются для обработки отдельных частей задачи. Несколько задач могут выполняться одновременно в системе параллельной обработки. Это отличается от использования одного процессора, когда одновременно выполняется только один поток, а задачи, составляющие поток, планируются последовательно.
Многоядерные процессоры на материнской плате ЦП имеют более одного независимого процессорного блока или ядра. Они отличаются от одноядерных процессоров, которые имеют только один процессор. Многоядерные процессоры обеспечивают повышенную скорость и время отклика по сравнению с одноядерными процессорами.
Многоядерные процессоры могут выполнять параллельно столько потоков, сколько имеется ядер ЦП. Это означает, что части задачи выполняются быстрее. В одноядерной системе потоки многопоточных приложений не выполняются параллельно. Вместо этого они используют одно ядро процессора.
Многопоточность
Многопото́чность — свойство платформы (например, операционной системы, виртуальной машины и т. д.) или приложения, состоящее в том, что процесс, порождённый в операционной системе, может состоять из нескольких потоков, выполняющихся «параллельно», то есть без предписанного порядка во времени. При выполнении некоторых задач такое разделение может достичь более эффективного использования ресурсов вычислительной машины.
Такие потоки называют также потоками выполнения (от англ. thread of execution ); иногда называют «нитями» (буквальный перевод англ. thread ) или неформально «тредами».
Сутью многопоточности является квазимногозадачность на уровне одного исполняемого процесса, то есть все потоки выполняются в адресном пространстве процесса. Кроме этого, все потоки процесса имеют не только общее адресное пространство, но и общие дескрипторы файлов. Выполняющийся процесс имеет как минимум один (главный) поток.
Многопоточность (как доктрину программирования) не следует путать ни с многозадачностью, ни с многопроцессорностью, несмотря на то, что операционные системы, реализующие многозадачность, как правило реализуют и многопоточность.
К достоинствам многопоточности в программировании можно отнести следующее:
- Упрощение программы в некоторых случаях за счет использования общего адресного пространства.
- Меньшие относительно процесса временны́е затраты на создание потока.
- Повышение производительности процесса за счет распараллеливания процессорных вычислений и операций ввода/вывода.
Типы реализации потоков
- Поток в пространстве пользователя. Каждый процесс имеет таблицу потоков, аналогичную таблице процессов ядра.
Достоинства и недостатки этого типа следующие: Недостатки
- Отсутствие прерывания по таймеру внутри одного процесса
- При использовании блокирующего системного запроса для процесса все его потоки блокируются.
- Сложность реализации
- Поток в пространстве ядра. Наряду с таблицей процессов в пространстве ядра имеется таблица потоков.
- «Волокна» (англ.fibers ). Несколько потоков режима пользователя, исполняющихся в одном потоке режима ядра. Поток пространства ядра потребляет заметные ресурсы, в первую очередь физическую память и диапазон адресов режима ядра для стека режима ядра. Поэтому было введено понятие «волокна» — облегчённого потока, выполняемого исключительно в режиме пользователя. У каждого потока может быть несколько «волокон».
Взаимодействие потоков
В многопоточной среде часто возникают проблемы, связанные с использованием параллельно исполняемыми потоками одних и тех же данных или устройств. Для решения подобных проблем используются такие методы взаимодействия потоков, как взаимоисключения (мьютексы), семафоры, критические секции и события
- Взаимоисключения (mutex, мьютекс) — это объект синхронизации, который устанавливается в особое сигнальное состояние, когда не занят каким-либо потоком. Только один поток владеет этим объектом в любой момент времени, отсюда и название таких объектов (от английского mutually exclusive access — взаимно исключающий доступ) — одновременный доступ к общему ресурсу исключается. После всех необходимых действий мьютекс освобождается, предоставляя другим потокам доступ к общему ресурсу. Объект может поддерживать рекурсивный захват второй раз тем же потоком, увеличивая счетчик, не блокируя поток, и требуя потом многократного освобождения. Таков, например, mutex в Win32 и KMUTEX в ядре Windows. Тем не менее есть и такие реализации, которые не поддерживают такое и приводят к взаимной блокировке потока при попытке рекурсивного захвата. Это FAST_MUTEX в ядре Windows и критическая секция в Win32.
- Семафоры представляют собой доступные ресурсы, которые могут быть приобретены несколькими потоками в одно и то же время, пока пул ресурсов не опустеет. Тогда дополнительные потоки должны ждать, пока требуемое количество ресурсов не будет снова доступно. Семафоры очень эффективны, поскольку они позволяют одновременный доступ к ресурсам. Семафор есть логическое расширение мьютекса — семафор со счетчиком 1 эквивалентен мьютексу, но счетчик может быть и более 1.
- События. Объект, хранящий в себе 1 бит информации «просигнализирован или нет», над которым определены операции «просигнализировать», «сбросить в непросигнализированное состояние» и «ожидать». Ожидание на просигнализированном событии есть отсутствие операции с немедленным продолжением исполнения потока. Ожидание на непросигнализированном событии приводит к приостановке исполнения потока до тех пор, пока другой поток (или же вторая фаза обработчика прерывания в ядре ОС) не просигнализирует событие. Возможно ожидание нескольких событий в режимах «любого» или «всех». Возможно также создания события, автоматически сбрасываемого в непросигнализированное состояние после пробуждения первого же — и единственного — ожидающего потока (такой объект используется как основа для реализации объекта «критическая секция»). Активно используются в MS Windows, как в режиме пользователя, так и в режиме ядра. Аналогичный объект имеется и в ядре Linux под названием kwait_queue.
- Критические секции обеспечивают синхронизацию подобно мьютексам за исключением того, что объекты, представляющие критические секции, доступны в пределах одного процесса. События, мьютексы и семафоры также можно использовать в однопроцессном приложении, однако реализации критических секций в некоторых ОС (например, Windows NT) обеспечивают более быстрый и более эффективный [1][2] механизм взаимно-исключающей синхронизации — операции «получить» и «освободить» на критической секции оптимизированы для случая единственного потока (отсутствия конкуренции) с целью избежать любых ведущих в ядро ОС системных вызовов. Подобно мьютексам объект, представляющий критическую секцию, может использоваться только одним потоком в данный момент времени, что делает их крайне полезными при разграничении доступа к общим ресурсам.
- Условные переменные (condvars). Сходны с событиями, но не являются объектами, занимающими память — используется только адрес переменной, понятие «содержимое переменной» не существует, в качестве условной переменной может использоваться адрес произвольного объекта. В отличие от событий, установка условной переменной в просигнализированное состояние не влечет за собой никаких последствий в случае, если на данный момент нет потоков, ожидающих на переменной. Установка события в аналогичном случае влечет за собой запоминание состояния «просигнализировано» внутри самого события, после чего следующие потоки, желающие ожидать события, продолжают исполнение немедленно без остановки. Для полноценного использования такого объекта необходима также операция «освободить mutex и ожидать условную переменную атомарно». Активно используются в UNIX-подобных ОС. Дискуссии о преимуществах и недостатках событий и условных переменных являются заметной частью дискуссий о преимуществах и недостатках Windows и UNIX.
- Порт завершения ввода-вывода (IO completion port, IOCP). Реализованный в ядре ОС и доступный через системные вызовы объект «очередь» с операциями «поместить структуру в хвост очереди» и «взять следующую структуру с головы очереди» — последний вызов приостанавливает исполнение потока в случае, если очередь пуста, и до тех пор, пока другой поток не осуществит вызов «поместить». Самой важной особенностью IOCP является то, что структуры в него могут помещаться не только явным системным вызовом из режима пользователя, но и неявно внутри ядра ОС как результат завершения асинхронной операции ввода-вывода на одном из дескрипторов файлов. Для достижения такого эффекта необходимо использовать системный вызов «связать дескриптор файла с IOCP». В этом случае помещенная в очередь структура содержит в себе код ошибки операции ввода-вывода, а также, для случая успеха этой операции — число реально введенных или выведенных байт. Реализация порта завершения также ограничивает число потоков, исполняющихся на одном процессоре/ядре после получения структуры из очереди. Объект специфичен для MS Windows, и позволяет обработку входящих запросов соединения и порций данных в серверном программном обеспечении в архитектуре, где число потоков может быть меньше числа клиентов (нет требования создавать отдельный поток с расходами ресурсов на него для каждого нового клиента).
- ERESOURCE. Мьютекс, поддерживающий рекурсивный захват, с семантикой разделяемого или эксклюзивного захвата. Семантика: объект может быть либо свободен, либо захвачен произвольным числом потоков разделяемым образом, либо захвачен всего одним потоком эксклюзивным образом. Любые попытки осуществить захваты, нарушающее это правило, приводят к блокировке потока до тех пор, пока объект не освободится так, чтобы сделать захват разрешенным. Также есть операции вида TryToAcquire — никогда не блокирует поток, либо захватывает, либо (если нужна блокировка) возвращает FALSE, ничего не делая. Используется в ядре Windows, особенно в файловых системах — так, например, любому кем-то открытому дисковому файлу соответствует структура FCB, в которой есть 2 таких объекта для синхронизации доступа к размеру файла. Один из них — paging IO resource — захватывается эксклюзивно только в пути обрезания файла, и гарантирует, что в момент обрезания на файле нет активного ввода-вывода от кэша и от отображения в память.
- Rundown protection. Полудокументированный (вызовы присутствуют в файлах-заголовках, но отсутствуют в документации) объект в ядре Windows. Счетчик с операциями «увеличить», «уменьшить» и «ждать». Ожидание блокирует поток до тех пор, пока операции уменьшения не уменьшат счетчик до нуля. Кроме того, операция увеличения может отказать, и наличие активного в данный момент времени ожидания заставляет отказывать все операции увеличения.
Критика терминологии
Перевод английского термина thread как «поток» в контексте, связанном с программированием, противоречит его же переводу «нить» в общеязыковом контексте, а также создает коллизии с термином stream («поток»).
Однако, термин «поток» связан с переводами иностранной технической литературы, выполненными в 1970-х годах издательством «Мир». В настоящее время в «академических кругах» (то есть в учебниках, методических пособиях, курсах ВУЗов, диссертациях и пр.) он считается эталонным. Термины же «нить», «тред» и т. п. считаются техническими жаргонизмами.
См. также
Ссылки
- Введение в технологии параллельного программирования (рус.)
- http://hardclub.donntu.edu.ua/projects/qt/qq/qq11-mutex.html#understandingmutexesandsemaphores
- Введение в многопоточность
Примечания
- ↑ Richter. «Джеффри Рихтер. Windows для профессионалов. Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows. 2001 год
- ↑MSDNhttp://msdn.microsoft.com/en-us/library/ms682530%28VS.85%29.aspx