Обзор моделей работы с потоками

http://justin.harmonize.fm/index.php/2008/09/threading-model-overview/
  • Перевод

Обзор моделей работы с потоками


Многие люди не понимают того, как многопоточность реализована в различных языках программирования. В наши времена многоядерных процессоров такое знание будет весьма полезно.
Вот вам небольшой обзор.

Начало (С и 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: по мотивам комментариев таки подправил пару фраз. Прости, автор! :)
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 56
  • +1
    Спасибо, написано здорово :)
    • 0
      А кто автор?
      • 0
        Исправил. Сначала никак не мог найти его имя, так как не подписывает статьи, подлец. :) Но потом увидел маленькую строчку в самом низу.
      • 0
        Для тех, кто еще не нашел: justin.harmonize.fm/index.php/2008/09/threading-model-overview/
        • +3
          Для тех, кто не знает: на этой странице, под статьёй, после звёздочки «избранное» как раз располагается ссылка на оригинал.
          • +2
            Спасибо, я не сразу заметил.
        • 0
          Спасибо за труд
          Можно развивать тему, показать примеры и сравнения, тогда будет настоящая исследовательская работа.
          Вообще говоря, развитие технологий показало что производительность будет наращиваться увеличением вычислительных узлов. Надо уже читать о кластерных вычислениях, там не все принципы многопоточности работать будут.
          ПС. в блог Разработка
          • 0
            Да, переносите в разработку.

            Жаль последний абзац скомканный немного.
            Так толком и не понятно есть ли многопоточно в JavaScript или нет :)
            (хотя распаралеливать задачи там можно, но насколько это будет «многопоточность», не ясно)

            Был приятно удивлен что многие динамические языки имеют инструменты по работе с многопоточностью.

            Кстати, а как с этим делом на php? а то я завно на нем уже не программирую и не слежу за новостями…

            • –1
              к сожалению, такого в PHP нет и вряд ли вообще будет.
              • 0
                Странно. Что их ограничевает? ведь Ruby смогли же.
                • +1
                  Есть, и уже давно. См. доку по pcntl_fork
                  • 0
                    Так эта функция запускает дочерний процесс. Это «немного» не поток…
                    Насколько я знаю, истинной многопоточности в PHP нет.
                    • 0
                      ну а в перле разве не примерно то же самое?
                      • +1
                        С Перлом не работал, так что не знаю…
                        Что касается PHP, то тут написано примерно следующее:
                        PHP does not have threading anywhere in its massive core. We can, however, fake it by relying on the underlying operating system’s multitasking abilities instead of PHP. This article will show you how.
                        PHP has no built in support for threading. But there can still be times when you’ve got lengthy code to run and idle CPU cyles you’d like to capitalize on. We can treat child processes as threads.

                        и далее
                        This isn’t true multithreading. We’re just running multiple processes, and facilitating communication between them. As each thread is a separate instance or part of your application, its entry point needs to re-include all code you’d normally need: functions and classes established at the time you create a thread are not automatically visable to the new thread. Performance isn’t great, as PHP has to re-parse every file multiple times.
                        • 0
                          Нет, в перле потоки — это паралелльные вычисления в рамках одного процесса, можно расшаривать переменные между потоками для совместного использования, использовать семафоры, лочить потоки из родителя и т.д… Хотя для каждого потока стартует свой интерпретатор. Т.е. потоки в перле — это не форк.
                          • 0
                            в Perl реализована «модель потоков интерпретаторов». в обсуждаемой статье более или менее корректно описана.

                            треды исполняются внутри процесса и могут обращаться к общему пулу данных (уже оговаривалось что процесс является контейнером для тредов).

                            запуск нового интерпретатора в контексте заданном статьей не стоит путать с запуском нового /usr/bin/perl или perl.exe :) для более глубокого понимания того что подразумевается под интерпретатором требуется чтение документации. с подрбностями можно ознакомиться в книге Ларри: в разделе «компиляция» есть глава «жизненный цикл программ на Perl». в итоге получаем самые настоящие потоки

                            при поверхностном анализе перлового многопоточного процесса треды можно разглядеть. например в виндовом processexplorer есть вкладка threads — из одного перлового процесса торчит несколько тредов :)

                            дорогое удовольствие только если треды рождаются в интенсивном цикле. в остальных случаях сущий пустяк
                      • 0
                        А зачем языку изначально предназначенному исключительно для веб-разработки нужны потоки? Лично не вижу никакого функционала, на реализацию которого мне было бы необходимо использовать потоки. А если вдруг кому-то они очень понадобятся, то будет реализовывать экстеншн основынный на pthreads ^__^
                      • 0
                        неправда
                        • 0
                          Где-то видел, что собираются добавить толи с 5.3, толи в 6.0. Может это лишь слухи, не знаю.
                        • 0
                          В JavaScript нет многопоточности (http://javascript.ru/blog/tenshi/mnogopotochnyi-yavaskript).

                          Вообще странно, что автор (не переводчик), поставил в один ряд C# и JavaScript.
                          В первом с этим всё чуть-ли не идеально, в последнем же этого вообще нет.
                          • 0
                            Многие PHP-разработчики полагают, что поскольку в стандарте PHP отсутствуют возможности для обработки потоков (thread), невозможно организовать работающее многозадачное PHP-приложение. Например, если приложению нужна информация с другого Web-сайта, оно должно остановиться и подождать завершения извлечения этой удаленной информации. Это не правда! Узнайте о внутрипроцессной (in-process) многозадачности в PHP, использующей функции stream_select и stream_socket_client.

                            www.ibm.com/developerworks/ru/library/os-php-multitask/index.html
                            • +1
                              Только это называется не «многозадачность», а «асинхронный ввод-вывод». Две большие разницы.
                              • 0
                                >а «асинхронный ввод-вывод»
                                select — это «неблокирующий ввод-вывод» (reactor) — приходит эвент о том что можно читать данные
                                «асинхронный ввод-вывод» (proactor) — делаешь запрос на чтение и система оповещает что прочитала и записала в буффер
                                • 0
                                  p.s. под select'ом — я имел в виду тот кусочик кода, который описан по ссылке выше :) а не сам вызов селекта, понимаю что это просто способ дожидаться эвентов, и там могут приходить как неблокирующие i/o эвенты, так и асинхронные…
                                  • 0
                                    Да, с такой поправкой согласен. Спасибо. :)
                                    • 0
                                      select — это мультиплексированный ввод-вывод. Его вызов всё таки блокирует программу, пока один из дискрипторов не будет готов к чтению. А если задать таймайт то это будет polling т. е. опрос.
                                    • 0
                                      и насколько я понял ту статью, там с помощью proc_open создают новые процессы, которые всё же попадают под понятие «многозадачность», а далее основным процессом с помощью «неблокирующего ввода-вывода» дожидаются от них результатов…
                                • 0
                                  переключение между нэйтив потоками дороже потому что переключение происходит через вход в режим ядра и выход из него.
                                  • 0
                                    Существует смешанная реализация. Потоки работают в режиме пользователя, но при системных вызовах переключаются в режим ядра. Переключение в режим ядра и обратно является ресурсоемкой операцией и отрицательно сказывается на производительности системы. Поэтому было введено понятие волокна — облегченного потока, выполняемого исключительно в режиме пользователя. У каждого потока может быть несколько волокон. Подобный тип многопоточности реализован в ОС Windows.
                                    [WIKI]
                                    • 0
                                      >Подобный тип многопоточности реализован в ОС Windows.
                                      и не только :)
                                      gnu portable threads — www.gnu.org/software/pth/
                                      ещё более легковесные
                                      protothreads — www.sics.se/~adam/pt/
                                      итд…
                                      • 0
                                        Следует добавить, что примитивы синхронизации для легковесных потоках работают только в пределах одного процесса.
                                        • 0
                                          наверное потому что планировщик тоже внутри процесса расположен, да?
                                          • +2
                                            Не сам планировщик, а та часть, которая отвечает за синхронизацию. Потому как Mutex, Events вызывают функции ядра. Кстати онные примитивы отлично подходят чтобы реализовать синхронизацию user mode и kernel mode кода. А, к примеру, CriticalSection основана на примитивном механизме spin lock, реализовать который можно в самом процессе.
                                            • 0
                                              CriticalSection это только виндоус прикол? :) реализуется насколько я понимаю просто установкой пользовательского мьютекса?
                                    • 0
                                      Интересная статья. Хотелось бы дополнения, конечно. Не совсем понятно как выглядит ситуация с потоками в функциональных языках (только Erlang и то, поверхностно), а ведь основную мощь «функциональные» программы приобретают при многопоточном исполнении.
                                      • +1
                                        Не знаю как там сейчас с ерлангом, но раньше было так — запускают нэтив трэды по кол-ву логических процессоров, далее пораждают свои легковесные процессы, в которых есть счётчик bif'ов(простые функции), после того как они выполняют отведёное для процесса число bif'ов, переключаются на другой процесс.
                                        кстати, erlang не полностью shared nothing(см. документацию на ets)
                                        • 0
                                          Насколько я понял из обсуждения полугодичной давности, в Erlang свой менеджер потоков, т.е. что-то типа green threads.
                                        • –4
                                          Вот интересная статья про многопоточность в РНР
                                          php.webconsulting.by/2008/05/02/mnogopotochnost-v-php-unix-funkciya-fork/
                                          • +2
                                            В Python 3k/2.6 проблему GIL и SMP решили кардинально: docs.python.org/dev/library/multiprocessing.html Ж)))
                                            • 0
                                              Сильно. Ждём Singularity/Midori, в которой это будет летать. :)
                                              • +1
                                                А что, в линуксе порождение процессов недостаточно быстрое?
                                            • +5
                                              Хорошо придумал: вот вам статья, но если она показалось вам поверхностной, то мопед не мой, я только разместил объяву :)

                                              Кроме того, имеются неточности в самой статье:

                                              > принципе, поток — это процесс, который исполняется на выделенном процессоре, выполнение которого контролирует планировщик (scheduler) ОС, и который может быть заблокирован.

                                              Поток — это не процесс, я думаю тут мешанина из терминов получилась. Лучше написать, что поток — это процесс выполнение на процессоре инструкций и т.д.

                                              > Linux позволяет использовать данные потоки с помощью библиотеки pthread. BSDs тоже поддерживает pthreads. Потоки Windows работают аналогично.

                                              Нууу, кто использовал pthreads и потоки win32, тот знает, что разница в их работе разительна. Например pthreads не умеют делать suspend/resume, win32 потоку нельзя сделать cancel, как в pthread, его можно только нелегальным образом убить. Также средства синхронизации разительно отличаются. Events нет в posix, conditional variables нет в win32 (только в висте) и т.д. То что и pthreads и win32 потоки — preemptive, зависит от операционной системы, а не от поточной модели. Далее можно было рассказать о preemptive и cooperative потоках.

                                              А так, еще один интересный факт. Carbon фреймворк на Mac OS X использует свой трид менеджер, который реализует cooperative модель, когда сама Mac OS X использует preemptive.
                                              • +1
                                                Процесс не выполняется на процессоре (он служит контейнером потоков). Процесс — группирует ресурсы (адресное пространство, потоки, ресурсы). Поток — исполняется на процессоре. Каждый процесс состоит хотя бы из одного потока. Процессы изолированы друг от друга, а потоки (находящиеся внутри одного процесса) могут использовать одно и тоже АП.

                                                • +2
                                                  >поток — это процесс, который исполняется на выделенном процессоре
                                                  А если у меня процессор только один, значит в системе может быть только один поток? Мне кажется статья скорее запутает, чем прояснит ситуацию, т.к. в ней полно неточностей (возможно, дело не в переводе).
                                                • +1
                                                  Да, «поток — это процесс» сильно режет по ушам.
                                                  • +1
                                                    > Хорошо придумал: вот вам статья, но если она показалось вам поверхностной, то мопед не мой, я только разместил объяву :)

                                                    У перевода есть свои ограничения, это очевидно. К сожалению мои знания не настолько широки, чтобы сделать детальный обзор по всем языкам самому.

                                                    > Поток — это не процесс, я думаю тут мешанина из терминов получилась.

                                                    Cам морщился.

                                                    > Нууу, кто использовал pthreads и потоки win32, тот знает, что разница в их работе разительна…

                                                    Вот это я и называю полезным комментарием. Тема интересная и «вечная». Почему бы не сделать более качественную статью по этой теме? Лично я буду рад почитать.
                                                    • +2
                                                      А так, еще один интересный факт. Carbon фреймворк на Mac OS X использует свой трид менеджер, который реализует cooperative модель, когда сама Mac OS X использует preemptive.

                                                      Это наверное потому, что в Mac OS Classic была только кооперативная модель, а карбон в первую очередь предназначен для легкого портирования приложений из Classic в OS X.

                                                      … да, кстати: thread = [θred], not [θri: d]
                                                      • 0
                                                        … да, кстати: thread = [θred], not [θri: d]

                                                        Люто, неистово плюсую. Что «триды», что «хидеры», как пенопластом по стеклу :(
                                                    • НЛО прилетело и опубликовало эту надпись здесь
                                                      • 0
                                                        Не ковыяй в носу, детей не будет! Да я неглубоко! ©УПИ
                                                        А наскольку глубоко, хотелось некоторым?:)
                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                      • 0
                                                        Ruby 1.9 опирается на системные потоки + GIL.
                                                        • 0
                                                          Точнее, там дело даже не в GIL'е. Некоторые из нативных библиотек не рассчитаны на многопоточность, поскольку ее во время их написания не ожидалось, поэтому полноценная параллельность пока что зарезана искусственно. Но над этим работают.
                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                          • 0
                                                            стало интересно какие потоки имеют ввиду авторы Хрома
                                                            • +2
                                                              Добавлю про Java и Green Threads.

                                                              Старые версии Linux не поддерживали Posix Threads, поэтому многопоточность реализовывалась банальным fork с запуском дочернего процесса. Для JVM это было прозрачно, но в системе болталась куча процессов, и запуск запуск треда занимал значительное время.

                                                              Обычная Sun JRE использует native threads. Однако в реализации под Solaris по желанию можно использовать библиотеку green threads самой ОС. В этом случае многопоточность реализуется по правилу m: n, то есть m native threads обслуживают по n green threads. Причем все это делается прозрачно для написанного кода. Некоторые другие имплементации JVM тоже поддерживают green threads (напр. JRockit).

                                                              Green threads уместно использовать в задачах с большим числом потоков. Классической задачей из книжки является тредовый сервер. На каждый установленный с клиентом коннекшн открывается новый тред, который обрабатывает запросы. Проблема в том, что при большом числе открытых потоков (>1000) планировщик задач ОС тратит основную часть времени только на бесполезное переключение между ними, несмотря на то, что сами потоки большую часть времени «спят» при чтении из сокета. Спасают green threads, где виртуальные потоки вешаются на один native thread.

                                                              Как работают green threads? Поскольку работает только один native thread, то в каждый момент времени может выполняться только один поток из виртуальных потоков. Переключение между потоками производится в момент, когда поток выполняет блокирующую операцию (синхронизация, I/O, sleep(), wait(), etc...). Более того, некоторые JVM искусственно вставляют в код потока операции переключения. В модели m: n после блокирующей операции виртуальный поток может быть продолжен другим m из имеющихся native потоков, поэтому выполнение всех потоков в этой модели происходит более-менее равномерно.

                                                              P.S. Более удачное решение для сервера в java является non-blocking I/O + thread pool
                                                              P.P.S. карма не позволяет писать статьи… :(

                                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.