Давайте синхронизировать потоки в Python
Давайте синхронизировать потоки в Python
Для меня это был волшебный момент, внезапный инсайт, когда я впервые узнал о многопоточности. Меня восхитила сама возможность параллельного выполнения действий, (хотя важно заметить, что на компьютере с одноядерным процессором вычисления выполняются не строго параллельно, причем вычисления в Python распараллеливаются частично из-за наличия GIL-концепции ‑ способа синхронизации потоков в Python. Многопоточность открывает новые возможности для вычислений, но вслед за могуществом приходит и ответственность.
Имеется ряд проблем, возникающих при использовании многопоточности – попытка множества потоков получить доступ к одному и тому же фрагменту данных может привести к проблемам несовместимости или получению искаженной информации (например, фраза HWeolrldo вместо Hello World на консоли). Подобные проблемы возникают, когда компьютеру не указан способ организации потоков.
Как правильно приказать компьютеру синхронизировать потоки? Для этого используются примитивы синхронизации — простые программные механизмы, обеспечивающие гармоничное взаимодействие потоков друг с другом.
В этом посте представлены некоторые популярные примитивы синхронизации в Python, определенные в стандартном модуле threading.py . Большинство методов блокировки (то есть методов, блокирующих выполнение конкретного потока до тех пор, пока не выполнится условие) этих примитивов предоставляют дополнительные функции тайм-аута, но для простоты изложения они не будут здесь упоминаться. Также ради простоты описаны только основные функции этих объектов. Предполагается, что читатель обладает базовыми знаниями многопоточности в Python.
Изучим Locks , RLocks , Semaphores , Events , Conditions и Barriers . Разумеется, можно создавать собственные примитивы пользовательской синхронизации, используя описанные мной в качестве подклассов. Начнем с Locks как с простейшего из примитивов и постепенно перейдем к более сложным.
Locks
Примитивы Lock вероятно, простейшие примитивы в Python. Для Lock возможны только два состояния ‑ заблокирован и разблокирован. Примитив создается в разблокированном состоянии и содержит два метода – acquire() и release() . Метод acquire() блокирует Lock и выполнение блока до тех пор, пока метод release() из другой сопрограммы не разблокирует его. Затем он снова блокирует Lock и возвращает значение True. Метод release() вызывается только в заблокированном состоянии – устанавливает состояние разблокировки и немедленно возвращает управление. Вызов release() в разблокированном состоянии приводит к RunTimeError .
Вот код, который использует примитив Lock для безопасного доступа к общей переменной:
Этот код просто дает результат в виде числа 3, но теперь мы уверены, что две функции не изменяют значение глобальной переменной g одновременно, хотя работают в двух разных потоках. Таким образом, Lock могут использоваться для предотвращения противоречивости в выходных данных, позволяя каждый раз только одному потоку изменять данные.
RLocks
Стандартный Lock не знает, какой поток блокируется в данный момент. Если блокировка сохраняется, блокируется любой из потоков, пытающихся получить доступ, даже если этот тот же самый поток, который уже удерживает блокировку. Именно для таких случаев и используется RLock — блокировка повторного входа. Вы можете расширить код в следующем фрагменте, добавив выходные инструкции для демонстрации возможностей RLock предотвращать нежелательную блокировку.
Возможно рекурсивное использование RLock — когда родительский вызов функции блокирует вложенный вызов. Таким образом RLock используются для вложенного доступа к общим ресурсам.
Семафоры
Семафоры – это просто дополнительные счетчики. Вызов acquire() будет блокироваться семафором только после превышении определенного количества запущенных потоков acquire() . Значение соответствующего счетчика уменьшается на каждый вызов на acquire() и увеличивается на каждый вызов release() . Значение ValueError будет возникать, если вызовы release() будут пытаться увеличивать значение счетчика после достижения заданного максимального значения (количества потоков, которые допустимые семафором acquire() до применения блокировки). Следующий код демонстрирует использование семафоров для простой задачи производитель-потребитель.
Модуль threading также предоставляет простой класс Semaphore . Класс Semaphore предоставляет счетчик, позволяющий вызывать release() произвольное количество раз. Однако, чтобы избежать ошибок при программировании, лучше использовать BoundedSemaphore , который вызывает ошибку, если вызов release() пытается увеличивать значение счетчика выше заданного максимального значения.
Семафоры, как правило, используются для ограничения ресурсов, например, ограничения доступа к серверу, допуская обрабатывать только 10 клиентов за раз. В этом случае несколько потоков соединений конкурируют за ограниченный ресурс (в нашем примере это сервер).
Events
Примитив синхронизации Event работает как простой коммуникатор между потоками. Он использует внутренний флаг, который потоки могут устанавливать set() или сбрасывать clear() . Другие потоки могут ожидать wait() установки внутреннего флага set() . Метод wait() блокирует пока флаг не станет истинным. Следующий фрагмент демонстрирует, как Event могут использоваться для запуска действий.
Conditions
Объект Condition является просто усовершенствованным вариантом объекта Event . Он тоже работает как коммуникатор между потоками и может применяться для уведомления notify() других потоков об изменении состояния программы. Например, его можно использовать для сигнализации доступности ресурса. Другие потоки также должны получать условие acquire() (и, следовательно, связанное с ним блокирование) до ожидания wait() для удовлетворения условия. Кроме того, поток должен освободить release() по условию Condition после завершения связанных с ним действий, так что другие потоки могут получить условие для своих целей. Нижеследующий код демонстрирует реализацию другой простой проблемы производитель-потребитель с помощью объекта Condition .
Возможны и другие применения для Condition . Например, при разработке потокового API, который уведомляет клиента о времени начала доступности данных.
Barriers
Барьеры являются простыми примитивами синхронизации и используются потоками для ожидании друг друга. Каждый поток пытается передать барьер с помощью вызова метода wait() , который будет блокироваться, пока все потоки не создадут этот вызов. Как только это произойдет, потоки будут запущены одновременно. Следующий фрагмент демонстрирует использование Barrier .
Для барьеров можно найти множество применений, одним из которых может стать синхронизация работы сервера и клиента, поскольку серверу часто приходится ожидать клиента после инициализации.
На этом завершим обсуждение примитивов синхронизации в Python.
Этот пост написан как решение упражнения в книге «Программирование приложений на основе ядра Python» Уэсли Чана. Если этот пост вам понравился, познакомьтесь с другими моими работами из этой книги на GitHub. Исходные коды из этой статьи также доступны в моем профиле.
Когда появился и зачем придумали
Delphi — среда программирования на основе языка Object Pascal. Он считается наследником Turbo Pascal, а тот — «чистого» Pascal Никлауса Вирта, созданного в 1970 году.
Изначальный Pascal обладал наиболее быстрым компилятором, но его IDE работала только на базе DOS. Когда появилась Windows, возникла потребность создания среды программирования для этой платформы. Раньше чтобы создать элементарную программу, разработчикам приходилось писать несколько страниц кода. В компании Borland понимали, что нужно как-то облегчить их жизнь, поэтому решили доработать Pascal, который к тому моменту уже почти не использовался.
Но изменения оказались настолько серьёзными, что в результате появился новый язык программирования — Delphi. В России он начал применяться в конце 1993 году и сразу обрёл большую популярность.
История языка
Object Pascal — результат развития языка Турбо Паскаль, который, в свою очередь, развился из языка Паскаль. Паскаль был полностью процедурным языком, Турбо Паскаль, начиная с версии 5.5, добавил в Паскаль объектно-ориентированные свойства, а в Object Pascal — динамическую идентификацию типа данных с возможностью доступа к метаданным классов (то есть к описанию классов и их членов) в компилируемом коде, также называемом интроспекцией — данная технология получила обозначение RTTI. Так как все классы наследуют функции базового класса TObject, то любой указатель на объект можно преобразовать к нему, после чего воспользоваться методом ClassType и функцией TypeInfo, которые и обеспечат интроспекцию.
Также отличительным свойством Object Pascal от С++ является то, что объекты по умолчанию располагаются в динамической памяти. Однако можно переопределить виртуальные методы NewInstance и FreeInstance класса TObject. Таким образом, абсолютно любой класс может осуществить «желание» «где хочу — там и буду лежать». Соответственно организуется и «многокучность».
Object Pascal (Delphi) является результатом функционального расширения Turbo Pascal [4] .
Delphi оказал огромное влияние на создание концепции языка C# для платформы .NET. [источник не указан 230 дней] Многие его элементы и концептуальные решения вошли в состав С#. Одной из причин называют переход Андерса Хейлсберга, одного из ведущих разработчиков Дельфи, из компании Borland Ltd. в Microsoft Corp.
- Версия 8 способна генерировать байт-код исключительно для платформы .NET. Это первая среда, ориентированная на разработку мультиязычных приложений (лишь для платформы .NET);
- Последующие версии (обозначаемые годами выхода, а не порядковыми номерами, как это было ранее) могут создавать как приложения Win32, так и байт-код для платформы .NET.
Delphi for .NET — среда разработки Delphi, а также язык Delphi (Object Pascal), ориентированные на разработку приложений для .NET.
Первая версия полноценной среды разработки Delphi для .NET — Delphi 8. Она позволяла писать приложения только для .NET. Delphi 2006 поддерживает технологию MDA с помощью ECO (Enterprise Core Objects) версии 3.0.
В марте 2006 года компания Borland приняла решение о прекращении дальнейшего совершенствования интегрированных сред разработки JBuilder, Delphi и C++ Builder по причине убыточности этого направления. Планировалась продажа IDE-сектора компании. Группа сторонников свободного программного обеспечения организовала сбор средств для покупки у Borland прав на среду разработки и компилятор [5] .
Однако в ноябре того же года было принято решение отказаться от продажи IDE бизнеса. Тем не менее, разработкой IDE продуктов теперь будет заниматься новая компания — CodeGear, которая будет финансово полностью подконтрольна Borland.
В августе 2006 года Borland выпустил облегченную версию RAD Studio под именем Turbo: Turbo Delphi (для Win32 и .NET), Turbo C#, Turbo C++.
В марте 2008 года было объявлено о прекращении развития этой линейки продуктов.
В марте 2007 года CodeGear порадовала пользователей обновленной линейкой продуктов Delphi 2007 for Win32 и выходом совершенно нового продукта Delphi 2007 for PHP.
В июне 2007 года CodeGear представила свои планы на будущее, то есть опубликовала так называемый roadmap [6] .
25 августа 2008 года компания Embarcadero, новый хозяин CodeGear, опубликовала пресс-релиз на Delphi for Win32 2009 [7] . Версия привнесла множество нововведений в язык, как то [8] :
- По умолчанию полная поддержка Юникода во всех частях языка, VCL и RTL; замена обращений ко всем функциям Windows API на юникодные аналоги (то есть MessageBox вызывает MessageBoxW, а не MessageBoxA). , они же generics. .
- Новая директива компилятора $POINTERMATH [ON|OFF].
- Функция Exit теперь может принимать параметры в соответствии с типом функции.
Вышедшая в 2011 году версия Delphi XE2 добавила компилятор Win64 и кросс-компиляцию для операционных систем фирмы Apple.
Типы данных Delphi
Теперь обсудим типы данных Delphi, которые программист использует при написании программы. Любая программа на Delphi может содержать данные многих типов:
- целые и дробные числа,
- символы,
- строки символов,
- логические величины.
Целый тип Delphi
Библиотека языка Delphi включает в себя 7 целых типов данных: Shortint, Smallint, Longint, Int64, Byte, Word, Longword, характеристики которых приведены в таблице ниже.
Вещественный тип Delphi
Кроме того, в поддержку языка Delphi входят 6 различных вещественных типов (Real68, Single, Double, Extended, Comp, Currency), которые отличаются друг от друга, прежде всего, по диапазону допустимых значений, по количеству значащих цифр, по количеству байт, которые необходимы для хранения некоторых данных в памяти ПК (характеристики вещественных типов приведены ниже). Также в состав библиотеки языка Delphi входит и наиболее универсальный вещественный тип — тип Real, эквивалентный Double.
Символьный тип Delphi
Кроме числовых типов, язык Delphi располагает двумя символьными типами:
Тип Ansichar — символы c кодировкой ANSI, им ставятся в соответствие числа от 0 до 255;
Тип Widechar — символы с кодировкой Unicode, им ставятся в соответствие числа от 0 до 65 535.
Строковый тип Delphi
Строковый тип в Delphi обозначается идентификатором string. В языке Delphi представлены три строковых типа:
Тип Shortstring — присущ статически размещаемым в памяти ПК строкам, длина которых изменяется в диапазоне от 0 до 255 символов;
Тип Longstring — этим типом обладают динамически размещаемые в памяти ПК строки с длиной, ограниченной лишь объемом свободной памяти;
Тип WideString — тип данных, использующийся для того, чтобы держать необходимую последовательность Интернациональный символов, подобно целым предложениям. Всякий символ строки, имеющей тип WideString, представляет собой Unicode-символ. В отличие от типа Shortstring, тип WideString является указателем, который ссылается на переменные.
Логический тип Delphi
Логический тип соответствует переменным, которые могут принять лишь одно из двух значений: true, false. В языке Delphi логические величины обладают типом Boolean. Вам были представлены основные типы данных Delphi. Движемся дальше.
Записки IT специалиста
- Автор: Уваров А.С.
- 28.10.2021
После того, как вы освоили базовые принципы работы с Linux, позволяющие более-менее уверенно чувствовать себя в среде этой операционной системы, следует начать углублять свои знания, переходя к более глубоким и фундаментальным принципам, на которых основаны многие приемы работы в ОС. Одним из важнейших является понятие потоков, которые позволяют передавать данные от одной программы к другой, а также конвейера, позволяющего выстраивать целые цепочки из программ, каждая из которых будет работать с результатом действий предыдущей. Все это очень широко используется и понимание того, как это работает важно для любого Linux-администратора.
Прежде всего определимся с терминами. Мы часто говорим: «консоль», «терминал», «командная строка» — не сильно задумываясь о смысле этих слов и подразумевая под этим в большинстве своем CLI — интерфейс командной строки. Во многих случаях такое упрощение допустимо и широко используется в профессиональной среде. Но в данном случае точный смысл этих терминов имеет значение для правильного понимания происходящих процессов.
Стандартные потоки
Начнем с основного понятия — терминал. Он уходит корнями в далекое (по компьютерным меркам) прошлое и обозначает собой оконечное устройство, предназначенное для взаимодействия оператора и компьютера, к одному компьютеру может быть присоединено несколько терминалов, каждый из которых работает самостоятельно и независимо от других. Смысл современного терминала, а приложение для работы с командной строкой называется в Linux именно так, не изменился и сегодня, хотя, если быть точными, его название — эмулятор терминала.
Данное приложение все также эмулирует оконечное устройство, предназначенное для взаимодействия пользователя с компьютером. Точно также мы можем запустить сразу несколько терминалов, каждый из которых будет работать независимо. Кроме того, следует понимать, что терминал может быть запущен как локально, так и удаленно, способ подключения к компьютеру может быть разным, но свойства терминала от этого не изменяются.
Работая с терминалом нам нужно каким-то образом передавать ему команды и получать результаты. Для этого предназначена консоль — совокупность устройств ввода-вывода обеспечивающих взаимодействие пользователя и компьютера. В качестве консоли на современных ПК выступают клавиатура и монитор, но это только один из возможных вариантов, например, в самых первых моделях терминалов в качестве консоли использовался телетайп. Консоль всегда подключена к текущему рабочему месту, в то время как терминал может быть запущен и удаленно.
Но в нашей схеме все еще чего-то не хватает. При помощи консоли мы вводим определенные команды и передаем их в терминал, а дальше? Терминал — это всего лишь оконечное устройство для взаимодействия с компьютером, но не сам компьютер, выполнять команды или производить какие-либо другие вычисления он не способен. Поэтому на сцену выходит третий компонент — командный интерпретатор. Это специальная программа, обеспечивающая базовое взаимодействие пользователя и ОС, а также дающая возможность запускать другие программы. В большинстве Linux-дистрибутивов командным интерпретатором по умолчанию является bash.
Теперь все становится на свои места. Для каждого сеанса взаимодействия пользователя и компьютера создается отдельный терминал, внутри терминала работает специальная программа — командный интерпретатор. При помощи консоли пользователь передает командному интерпретатору или запущенной с его помощью программе входящие данные и получает назад результат их работы. Осталось разобраться каким именно образом это происходит.
Для взаимодействия запускаемых в терминале программ и пользователя используются стандартные потоки ввода-вывода, имеющие зарезервированный номер (дескриптор) зарезервированный на уровне операционной системы. Всего существует три стандартных потока:
- stdin (standard input, 0) — стандартный ввод, по умолчанию нацелен на устройство ввода текущей консоли (клавиатура)
- stdout (standard output, 1) — стандартный вывод, по умолчанию нацелен на устройство вывода текущей консоли (экран)
- stderr (standard error, 2) — стандартный вывод ошибок, специальный поток для вывода сообщения об ошибках, также направлен на текущее устройство вывода (экран)
Как мы помним, в основе философии Linux лежит понятие — все есть файл. Стандартные потоки не исключение, с точки зрения любой программы — это специальные файлы, которые открываются либо на чтение (поток ввода), либо на запись (поток вывода). Это вызывает очевидный вопрос, а можно ли вместо консоли использовать файлы? Да, можно и здесь мы вплотную подошли к понятию перенаправления потоков.
Перенаправление потоков
Начнем с наиболее простого и понятного — потока вывода. В него программа отправляет результат своей работы, и он отображается на подключенном к консоли устройстве вывода, в современных системах это экран. Допустим мы хотим получить список файлов в директории, для этого мы набираем на устройстве ввода консоли команду:
Которая через стандартный поток ввода будет доставлена командному интерпретатору, тот запустит указанную программу и передаст ей требуемые аргументы, а результат вернет через стандартный поток вывода на устройство отображения информации.
Но что, если мы хотим сохранить результат работы команды в файл? Нет ничего проще, воспользуемся перенаправлением, для этого следует использовать знак > .
На экран теперь не будет выведено ничего, но весь вывод команды окажется в файле result, который мы можем прочитать следующим образом:
При таком перенаправлении вывода файл-приемник каждый раз будет создаваться заново, т.е. будет перезаписан. Это очень важный момент, сразу и навсегда запомните > — всегда перезаписывает файл!
Можно ли этого избежать? Можно, для того, чтобы перенаправленный поток был дописан в конец уже существующего файла используйте для перенаправления знак >> .
Немного изменим последовательность команд:
Теперь в файл попал вывод сразу двух команд, при этом, обратите внимание, первой командой мы перезаписали файл, а второй добавили в него содержимое из стандартного потока вывода.
Пойдем дальше. Как видим в выводе кроме списка файлов присутствуют строки «итого», нам они не нужны, и мы хотим от них избавиться. В этом нам поможет утилита grep, которая позволяет отфильтровать строки согласно некому выражению. Например, можно сделать так:
В целом результат достигнут, но ценой трех команд и наличием одного промежуточного файла. Можно ли сделать проще?
До этого мы перенаправляли поток вывода, но тоже самое можно сделать и с потоком ввода, используя для этого знак < . Например, мы можем сделать так:
Но это ничего не изменит, поэтому мы пойдем другим путем и перенаправим на ввод одной команды вывод другой:
На первый взгляд выглядит несколько сложно, но обратимся к man по grep:
В качестве паттерна мы используем rw, который есть в каждой интересующей нас строке, а в качестве файлов отдаем стандартный файл потока ввода, содержимого которого является результатом работы команды, указанной в скобках. А можно направить результат этой команды в файл? Конечно, можно:
В последней команде мы перенаправили не только потоки ввода, но и поток вывода, при этом нужно понимать, что все перенаправления относятся к первой команде, иначе можно подумать, что в result будет выведен результат работы ls -l dir2, однако это неверно.
Немного особняком стоит стандартный поток вывода ошибок, допустим мы хотим получить список файлов несуществующей директории с перенаправлением вывода в файл, но сообщение об ошибке мы все равно получим на экран.
Почему так? Да потому что вывод ошибок производится в отдельный поток, который мы никуда не перенаправляли. Если мы хотим подавить вывод сообщений об ошибках на экран, то можно использовать конструкцию:
В данном примере весь вывод стандартного потока ошибок будет перенаправлен в пустое устройство /dev/null.
Но можно пойти и другим путем, перенаправив поток ошибок в стандартный поток вывода:
В этом случае мы перенаправили поток вывода об ошибках в стандартный поток вывода и сообщение об ошибке не было выведено на экран, а было записано в файл, куда мы перенаправили стандартный поток вывода.
Конвейер
В предыдущих примерах мы научились перенаправлять стандартные потоки ввода-вывода и даже частично коснулись вопроса о направлении вывода одной команды на вход другой. А почему бы и нет? Потоки стандартные, это позволяет использовать вывод одной команды как ввод другой. Это еще один очень важный механизм, позволяющий раскрыть всю мощь Linux в реализации очень сложных сценариев достаточно простым способом.
Для того, чтобы перенаправить вывод одной программы на вход другой используйте знак | , на жаргоне «труба».
Самый простой пример:
Первая команда выведет список всех установленных пакетов, вторая отфильтрует только те, в наименовании которых есть строка «gnome».
Длинна конвейера ограничена лишь нашей фантазией и здравым смыслом, никаких ограничений со стороны системы в этом нет. Но также в Linuх нет и единственно верных путей, каждую задачу можно решить самыми различными способами. Возвращаясь к получению списка файлов двух директорий мы можем сделать так:
Какой из этих способов лучше? Любой, Linux ни в чем не ограничивает пользователей и предоставляет им много путей для решения одной и той же задачи.
Еще один важный момент, если вы повышаете права с помощью команды sudo, то данное повышение прав через конвейер не распространяется. Например, если мы решим выполнить:
То первая команда будет выполнена с правами суперпользователя, а вторая с правами запустившего терминал пользователя.
Дополнительные материалы:
Помогла статья? Поддержи автора и новые статьи будут выходить чаще:
Или подпишись на наш Телеграм-канал:
Отличия от предыдущих версий
Делфи 7 является отдельной ветвью в истории Borland, потому что до сих пор активно используется опытными разработчиками. Основными особенностями этой версии являются:
- Возможность поддержки Microsoft.NET
- Наличие средств моделирования UML
- Возможность разработки Web-проектов
- DBExpress в этой версии поддерживает такие СУБД, как Oracle9i, MySQL 3.23.49, Informix SE, InterBase 6.5, DB2 7.2.
- Обновленный интерфейс приложений DataSnap.
- Наличие новой компоненты Rave Reports, при помощи которой можно создавать качественные отчеты, а также иметь доступ к данным dbExpress, ADO и BDE.
- Наличие поддержки тем и элементов управления Windows XP, которые перешли (унаследовались) в версии Windows 7 и Windows 8.
- Поддержка операционной системы Linux.
5. Создание функций приостановки
Сопрограмма может быть приостановлена только с помощью функции приостановки. Поэтому большинство сопрограмм имеют внутри себя вызовы хотя бы одной такой функции.
Чтобы создать функцию приостановки, все, что вам нужно сделать, это добавить модификатор suspend к обычной функции. Вот типичная функция приостановки, выполняющая HTTP-запрос GET с использованием библиотеки khttp:
Обратите внимание, что функция приостановки может быть вызвана только сопрограммой или другой функцией приостановки. Если вы попытаетесь вызвать его из другого места, ваш код не сможет скомпилироваться.
Потоки (threads) и многопоточное выполнение программ (multi-threading)
Презентацию к данной лекции Вы можете скачать здесь.
Введение
Многопоточность (multi- threading ) – одна из наиболее интересных и актуальных тем в данном курсе и, по-видимому, в области ИТ вообще, и, кроме того, одна из излюбленных тем автора. Актуальность данной темы особенно велика, в связи с широким распространением многоядерных процессоров. В лекции рассмотрены следующие вопросы:
- Исторический обзор многопоточности
- Модели многопоточного исполнения
- Проблемы, связанные с потоками
- Потоки в POSIX (Pthreads)
- Потоки в Solaris 2
- Потоки в Windows 2000/XP
- Потоки в Linux
- Потоки в Java и .NET.
Однопоточные и многопоточные процессы
К сожалению, до сих пор мышление многих программистов при разработке программ остается чисто последовательным. Не учитываются широкие возможности параллелизма , в частности, многопоточности. Последовательный (однопоточный) процесс – это процесс, который имеет только один поток управления ( control flow ), характеризующийся изменением его счетчика команд . Поток (thread) – это запускаемый из некоторого процесса особого рода параллельный процесс, выполняемый в том же адресном пространстве, что и процесс-родитель. Схема организации однопоточного и многопоточного процессов изображена на рис. 10.1.
Как видно из схемы, однопоточный процесс использует, как обычно, код, данные в основной памяти и файлы, с которыми он работает. Процесс также использует определенные значения регистров и стек , на котором исполняются его процедуры. Многопоточный процесс организован несколько сложнее. Он имеет несколько параллельных потоков, для каждого из которых ОС создает свой стек и хранит свои собственные значения регистров. Потоки работают в общей основной памяти и используют то же адресное пространство , что и процесс-родитель, а также разделяют код процесса и файлы.
Многопоточность имеет большие преимущества:
- Увеличение скорости (по сравнению с использованием обычных процессов). Многопоточность основана на использовании облегченных процессов (lightweight processes),работающих в общем пространстве виртуальной памяти. Благодаря многопоточности, не возникает больше неэффективных ситуаций, типичных для классической системы UNIX, в которой каждая команда shell (даже команда вывода содержимого текущей директории ls исполнялась как отдельный процесс, причем в своем собственном адресном пространстве. В противоположность облегченным процессам, обычные процессы (имеющие собственное адресное пространство) часто называют тяжеловесными (heavyweight).
- Использование общих ресурсов. Потоки одного процесса используют общую память и файлы.
- Экономия. Благодаря многопоточности, достигается значительная экономия памяти, по причинам, объясненным выше. Также достигается и экономия времени, так как переключение контекста на облегченный процесс, для которого требуется только сменить стек и восстановить значения регистров, значительно быстрее, чем на обычный процесс (см. «Методы взаимодействия процессов» ).
Использование мультипроцессорных архитектур. Это особенно важно в настоящее время, в период широкого использования многоядерных гибридных и многопроцессорных систем. Именно многопоточность программ, основанная на многоядерности процессора, дает возможность, наконец, почувствовать реальные преимущества параллельного выполнения.
История многопоточности
Как небезынтересно отметить, один из первых шагов на пути к широкому использованию многопоточности, по-видимому, был сделан в 1970-е годы советскими разработчиками компьютерной аппаратуры и программистами. МВК «Эльбрус-1», разработанный в 1979 году, поддерживал в аппаратуре и операционной системе эффективную концепцию процесса, которая была близка к современному понятию облегченного процесса. В частности, процесс в «Эльбрусе» однозначно характеризовался своим стеком. Иначе говоря, все процессы были облегченными и исполнялись в общем пространстве виртуальной памяти – других процессов в «Эльбрусе» просто не было!
Концепция многопоточности начала складываться, по-видимому, с 1980-х гг. в системе UNIX и ее диалектах. Наиболее развита многопоточность была в диалекте UNIX фирмы AT&T, на основе которого, как уже отмечалось в общем историческом обзоре, была разработана система Solaris. Все это отразилось и в стандарте POSIX , в который вошла и многопоточность, наряду с другими базовыми возможностями UNIX .
Далее, в середине 1990-х гг. была выпущена ОС Windows NT, в которую была также включена многопоточность.
Однако в разных операционных системах API для многопоточности существенно отличались. Поэтому многопоточные программы, даже написанные на языках высокого уровня , оказались не переносимыми с одной платформы на другую, что, разумеется, создавало большие неудобства.
По-видимому, именно по причине различий в спецификациях и реализациях многопоточности в различных системах профессор Бьярн Страуструп не включил многопоточность в созданный им язык C++, ставший столь популярным, и его базовый набор библиотек. Программисты на языке C++ были вынуждены по-прежнему использовать многопоточность на уровне системных вызовов и библиотек конкретных операционных систем.
Важный шаг вперед сделали авторы языка Java и Java -технологии, первая версия реализации которых была выпущена в 1995 г. Именно в Java впервые многопоточность была реализована на уровне конструкций языка и базовых библиотек. В частности, в Java введен класс Thread,представляющий поток , и операции над ним в виде специальных методов и конструкций языка.
Платформа . NET , появившаяся в 2000 г., предложила свой механизм многопоточности, который фактически является развитием идей Java .
Различие подходов к многопоточности в разных ОС и на разных платформах разработки программ сохраняется и до настоящего времени, что приходится постоянно учитывать разработчикам. Для прикладных программ мы рекомендуем реализовывать многопоточность на платформе Java или . NET , что наиболее удобно и позволяет использовать высокоуровневые понятия и конструкции. Однако в нашем курсе, посвященном операционным системам, мы, естественно, больше внимания уделяем системным вопросам многопоточности и ее реализации в операционных системах.