адресном пространстве и с общими прочими ресурсами, была легендарная ОС Multics. Мне же хочется высказаться по поводу легковесных процессов в современных вариантах UNIX.
К написанию заметки меня побудил выход новой книги, посвященной внутренней организации различных разновидностей ОС UNIX (Uresh Vahalia. UNIX Internals: The New Frontiers. Prentice Hall, 1996. ISBN 0-13-101908-2). В книге описываются методы управления памятью, планирования процессов, организация разнообразных файловых систем и т.д. Конечно, Вахалия не забыл и о легковесных процессах.
Он отмечает, что несмотря на путаницу в терминологии, в различных реализациях легковесных процессов выделяются три класса. Базовым являются ядерные нити. В мире UNIX это не новость. Когда в пользовательском процессе происходит системный вызов или прерывание, то при этом выполняется ядерная составляющая пользовательского процесса в своем собственном контексте, включающем набор ядерных стеков и регистровое окружение. Все ядерные составляющие пользовательских процессов работают в общем адресном пространстве с общим набором ресурсов ядра. Поэтому их вполне можно назвать ядерными легковесными процессами. Наличие ядерных нитей, в частности, облегчает обработку прерываний в режиме ядра. Как и в случае прерывания обычного пользовательского процесса, обработка прерывания ядерной нити производится в ее контексте, и после возврата из прерывания продолжается выполнение прерванной ядерной нити. Кроме того, каждая ядерная нить, вообще говоря, обладает собственным приоритетом по отношению к праву выполняться на процессоре (конечно, этот приоритет связан с приоритетом соответствующего пользовательского процесса). Это позволяет использовать гибкую политику планирования процессорных ресурсов для ядерных составляющих. Итак, ядерные нити должны существовать независимо от того, поддерживаются легковесные процессы в режиме пользователя или нет. Трудно найти сегодня многопользовательскую ОС, в ядре которой не поддерживались бы нити.
Следующим по важности классом легковесных процессов, на мой взгляд, являются пользовательские LWP (LightWeight Processes). Механизмы этого рода позволяют организовать несколько потоков управления в одном адресном пространстве. Все LWP одного пользовательского процесса совместно используют все ресурсы процесса. При поступлении сигнала в адрес процесса на него реагируют все LWP в соответствии с собственными установками. С другой стороны, каждый LWP обладает своим собственным контекстом, куда, как и в случае ядерных нитей, входят стек и регистровое окружение. Любому LWP пользовательского процесса соответствует отдельная ядерная нить. Это означает, что каждый LWP может отдельно планироваться (и поэтому LWP одного пользовательского процесса могут параллельно выполняться на разных процессорах компьютера), и системные вызовы и прерывания LWP могут обрабатываться независимо. Основное преимущество LWP - возможность достижения реального распараллеливания программы при ее выполнении на симметричном мультипроцессоре.
Наконец, к третьему классу легковесных процессов относятся пользовательские нити. Они называются пользовательскими, поскольку реализуются не ядром ОС, а с помощью специальной библиотеки (поэтому в ОС Mach, например, их называют C-Threads). Это тоже очень старая идея, к которой неоднократно прибегали все опытные программисты (здесь уже даже неважно, в среде какой ОС выполняется программа). Суть идеи в том, что вся программа пользователя строится в виде набора сопрограмм (coroutine), которые выполняются под управлением общего монитора. Естественно, в мониторе поддерживаются контексты всех сопрограмм, но и монитор, и сопрограммы являются составляющими одного пользовательского процесса, которому соответствует одна ядерная нить. Конечно, при применении пользовательских нитей невозможно достичь действительного распараллеливания программы. Единственный реальный эффект состоит в возможности распараллеливания обменов при использовании асинхронного режима системных вызовов. По мнению Вахалия, основное достоинство применения пользовательских нитей состоит в лучшей структуризации программы (я с ним не согласен).
От достоинств разных видов легковесных процессов перейдем к краткому анализу их недостатков. При программировании с явным использованием техники легковесных процессов возникает потребность в явной синхронизации по отношению к общим ресурсам. В современных вариантах ОС UNIX имеется несколько разновидностей средств синхронизации: блокировки, семафоры, условные переменные. Но в любом случае, механизм синхронизации является явным, оторванным от ресурса, для доступа к которому производится синхронизация. Если у легковесных процессов одного пользовательского процесса общих ресурсов немного, то такую программу написать и отладить сравнительно несложно. В противном случае отладка программы становится очень сложным делом (даже при помощи одних только пользовательских нитей).
Основная проблема - недетерминированность поведения программы, поскольку время выполнения каждого легковесного процесса, вообще говоря, различно при каждом запуске программы. При использовании явных примитивов для синхронизации набора легковесных процессов наиболее распространенной ошибочной ситуацией является возникновение синхронизационных тупиков. Казалось бы, ну и что? Если при отладке параллельной программы возник тупик, нужно исследовать ситуацию, установить причину ее возникновения (как правило, она в несогласованном выполнении синхронизационных примитивов в разных легковесных процессах) и устранить тупик. Из-за недетерминированности поведения программы тупики могут возникать при одном запуске программы из ста, и нельзя быть полностью уверенным, что при определенном сочетании временных характеристик тупик все-таки не проявится. Это относится и к LWP, и к пользовательским нитям.
Трудно согласиться и с тем, что использование явных средств распараллеливания улучшает структурность программы. Человек мыслит последовательно. Для программиста наиболее естественна модель фон Неймана. Но с другой стороны, было бы странно не использовать возможности мультипроцессоров для повышения скорости выполнения программ. Это противоречие устраняют распараллеливающие компиляторы, которые получают текст последовательной программы, а выдают параллельный код, включающий в себя необходимые примитивы синхронизации. Техника автоматического распараллеливания развивается, уже имеются хорошие распараллеливающие компиляторы.
Проблемой для UNIX-программистов является то, что они, как правило, используют нестрогие языки, такие как Си или Си++. Специфические свойства этих языков, в частности, свободное манипулирование указателями, не позволяют полностью применять технику автоматического распараллеливания. Для достижения максимальной эффективности приходится прибегать к явному распараллеливанию и получать отмеченные выше отрицательные эффекты. Увы, господа...
Какие же можно сделать выводы?
Во-первых, с введением легковесных процессов UNIX, как говаривал Николай Николаевич Озеров, "потерял половину своей зрелищной привлекательности". Традиционный UNIX стимулировал простое и понятное программирование. Современнный провоцирует сложное, запутанное и опасное программирование. Как заметил Деннис Ритчи, он не может называть словом UNIX то, что теперь из него получилось.
Во-вторых, системные программисты (в частности, разработчики серверов баз данных) с успехом используют новые средства. При наличии желания, времени и денег параллельную программу можно надежно отладить, что демонстрирует большинство компаний, разрабатывающих ПО баз данных.
В-третьих, технику явного распараллеливания не следует использовать при прикладном программировании. Господа прикладные программисты, у вас хватает собственных проблем. Если вы займетесь параллельным программированием, то получите сомнительное удовольствие познакомиться с проблемами системных программистов.
Наконец, последнее замечание. Разработчики операционных систем давно знали и крепко помнили, что явное параллельное программирование не является удовлетворительным решением. Много лет мы пытались придумать альтернативные подходы. Много чего придумали, но совсем хорошие вещи не получились. Потому-то легковесные процессы реанимировались. Жаль, что среди нас не нашелся гений...
Сергей Кузнецов - главный редактор журнала "Открытые системы". С ним можно связаться по телефону: (095) 932-9212