Теперь несколько слов о многочисленных примерах программного кода в тексте. Так уж получилось, что по ходу работы над текстом мы постепенно стали нацеливать программные примеры под тестирование возможностей и эффективности иллюстрируемых программных механизмов. Однако такая ориентация, попутно предоставляющая разработчику количественные ориентиры по ОС, не должна затуманивать главное предназначение примеров кода: в них мы стараемся наиболее широко манипулировать разнообразными средствами API, с тем чтобы фрагменты этого кода могли быть непосредственно заимствованы читателями для своих будущих проектов и далее развивались там. Тем не менее в некоторых случаях мы показываем в коде, «как это можно сделать» (когда нужно иллюстрировать специфический механизм), но это вовсе не значит, что «так нужно делать» из соображений производительности, переносимости и т.д. Программный код всех примеров и все необходимое для их сборки, исполнения и проверки находятся в составе файлов архива, доступного по адресу http://www.symbol.ru/library/qnx-unix/pthread.tgz.
Чего нет в этой книге…
В этой книге нет множества приятных и полезных вещей: кулинарных рецептов, эротических сцен, кроссвордов… Но здесь мы хотим перечислить те тематические разделы, которые имеют прямое отношение к вопросам параллельного программирования и которые должны были бы войти в книгу, но в силу определенных обстоятельств в нее не вошли:
• Общие UNIX-механизмы IPC (Inter Process Communication). Из всех механизмов, традиционно относимых к IPC, мы детально затрагиваем только один - сигналы. Другие, крайне интересные в применениях и полноценно представленные в API QNX, такие как неименованные (pipe) и именованные (FIFO) каналы, очереди сообщений POSIX (mq_*), блокирование записей и файлов (fcntl()) и ряд других механизмов, полностью и сознательно обойдены вниманием. Это связано с тем, что: а) в программирование этих механизмов мало что привносит именно «потоковая» (thread) ориентация, положенная во главу угла нашего рассмотрения; б) эти механизмы настолько исчерпывающе описаны У. Стивенсом [2], что добавить что-либо трудно; в) нам крайне не хотелось раздувать объем текста сверх некоторой разумной меры без крайней на то необходимости.
• Совместно используемая (разделяемая) память (shared memory). Это также один из механизмов, традиционно относимый к подмножеству IPC, но мы его выделяем особо. Это именно тот механизм, который должен быть описан применительно к QNX самым тщательным образом, но… Самые поверхностные эксперименты наводят на мысль, что именно в QNX реализации механизмов разделяемой памяти выполнены достаточно «рудиментарно»: ряд возможностей, рассматриваемых POSIX как стандартные, не реализован или реализован в ограниченной мере. Поэтому механизмы разделяемой памяти в QNX требуют отдельного пристального изучения и тестирования. Возможно, это должно быть сделано в отдельной публикации, что мы и планируем восполнить в ближайшем будущем.
• Таймеры в системе. Таймерные механизмы в QNX развиты в полной мере, что неудивительно для ОС реального времени, ориентированной во многом на «встраиваемые» (embedded) применения. Однако таймеры а) имеют все же косвенное отношение к вопросам параллелизма и синхронизации и б) блестяще и полно описаны Р. Кертеном [1].
Вообще, при отборе материала для книги мы старались максимально придерживаться следующего алгоритма: чем шире некоторый предмет освещен в литературе (объект или механизм ОС, приемы его использования и тому подобное), по крайней мере, в известной нам литературе, тем меньше внимания мы уделяли ему в своем тексте.
Благодарности
Предварительный вариант книги был вынесен на обсуждение широкой QNX-общественности (да и UNIX/Linux) на форуме http://qnxclub.net. Было высказано столько замечаний, пожеланий и рекомендаций, что окончательный текст, и котором все они были учтены, стал радикально отличаться от исходной редакции. Невозможно перечислить всех членов интернет-сообщества (в первую очередь, конечно, http://qnx.org.ru и http://qnxclub.net), кто внес свой вклад в это издание — мы благодарны всем без исключения. Но особую признательность мы выражаем:
• Владимиру Зайцеву из г. Харькова, который не только предоставил свой авторский материал, составивший отдельное, самоценное приложение, дополнившее книгу, но и обстоятельно вычитал весь остальной текст, а также внес ряд весьма ценных уточнений.
• Евгению Видревичу из г. Монреаля, который вычитал весь текст с позиции профессионального разработчика и указал на ряд сомнительных мест, а вместо некоторых наших путаных и не совсем внятных формулировок предложил свои — ясные и прозрачные.
• Евгению Тарнавскому из г. Харькова, который высказал ряд очень точных рекомендаций и замечаний, нашедших свое отражение в окончательном варианте текста.
Типографские соглашения
В тексте содержится множество ссылок на программные конструкции: фрагменты кода, имена функций API, символические константы и многое другое, которые при их использовании должны в неизменном виде (именно в таком написании) «перекочевывать» в программный код. Такие фрагменты, конструкции и лексемы выделены моноширинным шрифтом, например pthread_create().
Фрагменты текста, цитируемые из указанных источников, выделены курсивом. Таких мест очень немного: прямое цитирование допускалось нами только в отношении крайне принципиальных утверждений.
В отличие от коротких (в две-три строки) фрагментов кода, листинги программ, приводимых и обсуждаемых в тексте, предваряются отчетливо выделенным заголовком. Это указывает на то, что данную программу как законченную программную единицу можно найти в архиве по адресу http://www.symbol.ru/library/qnx-unix/pthread.tgz. Помимо крупных законченных проектов там же можно найти и отдельные фрагменты кода, обсуждаемые в тексте. Для удобства поиска названия программных файлов, содержащихся в архиве, приводятся в тексте книги перед соответствующим кодом в скобках, например (файл s2.cc).[3]
Феномен параллелизма при выполнении принципиально последовательного по своей природе компьютерного кода возникает даже раньше, чем он начинает отчетливо требоваться для многозадачных и многопользовательских операционных систем:
• Код обработчиков аппаратных прерываний, являющихся принципиально асинхронными, в самых последовательных ОС выполняется параллельно прерываемому ими коду.
• Для работы многих системных служб необходимо, чтобы они выполнялись параллельно с выполнением пользовательской задачи.
Примечание
Например, в принципиально однозадачной операционной системе MS-DOS исторически первой службой, требующей параллельного выполнения, была подсистема спулинга печати. Но добавлять ее в систему пришлось «по живому», поскольку основная структура системы уже сложилась и стабилизировалась (к версии 2.x), а механизмы параллелизма в этой структуре были изначально отвергнуты на корню. И с этого времени начинается затянувшаяся на многие годы история развития уродливой надстройки над MS-DOS — технологии создания TSR-приложений (terminate and stay resident), программного мультиплексора INT 2F и других.
Новое «пришествие» механизмов параллельного выполнения (собственно, уже хорошо проработанных к этому времени в отрасли мэйнфреймов) начинается с появлением многозадачных ОС, разделяющих по времени выполнение нескольких задач. Для формализации (и стандартизации поведения) развивающихся параллельно программных ветвей создаются абстракции процессов, а позже и потоков. Простейший случай параллелизма — когда N (N>1) задач разделяют между собой ресурсы: время единого процессора, общий объем физической оперативной памяти…
Но многозадачное разделение времени — не единственный случай практической реализации параллельных вычислений. В общем случае программа может выполняться в аппаратной архитектуре, содержащей более одного (M) процессора (SMP-системы). При этом возможны принципиально отличающиеся по поведению ситуации:
• Количество параллельных ветвей (процессов, потоков) N больше числа процессоров M, при этом некоторые вычислительные ветви находятся в блокированных состояниях, конкурируя с выполняющимися ветвями за процессорное время. (Частный случай — наиболее часто имеющее место выполнение N ветвей на одном процессоре.)
• Количество параллельных ветвей (процессов, потоков) N меньше числа процессоров M, при этом все ветви вычисления могут развиваться действительно параллельно, а блокированные состояния возникают только при необходимости синхронизации и обмена данными между параллельными ветвями.
Все механизмы параллелизма проектируются (и это находит прямое отражение в POSIX-стандартах, а еще более в текстах комментариев к стандартам) и должны использоваться так, чтобы неявно не допускались какие-либо предположения об относительных скоростях параллельных ветвей и моментах достижения ими (относительно друг друга) конкретных точек выполнения.[4] Так, в программном фрагменте: