Обзор моделей работы с потоками
Многие люди не понимают того, как многопоточность реализована в различных языках программирования. В наши времена многоядерных процессоров такое знание будет весьма полезно.
Вот вам небольшой обзор.
Начало (С и native threads)
Первая модель, которую мы рассмотрим — это стандартные потоки ОС (threads). Их поддерживает каждая современная ОС, несмотря на разницу в API. В принципе, поток — это процесс выполнения инструкций, который работает на выделенном процессоре, выполнение которого контролирует планировщик (scheduler) ОС, и который может быть заблокирован. Потоки создаются внутри процесса и пользуются общими ресурсами. Это означает, что, например, память и декскрипторы файлов, являются общими для всех потоков процесса. Подобный подход и принято называть native threads.
Linux позволяет использовать данные потоки с помощью библиотеки pthread. BSDs тоже поддерживают pthreads. Потоки Windows работают немного иначе, но базовый принцип тот же.
Java and Green Threads
Когда появилась Java, она принесла с собой другой тип многопоточности, который называется green threads. Green threads — это, по сути, имитация потоков. Виртуальная машина Java берёт на себя заботу о переключении между разными green threads, а сама машина работает как один поток ОС. Это даёт несколько преимуществ. Потоки ОС относительно дороги в большинстве POSIX-систем. Кроме того, переключение между native threads гораздо медленнее, чем между green threads.
Это всё означает, что в некоторых ситуациях green threads гораздо выгоднее, чем native threads. Система может поддерживать гораздо большее количество green threads, чем потоков OС. Например, гораздо практичнее запускать новый green thread для нового HTTP-соединения к веб-серверу, вместо создания нового native thread.
Однако есть и недостатки. Самый большой заключается в том, что вы не можете исполнять два потока одновременно. Поскольку существует только один native thread, только он и вызывается планировщиком ОС. Даже если у вас несколько процессоров и несколько green threads, только один процессор может вызывать green thread. И всё потому, что с точки зрения планировщика заданий ОС всё это выглядит одним потоком.
Начиная с версии 1.2 Java поддерживает native threads, и с тех пор они используются по умолчанию.
Python
Python — это один из моих любимейших скриптовых языков и он был одним из первых предложивших работу с потоками. Python включает в себя модуль, позволяющий манипулировать native threads, поэтому он может пользоваться всеми благами настоящей многопоточности. Но есть и одна проблема.
Python использует глобальную блокировку интерпретатора (Global Interpreter Lock, GIL). Эта блокировка необходима для того, чтобы потоки не могли испортить глобальное состояние интерпретатора. Поэтому две инструкции Python не могут выполняться одновременно. GIL снимается примерно каждые 100 инструкций и в этот момент другой поток может перехватить блокировку и продолжить своё выполнение.
Сперва это может показаться серьёзным недостатком, однако на практике проблема не столь велика. Любой заблокированный поток как правило освобождает GIL. Расширения С также освобождают её когда не взаимодействуют с Python/C API, поэтому интенсивные вычисления можно перенести в C и избежать блокировки выполняющихся потоков Python. Единственная ситуация, когда GIL действительно представляет проблему — это ситуация когда поток Python пытается выполняться на многоядерной машине.
Stackless Python — это версия Python, которая добавляет “tasklets” (фактически green threads). По их мотивам был создан модуль greenlet, который совместим со де-факто стандартом: cPython.
Ruby
Модель потоков Ruby постоянно меняется. Изначально Ruby поддерживал лишь собственную версию green threads. Это хорошо работает во многих сценариях, но не даёт пользоваться возможностями многопроцессорности.
JRuby перевёл потоки Ruby в стандартные потоки Java, которые, как мы выяснили выше, являются native threads. И это создало проблемы. Потокам Ruby нет необходимости взаимно синхронизоваться. Каждому потоку гарантируется, что никакой другой поток не получит доступа к используемому общему ресурсу. Подобное поведение было сломано в JRuby, так как native threads переключаются принудительно (preemptive) и поэтому любой поток может обратиться к общему ресурсу в произвольное время.
Из-за подобной несостыковки и желания получить native threads разработчиками C Ruby было решено, что Ruby будет переходить на них в версии 2.0. В состав Ruby 1.9 был включён новый интерпретатор, который добавил поддержку fibers, которое, насколько я знаю, являются более эффективной версией green threads.
Короче, модель потоков Ruby — это плохо документированная каша.
Perl
Perl предлагает интересную модель, которую Mozilla позаимстовала для SpiderMonkey, если я не ошибаюсь. Вместо использования глобальной блокировки интерпретатора как в Python, Perl сделал глобальное состояние локальным и фактически запускает новый интерпретатор для каждого нового потока. Это позволяет использовать настоящие native threads. Не обошлось и без пары загвоздок.
Во-первых, вы должны явно указывать переменные доступными для других потоков. Вот что происходит, когда всё становится локальным для потока. Приходится синхронизировать значения для межпоточного взаимодействия.
Во-вторых, создание нового потока стало очень дорогой операцией. Интерпретатор — большая штука и многократное копирование его съедает много ресурсов.
Erlang, JavaScript, C# and so on
Существует множество других моделей, которым время от времени находят применение. Например Erlang, использует архитектуру «ничего-общего» (shared nothing), которая стимулирует использование лёгких пользовательских процессов вместо потоков. Подобная архитектура просто великолепна для параллельного программирования, так как она устраняет всю головную боль насчёт синхронизации, а процессы настолько легки, что вы можете создать любое их количество.
JavaScript обычно не воспринимается как язык, который поддерживает работу с потоками, но она необходима и там. Модель работы с потоками в JavaScript очень похожа на ту, что используется в Perl.
C# использует native threads.
От себя: досаду на некоторую поверхностность статьи (которую я и сам осознаю) адресуйте автору. Я всего-навсего перевёл в меру своих скромных возможностей. ;) Буду рад уточнениям и дополнениям в комментариях.
От себя 2: по мотивам комментариев таки подправил пару фраз. Прости, автор! :)