1 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 1 из 8 Многопотоковое программирование Для организации (псевдо)параллельной обработки в POSIX используются два понятия - процессы и потоки управления. Процессы в POSIX являются держателями ресурсов (память, таблица открытых файлов и др.) и работают в значительной степени независимо друг от друга. Одной из функций ОС является защита процессов от нежелательного воздействия друг на друга. Поток выполнения — это элемент кода программы, выполняемый последовательно. Большинство UNIX-приложений — однопотоковые программы, так как каждая из них выполняет в каждый момент времени только один элемент кода. Например, однопотоковый процесс получает команду от пользователя, выполняет ее, сообщает пользователю результаты, а затем ожидает следующую команду. Пользователь должен дождаться, пока процесс закончит выполнение команды, и лишь затем вводить следующие команды. В многопотоковой программе в каждый момент времени могут выполняться "параллельно" несколько элементов кода, при этом каждый элемент кода выполняется одним потоком управления. Работая с многопотоковым процессом, пользователь может вводить команды непрерывно, одну за другой, и процесс выполняет все команды параллельно. Многопотоковое программирование можно использовать для разработки приложений, которые могут выполняться параллельно. Эти приложения можно запускать на любых многопроцессорных системах, эффективно используя аппаратные ресурсы. В частности, если многопотоковое приложение запускается на системе с М процессорами, то все его потоки могут выполняться одновременно, каждый — на отдельном процессоре. Следовательно, производительность такого приложения можно увеличить в N раз, где N — максимальное число свободных в данный момент процессоров (N меньше или равно М). Производительность многопотокового приложения можно улучшить да - же в том случае, если оно запускается на однопроцессорной системе. Например, если один из потоков приложения блокируется каким-то системным вызовом, на этом процессоре может выполняться другой поток. Таким образом, сокращается общее время выполнения приложения. Потоки выполнения отличаются от порожденных процессов создаваемых функцией API fork 2 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 2 из 8 • Потоками выполнения можно управлять либо с помощью библиотечных функций пользовательского уровня, либо с помощью ядра операционной системы. Процессами, которые порождаются системным вызовом fork, управляет ядро операционной системы. Вообще говоря, потоки более эффективны и требуют гораздо меньше внимания со стороны ядра в процессе создания и управления. • Все потоки выполнения в процессе совместно используют сегменты данных и кода. Порожденный процесс имеет собственную копию виртуального адресного пространства, отдельную от родительского процесса. Таким образом, потоки используют гораздо меньше системных ресурсов, чем порожденные процессы. • Функции exit или ехес, вызываемые потоком, завершают все потоки в этом процессе. Если же эти функции вызывает порожденный процесс, то на родительский процесс ее действие не распространяется. • Если поток модифицирует в процессе какую-то глобальную переменную, эти изменения видимы для остальных потоков этого процесса. Поэтому для потоков, обращающихся к совместно используемым данным, необходима синхронизация. Во взаимоотношениях между порожденным и родительским процессами эта проблема не возникает. Теперь перечислим преимущества многопотокового программирования; • повышается производительность процессов и ускоряется реакция на действия пользователя; • процесс может использовать все свободные аппаратные средства многопроцессорной системы, в которой выполняется многопотоковое приложение. • программистам могут структурировать код в независимо выполняемые компоненты; • снижается необходимость использования функции fork для создания порожденных процессов и таким образом увеличивается производительность каждого процесса (реже выполняется переключение контекста); в управлении выполнением потоков в меньшей степени участвует ядро системы; многопотоковое программирование — оптимальный способ повышения производительности объектно-ориентированных приложений, рассчитанных на использование в многопроцессорных системах. 3 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 3 из 8 Недостаток многопотокового программирования состоит в том, что пользователи должны обеспечить синхронизацию потоков в каждой программе. Синхронизация нужна для того, чтобы потоки не делали случайных ошибок при чтении и записи совместно используемых данных и не могли уничтожить свой процесс системным вызовом exit или ехес. 13.1. Структура и методика использования потоков выполнения Поток выполнения состоит из следующих элементов: • идентификатора потока; • динамического стека; • набора регистров (счетчик команд, указатель стека); • сигнальной маски; • значения приоритета; • специальной памяти. Поток выполнения создается функцией thr_create (в POSIX. 1с — pthreacl_creafe). Каждому потоку присваивается идентификатор, уникальный среди всех потоков процесса. Вновь созданный поток наследует сигнальную маску процесса, динамический стек, значение приоритета и набор регистров. Динамический стек и регистры (счетчик команд и указатель стека) позволяют потоку выполняться независимо от других потоков. Поток может изменить унаследованную сигнальную маску и выделить динамическую память для хранения своих данных. Потоку при создании назначается соответствующая функция, поток завершается, когда эта назначенная функция возвращает результат или когда поток вызывает функцию thr_exit (в POSIX.lc — pthread_exit). Когда в процессе создается первый поток, то фактически создаются два потока один — для выполнения указанной функции, а другой ~ для продолжения выполнения процесса. Последний поток завершается, когда функция main возвращает результат или он сам вызывает функцию thr_exit. Все потоки в процессе совместно используют одни сегменты даных и кода. Когда один поток записывает данные в глобальные переменные в процессе, остальные потоки сразу же видят эти изменения. Если какой-либо поток вызывает API exit или API exec, то завершаются все потоки и сам процесс. Поэтому завершающийся поток, если он не хочет разрушить процесс, в котором выполняется, должен вызывать функцию thr exit 4 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 4 из 8 Поток может изменить свою сигнальную маску с помощью функции thr_sigsetmask (в POSIX.lc — pthread_sigmask). Сигнал, передаваемый процессу, получат все потоки, которые не замаскировали его. Поток может посылать сигналы в другие потоки этого же процесса, используя функцию thr_kill (в POSIX.lc — plhread_kill), но не может посылать сигналы в потоки другого процесса, так как уникальность идентификаторов потоков не распространяется на другие процессы. Для настройки собственного механизма обработки сигналов поток может использовать API signal или sigaction. Потоку присваивается целочисленное значение приоритета. Чем больше это значение, тем чаще планируемое выполнение потока. Значение приоритета потока можно запросить с помощью функции thr_getprio и изменить с помощью функции thr_setprio (в POSIX.lc — pthread_attr_getschedparam и pthread_attr_setschedparam соответственно). Поток может намеренно передать выполнение другим потокам с таким же приоритетом; для этого используется функция thr_yield (в POSIX.lc — sched_yield). Кроме того, поток может ожидать завершения другого потока и получить его код возврата с помощью функции thr_join (в POSIX.lc — pthread_join). В API фирмы Sun поток может, пользуясь функциями thr_suspend и thr_continue, приостанавливать и возобновлять выполнение другого потока. Если какая-то функция выполняется множеством потоков и содержит статические или глобальные переменные, которые используются разными потоками, нужно создать специальную память для хранения этих фактических данных по каждому потоку. Эта специальная память выделяется с помощью функций thr _keycreate, thr _setspecific и thr_getspecific. 13.2. Потоки и облегченные процессы Многопотоковые библиотечные функции, разработанные фирмой SUN Microsystems, создают облегченные процессы (lightweight processes, LWP), выполнение которых планируется ядром. Такие процессы похожи на виртуальные процессоры тем, что многопотоковые библиотечные функции упррав ляют выполнением потоков в процессе, связывая их с LWP. Если связанный с LWP поток приостанавливается (например, функцией thr_yield или thr_suspend), этот LWP можно связать с другим потоком, функцию которого он должен будет выполнять. Если LWP выполняет системный вызов от имени потока, он остается связанным с этим потоком до возврата из вызова. Если все LWP, связанные с потоками, заблокированы системными вызовами, многопотоковые библиотечные функции создают новые LWP, с которыми могут быть связаны потоки, ожидающие выполнения. 5 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 5 из 8 Таким образом, обеспечивается непрерывность выполнения процесса. Наконец, если LWT больше чем потоков в процессе, то в целях экономии системных ресурсов многопотоковые библиотечные функции удаляют лишние LWP. Большинство потоков не связаны, поэтому их можно связать с любыми свободными LWP. Вместе с тем процесс может создать один и более потоков, постоянно связанных с облегченными процессами. Эти потоки называются связанными потоками. Такое связывание потоков используется главным образом в тех случаях, когда потоки должны: • планироваться ядром для обработки в режиме реального времени; • иметь собственные альтернативные сигнальные стеки; • иметь собственные будильники и таймер. Взаимосвязь потоков, облегченных процессов и аппаратных процессоров показана на рис. 13.1. На рис. 13.1 в процессе 123 есть два несвязанных потока, которые планируются на два LWP. В процессе 6231 — три несвязанных потока которые планируются на два LWP, и один связанный, который выполняется третьим LWP. В процессе 251 — четыре несвязанных потока которые планируются на один LWP. Несвязанные потоки в каждом процессе планируются многопотоковыми библиотечными функциями к связыванию с LWP и выполнению в этом процессе. LWP всех процессов, в свою очередь, планируются ядром к выполнению на трех имеющихся аппаратных процессорах. В POSIX.lc у потоков есть атрибут, который называется областью действия конкуренции при планировании (scheduling contention scope). Если этот атрибут установлен в PTHREAD_SCOPE_PROCESS, то данным потоком управляют библиотечные функции пользовательского уровня, и он является "несвязанным". Все потоки с этим атрибутом совместно используют ресурсы процессора, доступные для содержащего их процесса. Если же вышеупомянутый атрибут установлен в PTHREAD_SCOPE_SYSTEM, то данным потоком управляет ядро операционной системы, и он считается "связанным". В стандарте POSIX.loc не указано, как ядро должно обрабатывать "связанный" поток 13.3. API потоков выполнения фирмы Sun Microsystems В этом разделе рассматриваются только API потоков выполнения, разработанные фирмой Sun Microsystems. API потоков выполнения стандарта POSIX.lc будут рассмотрены в 6 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 6 из 8 разделе 13.4. Мы обсуждаем эти потоки выполнения отдельно, чтобы не запутать читателей. Прочитав раздел 13.4, вы узнаете о соответствии API Sun и POSIX.lc, благодаря которому многопотоковые приложения можно преобразовывать из формата Sun в стандарт POSIX.lc. Чтобы использовать API потоков выполнения фирмы Sun, необходимо сделать следующее: • включить в программу заголовок <thread.h>; • откомпилировать и скомпоновать программу с опцией -lthread. Если указывается опция -lС, то опцию -lthread нужно поместить перед ней. Например, следующая команда компилирует многопотоковую C++-программу х. С: % СС х.С -о х -Ithread -1C 7 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 7 из 8 Если не указано иного, то большинство описанных ниже API при успешном завершении возвращают 0, а в случае неудачи — -1. В последнем случае может вызываться функция perror, которая выводит сообщения ошибках. 8 D:\681452284.doc Last printed 4/29/2016 8:16 AM стр. 8 из 8 #include <stdio.h> #include <iostream.h> #include <thread.h> static mutex_t lockx; // define the lock storage void* printmsg (void* msg ) { if (mutex_lock(&lockx)) perror("mutex_lock"); else { cout << (char*)msg << endl << flush; /* release the lock */ if (mutex_unlock(&lockx)) perror("mutex_unlock"); } return 0; } int main(int argc, char* argv[]) { if (mutex_init(&lockx, USYNC_THREAD,NULL)) perror("mutex_lock"); /* create threads which call printmsg */ while (--argc > 0) if (thr_create(0,0,printmsg,argv[argc],0, 0)) perror("thr_create"); while (!thr_join(0,0,0)) ; if (mutex_destroy(&lockx)) perror("mutex_destroy"); return 0; }