Приоритеты потоков в linux

Обновлено: 05.07.2024

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

Обычно вызываются три функции:

  • инициализация атрибутов потока - pthread_attr_init () - создает объект pthread_attr_t tattr по умолчанию;
  • изменение значений атрибутов (если значения по умолчанию не подходят) - разнообразные функции pthread_attr_ *(), позволяющие установить значения индивидуальных атрибутов для структуры pthread_attr_t tattr ;
  • создание потока вызовом pthread_create () с соответствующими значениями атрибутов в структуре pthread_attr_t tattr .

/* инициализация атрибутами по умолчанию */

/* вызов соответствующих функций для изменения

ret = pthread_create(&tid, &tattr, start_routine, arg);

Объект атрибутов является закрытым и не может быть непосредственно изменен операциями присваивания. Существует множество функций, позволяющих инициализировать, конфигурировать и уничтожать любые типы объекта. Как только атрибут инициализируется и конфигурируется, он доступен всему процессу. Поэтому рекомендуется конфигурировать все требуемые спецификации состояния один раз, на ранних стадиях выполнения программы. При этом соответствующий объект атрибутов может использоваться везде, где это нужно. Использование объектов атрибутов имеет два основных преимущества.

  • Во-первых, обеспечивается мобильность кода. Даже в случае, когда поддерживаемые атрибуты могут измениться в зависимости от реализации, не нужно будет изменять вызовы функций, которые создают объекты потоков, потому что объект атрибутов скрыт от интерфейса. Если целевая система поддерживает атрибуты, отсутствующие в текущей системе, должны быть созданы условия для управления новыми атрибутами. Задача портирования облегчается, потому что объекты атрибутов будут инициализироваться однажды и в определенном месте.
  • Во-вторых, упрощается спецификация состояний в приложении. Пусть в пределах процесса существует несколько множеств потоков, при этом каждое обеспечивает отдельный сервис, и имеет свои собственные требования к состоянию. В некоторый момент на ранних стадиях приложения, можно инициализировать объект атрибутов потока для каждого множества. Все будущие вызовы создания потока будут обращаться к объекту атрибутов, инициализированному для этого типа потока. Стадия инициализации будет простой и локализованной, и любые возможные модификации можно сделать быстро и надежно.

Функция pthread_attr_init () используется, чтобы инициализировать объект атрибутов значениями по умолчанию. Память распределяется системой потоков во время выполнения.

Пример вызова функции:

ret = pthread_attr_init(&tattr); Значения по умолчанию для атрибутов ( tattr ) приведены в табл. 5.

Атрибут Значение Смысл
scope PTHREAD_SCOPE_PROCESS Новый поток не ограничен - не присоединен ни к одному процессу
detachstate PTHREAD_CREATE_JOINABLE Статус выхода и поток сохраняются после завершения потока
stackaddr NULL Новый поток получает адрес стека, выделенного системой
stacksize 1 Мбайт Новый поток имеет размер стека, определенный системой
inheritsched PTHREAD_INHERIT_SCHED Поток наследует приоритет диспетчеризации родительского потока
schedpolicy SCHED_OTHER Новый поток использует диспетчеризацию с фиксированными приоритетами. Поток работает, пока не будет прерван потоком с высшим приоритетом или не приостановится

Функция возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Код ошибки устанавливается в переменной errno .

Функция pthread_attr_destroy () используется, чтобы удалить память для атрибутов, выделенную во время инициализации. Объект атрибутов становится недействительным.

Пример вызова функции:

ret = pthread_attr_destroy(&tattr); pthread_attr_destroy () возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки.

Как задать или изменить приоритет процесса в Linux?

В некоторых ситуациях может возникнуть необходимость запускать скрипт/программу не со стандартным приоритетом, а с более высоким или низким, либо повысить или понизить приоритет у запущенного ранее процесса. В данной статье мы рассмотрим несколько способов, как можно это с делать.

Что такое приоритет процесса?

Приоритет процесса определяет, как часто именно этот процесс, по сравнению с другими запущенными процессами, стоящими в очереди на выполнение, будет исполняться процессором. В ОС Linux значение приоритета процесса варьируется в диапазоне значений от -20 до 19 (т.е. получается 40 возможных значений: -20, -19, -18 . 0, 1, 2 . 19) и называется niceness (сокращенно NI).

Чем меньше это значение, тем выше приоритет будет у такого процесса. Например, если у нас есть один процесс, работающий с приоритетом 10, а другой процесс работающий с приоритетом 15, то в первую очередь будет выполняться процесс приоритетом 10, а уже после него, тот, где приоритет 15. А в ситуации, когда есть 2 процесса и у одного из них приоритет будет равен -20, а у другого равен 10, то в первую очередь процессор будет обрабатывать тот процесс, у которого приоритет равен -20, а уже после тот, у которого приоритет равен 10.

Как узнать приоритет процесса?

С помощью команды top (все запущенные процессы)


Посмотреть приоритет процесса можно с помощью команды top

С помощью команды ps (конкретный процесс(ы) по его имени)

С помощью команды ps (конкретный процесс по его PID)

Задание приоритета при запуске процесса

Для того, чтобы задать приоритет при старте нового процесса, необходимо воспользоваться командой nice
nice -n [значение приоритета] [команда]
Запустить утилиту top с приоритетом 15:


Изменение приоритета у существующего процесса

Для того, чтобы изменить приоритет у существующего процесса (т.е. такого процесса, который ранее был уже запущен), необходимо воспользоваться командой renice
renice [значение приоритета] -p [id процесса]

При понижении приоритета у процесса, который является вашим (т.е. запущен под той же учетной записью, под которой вы работаете в системе) - права суперпользователя не требуются, НО при повышении приоритета у процесса, требуется запускать команду renice с правами суперпользователя, т.е. с помощью sudo renice.
В противном случае, вы будете получать ошибку примерно такого содержания:

renice: failed to set priority for 91197 (process ID): Permission denied


Мы изменили приоритет у существующего процесса (команда top из предыдущего примера) с 15 на 0.

Была ли эта статья Вам полезна?
Что в статье не так? Пожалуйста, помогите нам её улучшить!

Комментарии к статье (5)

Вы работаете под суперпользователем root? Если нет, то используйте команду sudo или su.

Добавил информацию в статью:

При понижении приоритета у процесса, который является вашим (т.е. запущен под той же учетной записью, под которой вы работаете в системе) - права суперпользователя не требуются, НО при повышении приоритета у процесса, требуется запускать команду renice с правами суперпользователя, т.е. с помощью sudo renice.

было бы еще неплохо добавить шпаргалку, как повесить приоритет по умолчанию для какой-нибудь программы, например на тот же mysql

SCHED_OTHER
Это стратегия планирования с разделением времени потоков по умолчанию. Уровень приоритета всех потоков равен 0, а планирование потоков выполняется с разделением времени. Проще говоря, если система использует эту стратегию планирования, программа не сможет установить приоритет потока. Обратите внимание, что эта стратегия планирования также является вытесняющей: когда поток с высоким приоритетом готов к запуску, текущий поток будет вытеснен и попадет в очередь ожидания. Эта стратегия планирования определяет только порядок выполнения потоков с одинаковым приоритетом в очереди выполняемых потоков.

SCHED_FIFO
Это стратегия вызова в реальном времени по принципу «первым пришел - первым обслужен», и ее можно запустить только от имени суперпользователя. Эта стратегия вызова используется только для потоков с приоритетом больше 0. Это означает, что исполняемый поток, использующий SCHED_FIFO, всегда будет вытеснять запущенный поток, используя SCHED_OTHER. Вдобавок SCHED_FIFO - это простая стратегия планирования без разделения времени. Когда поток становится работоспособным, он добавляется в конец соответствующей очереди приоритетов ((POSIX 1003.1). Когда все потоки с высоким приоритетом завершаются или блокируются, он будет запущен. Для потоков с одинаковым приоритетом запускайте в соответствии с простым правилом "первым вошел первым". Мы считаем очень плохой ситуацией, если есть несколько потоков с одинаковым приоритетом, ожидающих выполнения, но самое раннее выполнение thread не завершается или блокирует действия, тогда другие потоки не могут быть выполнены, если текущий поток не вызывает такие функции, как pthread_yield, поэтому при использовании SCHED_FIFO будьте осторожны, чтобы обрабатывать тот же уровень действий потока.

SCHED_RR
Ввиду некоторых недостатков стратегии планирования SCHED_FIFO, SCHED_RR внес некоторые улучшения в SCHED_FIFO. По сути, это все еще стратегия вызова SCHED_FIFO. Он использует максимальное время выполнения, чтобы ограничить выполнение текущего процесса.Когда время выполнения больше или равно максимальному времени выполнения, текущий поток будет переключен и помещен в конец той же приоритетной очереди. Преимущество этого состоит в том, что другие потоки того же уровня могут выполняться под «эгоистичными» потоками.

Подводя итог, я рекомендую использовать SCHED_RR, который может изменить стратегию планирования потоков и приоритет, который мы хотим улучшить в пределах контролируемого диапазона.

Система Linux предоставляет нам два приведенных выше API для установки и получения одногоуже существуетСтратегия планирования и приоритет планирования потока, эксперимент 1:

Эта же система также предоставляет нам другой набор API. Эти два интерфейса сначала устанавливают соответствующие атрибуты для объекта attr, а затем передают этот объект в качестве параметра в pthread_create, чтобы вновь созданный поток имел соответствующие атрибуты. Разница между ним и API, упомянутым во втором пункте выше:
1. Необходимо отдельно установить политику и приоритет (не важно)
2. Этот набор API может устанавливать только те потоки, которые еще не были созданы. Для созданных потоков он может быть установлен только с помощью pthread_setschedparam.
Эксперимент 2:

  • Когда мы используем интерфейс pthread_attr_setschedxxx, мы также должны установить для атрибута наследуемый объекта attr значение «PTHREAD_EXPLICIT_SCHED», иначе дочерний поток унаследует все атрибуты родительского потока.
  • Обычно интерфейс pthread_getschedparam используется для получения политики политики планирования и параметра приоритета планирования param.sched_priority. Если вы используете pthread_attr_getschedxxx для получения атрибутов планирования, вы должны использовать объект attr, используемый при создании потока, иначе он не может быть получен.

По оценкам, многие люди похожи на меня.Увидев такую ​​полезную функцию, быстро напишите демонстрационную программу, чтобы проверить ее, но трагедия в том, что если вы используете pthread_setschedpolicy, вы получите ошибку:

Использование pthread_attr_setschedxxx сообщит об ошибке:

После некоторых поисков и экспериментов были найдены следующие решения:

решение:

Сетевое решение: выполните "sysctl -w kernel.sched_rt_runtime_us = -1" на хосте
может решить проблему, но эта команда изменяет глобальное sched_rt_runtime_us. Это рискованно и может привести к тому, что другие потоки не получат временной интервал, поэтому это не рекомендуется!

Неконтейнерная среда

Для потоков на хосте мы можем решить проблему, добавив pid родительского потока в / sys / fs / cgroup / cpu / tasks

Контейнерная среда

Установить на хосте:

Если при настройке разрешения не разрешены, можно добавить текущую оболочку в задачи

Как видно из двух приведенных выше примеров, дочерний поток наследует атрибуты потока родительского потока. Если мы не хотим, чтобы он наследовал атрибуты родительского потока, мы можем реализовать его через следующий интерфейс:

Значение по умолчанию для наследования - PTHREAD_INHERIT_SCHED, что означает, что вновь созданный поток унаследует политику планирования, определенную в потоке-создателе. Все атрибуты планирования, определенные в вызове pthread_create (), будут проигнорированы. Если используется значение PTHREAD_EXPLICIT_SCHED, будут использоваться атрибуты в вызове pthread_create ().
Эксперимент 3:

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

Итак, мы должны отобразить атрибуты указанного attr:

виджет chrt

Мы можем использовать этот небольшой инструмент для динамической установки готовых политик и приоритетов планирования, таких как:

Как вы думаете, операционная система Linux может автоматически позаботиться сама о себе? Когда работает все нормально или вам не нужны никакие нестандартные возможности - вполне да. Но иногда может понадобиться ваше вмешательство в ее работу.

В Linux для каждой отдельной программы, при ее запуске создается процесс. Неважно запускаете программу вы вручную самостоятельно или это делает система или ядро. Например, программа инициализации, которая запускается сразу после завершения загрузки ядра тоже имеет свой процесс с идентификатором 0. Процессы в linux можно описать как контейнеры, в которых хранится вся информация о состоянии и выполнении программы. Если программа работает хорошо, то все нормально, но если она зависла или вам нужно настроить ее работу может понадобиться управление процессами в Linux.

В этой статье будет затронута обширная тема, мы рассмотрим такие возможности:

  • Просмотр запущенных процессов
  • Просмотр информации о процессах
  • Поиск процессов в Linux
  • Изменение приоритета процессов
  • Завершение процессов
  • Ограничение памяти доступной процессу

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

Что такое процесс?

Начнем с того, что разберемся в терминах. По сути, процесс - это каждая программа. Как я уже говорил для каждой запускаемой программы создается отдельный процесс. В рамках процесса программе выделяется процессорное время, оперативная память и другие системные ресурсы. У каждого процесса есть свой идентификатор, Proccess ID или просто PID, по ним, чаще всего и определяются процессы Linux. PID определяется неслучайно, как я уже говорил, программа инициализации получает PID 1, а каждая следующая запущенная программа - на единицу больше. Таким образом PID пользовательских программ доходит уже до нескольких тысяч.

На самом деле, процессы Linux не настолько абстрактны, какими они вам сейчас кажутся. Их вполне можно попытаться пощупать. Откройте ваш файловый менеджер, перейдите в корневой каталог, затем откройте папку /proc. Видите здесь кучу номеров? Так вот это все - PID всех запущенных процессов. В каждой из этих папок находится вся информация о процессе.

Например, посмотрим папку процесса 1. В папке есть другие под каталоги и много файлов. Файл cmdline содержит информацию о команде запуска процесса:

Поскольку у меня используется система инициализации Systemd, то и первый процесс запускается для нее. С помощью каталога /proc можно сделать все. Но это очень неудобно, особенно учитывая количество запущенных процессов в системе. Поэтому для реализации нужных задач существуют специальные утилиты. Перейдем к рассмотрению утилит, которые позволяют реализовать управление процессами в Linux.

Управление процессами в Linux

В Linux есть очень большое количество утилит для решения различных задач по управлению процессами. Это и такие многофункциональные решения, как htop, top, а также простые утилиты, например, ps, kill, killall, who и т д. Я не буду рассматривать в этой статье графические утилиты, и top тоже рассматривать не буду. Первое потому что слишком просто, второе - потому что htop лучше. Мы остановимся на работе с программой htop и ее аналогами в форме утилит в стиле GNU, одна утилита - одна функция.

Давайте установим htop, если она у вас еще не установлена. В Ubuntu это делается так:

sudo apt install htop

В других дистрибутивах вам нужно просто использовать свой менеджер пакетов. Имя пакета такое же.

Посмотреть запущенные процессы

Это очень простая задача, и также просто она решается. Для этого существует множество утилит, начиная от обычной ps, до более продвинутых интерактивных top, htop и так далее.

Открыв htop, мы сразу видим список запущенных процессов. Конечно, здесь отображены не все процессы linux, их-то в системе очень много, вы уже знаете, все они на один экран не поместятся. По умолчанию выводятся процессы, запущенные от имени вашего пользователя:

htop

Вы можете увидеть такую информацию о процессе:

  • PID - идентификатор процесса
  • USER - пользователь, от которого был запущен процесс
  • PRI - приоритет процесса linux на уровне ядра (обычно NI+20)
  • NI - приоритет выполнения процесса от -20 до 19
  • S - состояние процесса
  • CPU - используемые ресурсы процессора
  • MEM - использованная память
  • TIME - время работы процесса

К отображению можно добавить и дополнительные параметры, но эти главные. Добавить параметры можно с помощью меню Setup. Там все очень просто, читайте подсказки и следуйте указаниям. Например, добавлен параметр PPID:


Очень важной особенностью программы есть то, что вы можете сортировать процессы в Linux по нужному параметру. Просто кликните по названию параметра, оно выделится зеленым и будет выполнена сортировка. Например, хотите посмотреть в каком порядке запускались процессы, сортируем по PID:

htop2

Также есть интересная возможность разместить процессы в виде дерева. Вы сможете увидеть, каким процессом был запущен тот или иной процесс. Для отображения дерева нажмите кнопку F5:

htop3

Почти те же действия вы можете выполнять с помощью программы ps. Только здесь нет такого удобного интерактивного режима. Все делается с помощью опций.

ps

Рассмотрим основные опции, которые будем использовать:

  • -e - вывести информацию обо всех процессах
  • -a - вывести информацию обо всех наиболее часто запрашиваемых процессах
  • -t - показывать только процессы из этого терминала
  • -p - показывать информацию только об указанном процессе
  • -u - показывать процессы только определенного пользователя

Одним словом, чтобы посмотреть все активные на данный момент процессы в linux, используется сочетание опций aux:

Программа показывает все те же параметры, только здесь нет интерактивного интерфейса. Думаете здесь нельзя отсортировать процессы, но ошибаетесь, можно. Для этого есть опция sort. Вы можете сортировать их по любому полю, например:

Список будет отсортирован в обратном порядке, внизу значения больше, вверху - меньше. Если нужно в обратном порядке, добавьте минус:

ps1

В качестве поля для сортировки могут быть использованы приоритеты процессов Linux или любые другие параметры. Также вы можете обрезать вывод, если не нужно выводить всю информацию:

Казалось бы, у ps нет возможности стоить деревья процессов. Но не совсем, для этого существует отдельная команда:

pstree

Поиск процессов в Linux

Список процессов, это хорошо. Но иногда, когда какой-нибудь процесс завис и нужно убить процесс Linux или нам нужно провести с ним какие-либо действия, нужно выделить этот процесс из списка, узнать его PID и информацию о нем.

Чтобы найти процесс linux в htop можно использовать кнопку F3. Нажмите F3 и наберите нужное слово. Дальше чтобы перейти к следующему вхождению нажимайте F2 или Esc для завершения поиска:

htop4

Для поиска процессов в htop можно использовать также фильтр htop. Нажмите F4, введите слово и будут выведены только процессы linux, имя которых включает это слово.

htop6

В утилите ps фильтрации нет, но зато мы можем использовать утилиту grep, перенаправив вывод ps на нее чтобы найти процесс linux:

ps aux | grep chromium

Это очень часто употребляемая команда.

Изменение приоритета процессов

Приоритет процесса linux означает, насколько больше процессорного времени будет отдано этому процессу по сравнению с другими. Так мы можем очень тонко настроить какая программа будет работать быстрее, а какая медленнее. Значение приоритета может колебаться от 19 (минимальный приоритет) до -20 - максимальный приоритет процесса linux. Причем, уменьшать приоритет можно с правами обычного пользователя, но чтобы его увеличить нужны права суперпользователя.

В htop для управления приоритетом используется параметр Nice. Напомню, что Priv, это всего лишь поправка, она в большинстве случаев больше за Nice на 20. Чтобы изменить приоритет процесса просто установите на него курсор и нажимайте F7 для уменьшения числа (увеличения приоритета) или F8 - для увеличения числа.

Но и для решения этой задачи управления процессами Linux необязательно использовать htop. Вы можете сделать все и другими командами. Например, команда nice. С помощью нее вы можете указать приоритет для запускаемого процесса:

nice -n 10 apt-get upgrade

Или изменить приоритет для уже существующего по его pid:

renice -n 10 -p 1343

Завершение процессов в Linux

Если процесс завис и не отвечает, его необходимо завершить. В htop, чтобы убить процесс Linux, просто установите курсор на процесс и нажмите F9:

htop7

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

  • SIGTERM - попросить процесс сохранить данные и завершится
  • SIGKILL - завершить процесс немедленно, без сохранения

Вообще сигналов есть несколько десятков, но мы не будем их рассматривать. Отправим сигнал SIGKILL:

Также можно воспользоваться утилитой kill:

Также можно уничтожить процесс по имени:

Ограничение процессов

Управление процессами в Linux позволяет контролировать практически все. Вы уже видели что можно сделать, но можно еще больше. С помощью команды ulimit и конфигурационного файла /etc/security/limits.conf вы можете ограничить процессам доступ к системным ресурсам, таким как память, файлы и процессор. Например, вы можете ограничить память процесса Linux, количество файлов и т д.

Запись в файле имеет следующий вид:

<домен> <тип> <элемент> <значение>

  • домен - имя пользователя, группы или UID
  • тип - вид ограничений - soft или hard
  • элемент - ресурс который будет ограничен
  • значение - необходимый предел

Жесткие ограничения устанавливаются суперпользователем и не могут быть изменены обычными пользователями. Мягкие, soft ограничения могут меняться пользователями с помощью команды ulimit.

Рассмотрим основные ограничения, которые можно применить к процессам:

  • nofile - максимальное количество открытых файлов
  • as - максимальное количество оперативной памяти
  • stack - максимальный размер стека
  • cpu - максимальное процессорное время
  • nproc - максимальное количество ядер процессора
  • locks - количество заблокированных файлов
  • nice - максимальный приоритет процесса

Например, ограничим процессорное время для процессов пользователя sergiy:

sergiy hard nproc 20

Посмотреть ограничения для определенного процесса вы можете в папке proc:

Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 204800 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 23562 23562 processes
Max open files 1024 4096 files
Max locked memory 18446744073708503040 18446744073708503040 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 23562 23562 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

Ограничения, измененные, таким образом вступят в силу после перезагрузки. Но мы можем и устанавливать ограничения для текущего командного интерпретатора и создаваемых им процессов с помощью команды ulimit.

Вот опции команды:

  • -S - мягкое ограничение
  • -H - жесткое ограничение
  • -a - вывести всю информацию
  • -f - максимальный размер создаваемых файлов
  • -n - максимальное количество открытых файлов
  • -s - максимальный размер стека
  • -t - максимальное количество процессорного времени
  • -u - максимальное количество запущенных процессов
  • -v - максимальный объем виртуальной памяти

Например, мы можем установить новое ограничение для количества открываемых файлов:

Установим лимит оперативной памяти:

ulimit -Sv 500000

Напоминаю, что это ограничение будет актуально для всех программ, выполняемых в этом терминале.

Выводы

Вот и все. Теперь управление процессами в Linux не вызовет у вас проблем. Мы рассмотрели очень даже подробно эту тему. Если у вас остались вопросы или есть предложения по дополнению статьи, пишите в комментариях!

Современные операционные системы и микропроцессоры уже давно поддерживает многозадачность и вместе с тем, каждая из этих задач может выполняться в несколько потоков. Это дает ощутимый прирост производительности вычислений и позволяет лучше масштабировать пользовательские приложения и сервера, но за это приходится платить цену — усложняется разработка программы и ее отладка.


В этой статье мы познакомимся с POSIX Threads для того, чтобы затем узнать как это все работает в Linux. Не заходя в дебри синхронизации и сигналов, рассмотрим основные элементы Pthreads. Итак, под капотом потоки.

Общие сведения

Множественные нити исполнения в одном процессе называют потоками и это базовая единица загрузки ЦПУ, состоящая из идентификатора потока, счетчика, регистров и стека. Потоки внутри одного процесса делят секции кода, данных, а также различные ресурсы: описатели открытых файлов, учетные данные процесса сигналы, значения umask , nice , таймеры и прочее.


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


Ядро задействует копирование при записи для страниц с данными, сегментов памяти родительского процесса содержащие стек и кучу. Вследствие того, что процессы часто выполняют вызов fork и сразу после этого exec , копирование их страниц во время выполнения вызова fork становится ненужной расточительностью — их все равно приходится отбрасывать после выполнения exec . Сперва записи таблицы страниц указывают на одни и те же страницы физической памяти родительского процесса, сами же страницы маркируются только для чтения. Копирование страницы происходит ровно в тот момент, когда требуется ее изменить.

Таблицы страниц до и после изменения общей страницы памяти во время копирования при записи.


Существует закономерность между количеством параллельных нитей исполнения процесса, алгоритмом программы и ростом производительности. Это зависимость называется Законом Амдаля.

Закон Амдаля для распараллеливания процессов.


Используя уравнение, показанное на рисунке, можно вычислить максимальное улучшение производительности системы, использующей N процессоров и фактор F, который указывает, какая часть системы не может быть распараллелена. Например 75% кода запускается параллельно, а 25% — последовательно. В таком случае на двухядерном процессоре будет достигнуто 1.6 кратное ускорение программы, на четырехядерном процессоре — 2.28571 кратное, а предельное значение ускорения при N стремящемся к бесконечности равно 4.

Отображение потоков в режим ядра

Практически все современные ОС — включая Windows, Linux, Mac OS X, и Solaris — поддерживают управление потоками в режиме ядра. Однако потоки могут быть созданы не только в режиме ядра, но и в режиме пользователя. При использовании этого уровня ядро не знает о существовании потоков — все управление потоками реализуется приложением с помощью специальных библиотек. Пользовательские потоки по разному отображаются на потоки в режиме ядра. Всего существует три модели, из которых 1:1 является наиболее часто используемой.

Отображение N:1

В данной модели несколько пользовательских потоков отображаются на один поток ядра ОС. Все управление потоками осуществляет особая пользовательская библиотека, и в этом преимущество такого подхода. Недостаток же в том, что если один единственный поток выполняет блокирующий вызов, то тогда тормозится весь процесс. Предыдущие версии Solaris OS использовали такую модель, но затем вынуждены были от нее отказаться.


Отображение 1:1

Это самая проста модель, в которой каждый поток созданный в каком-нибудь процессе непосредственно управляется планировщиком ядра ОС и отображается на один единственный поток в режиме ядра. Чтобы приложение не плодило бесконтрольно потоки, перегружая ОС, вводят ограничение на максимальное количество потоков поддерживаемых в ОС. Данный способ отображения потоков поддерживают ОС Linux и Windows.


Отображение M:N

При таком подходе M пользовательских потоков мультиплексируются в такое же или меньшее N количество потоков ядра. Преодолеваются негативные эффекты двух других моделей: нити по-настоящему исполняются параллельно и нет необходимости в ОС вводить ограничения на их общее количество. Вместе с тем данную модель довольно трудно реализовать с точки зрения программирования.


Потоки POSIX

В конце 1980-х и начале 1990-х было несколько разных API, но в 1995 г. POSIX.1c стандартизовал потоки POSIX, позже это стало частью спецификаций SUSv3. В наше время многоядерные процессоры проникли даже в настольные ПК и смартфоны, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им одновременно выполнять несколько потоков. В былые времена одновременное исполнение потоков на одноядерных ЦПУ было лишь впечатляюще изобретательной, но очень эффективной иллюзией.

Pthreads определяет набор типов и функций на Си.

  • pthread_t — идентификатор потока;
  • pthread_mutex_t — мютекс;
  • pthread_mutexattr_t — объект атрибутов мютекса
  • pthread_cond_t — условная переменная
  • pthread_condattr_t — объект атрибута условной переменной;
  • pthread_key_t — данные, специфичные для потока;
  • pthread_once_t — контекст контроля динамической инициализации;
  • pthread_attr_t — перечень атрибутов потока.

В традиционном Unix API код последней ошибки errno является глобальной int переменной. Это однако не годится для программ с множественными нитями исполнения. В ситуации, когда вызов функции в одном из исполняемых потоков завершился ошибкой в глобальной переменной errno , может возникнуть состояние гонки из-за того, что и остальные потоки могут в данный момент проверять код ошибки и оконфузиться. В Unix и Linux эту проблему обошли тем, что errno определяется как макрос, задающий для каждой нити собственное изменяемое lvalue .

Из man errno
Переменная errno определена в стандарте ISO C как изменяемое lvalue int и не объявляемая явно; errno может быть и макросом. Переменная errno является локальным значением нити; её изменение в одной нити не влияет на её значение в другой нити.

Создание потока

В начале создается потоковая функция. Затем новый поток создается функцией pthread_create() , объявленной в заголовочном файле pthread.h. Далее, вызывающая сторона продолжает выполнять какие-то свои действия параллельно потоковой функции.

При удачном завершении pthread_create() возвращает код 0, ненулевое значение сигнализирует об ошибке.

  • Первый параметр вызова pthread_create() является адресом для хранения идентификатора создаваемого потока типа pthread_t .
  • Аргумент start является указателем на потоковую void * функцию, принимающей бестиповый указатель в качестве единственной переменной.
  • Аргумент arg — это бестиповый указатель, содержащий аргументы потока. Чаще всего arg указывает на глобальную или динамическую переменную, но если вызываемая функция не требует наличия аргументов, то в качестве arg можно указать NULL .
  • Аргумент attr также является бестиповым указателем атрибутов потока pthread_attr_t . Если этот аргумент равен NULL , то поток создается с атрибутами по умолчанию.

Рассмотрим теперь пример многопоточной программы.

Чтобы подключить библиотеку Pthread к программе, нужно передать компоновщику опцию -lpthread .

О присоединении потока pthread_join расскажу чуть позже. Строка pthread_t tid задает идентификатор потока. Атрибуты функции задает pthread_attr_init(&attr) . Так как мы не задавали их явно, будут использованы значения по умолчанию.

Завершение потока

Поток завершает выполнение задачи когда:

  • потоковая функция выполняет return и возвращает результат произведенных вычислений;
  • в результате вызова завершения исполнения потока pthread_exit() ;
  • в результате вызова отмены потока pthread_cancel() ;
  • одна из нитей совершает вызов exit()
  • основная нить в функции main() выполняет return , и в таком случае все нити процесса резко сворачиваются.

Синтаксис проще, чем при создании потока.

Если в последнем варианте старшая нить из функции main() выполнит pthread_exit() вместо просто exit() или return , то тогда остальные нити продолжат исполняться, как ни в чем не бывало.

Ожидание потока

Функция pthread_join() ожидает завершения потока обозначенного THREAD_ID . Если этот поток к тому времени был уже завершен, то функция немедленно возвращает значение. Смысл функции в том, чтобы синхронизировать потоки. Она объявлена в pthread.h следующим образом:

При удачном завершении pthread_join() возвращает код 0, ненулевое значение сигнализирует об ошибке.

Если указатель DATA отличается от NULL , то туда помещаются данные, возвращаемые потоком через функцию pthread_exit() или через инструкцию return потоковой функции. Несколько потоков не могут ждать завершения одного. Если они пытаются выполнить это, один поток завершается успешно, а все остальные — с ошибкой ESRCH. После завершения pthread_join() , пространство стека связанное с потоком, может быть использовано приложением.

В каком-то смысле pthread_joini() похожа на вызов waitpid() , ожидающую завершения исполнения процесса, но с некоторыми отличиями. Во-первых, все потоки одноранговые, среди них отсутствует иерархический порядок, в то время как процессы образуют дерево и подчинены иерархии родитель — потомок. Поэтому возможно ситуация, когда поток А, породил поток Б, тот в свою очередь заделал В, но затем после вызова функции pthread_join() А будет ожидать завершения В или же наоборот. Во-вторых, нельзя дать указание одному ожидай завершение любого потока, как это возможно с вызовом waitpid(-1, &status, options) . Также невозможно осуществить неблокирующий вызов pthread_join() .

Досрочное завершение потока

Точно так же, как при управлении процессами, иногда необходимо досрочно завершить процесс, многопоточной программе может понадобиться досрочно завершить один из потоков. Для досрочного завершения потока можно воспользоваться функцией pthread_cancel .

При удачном завершении pthread_cancel() возвращает код 0, ненулевое значение сигнализирует об ошибке.

Важно понимать, что несмотря на то, что pthread_cancel() возвращается сразу и может завершить поток досрочно, ее нельзя назвать средством принудительного завершения потоков. Дело в том, что поток не только может самостоятельно выбрать момент завершения в ответ на вызов pthread_cancel() , но и вовсе его игнорировать. Вызов функции pthread_cancel() следует рассматривать как запрос на выполнение досрочного завершения потока. Поэтому, если для вас важно, чтобы поток был удален, нужно дождаться его завершения функцией pthread_join() .

Небольшая иллюстрация создания и отмены потока.

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


Как мы видим есть вовсе неотменяемые потоки, а поведением по умолчанию является отложенное завершение, которое происходит в момент завершения. А откуда мы узнаем, что этот самый момент наступил? Для этого существует вспомогательная функция pthread_testcancel .

Отсоединение потока

Любому потоку по умолчанию можно присоединиться вызовом pthread_join() и ожидать его завершения. Однако в некоторых случаях статус завершения потока и возврат значения нам не интересны. Все, что нам надо, это завершить поток и автоматически выгрузить ресурсы обратно в распоряжение ОС. В таких случаях мы обозначаем поток отсоединившимся и используем вызов pthread_detach() .

При удачном завершении pthread_detach() возвращает код 0, ненулевое значение сигнализирует об ошибке.

Отсоединенный поток — это приговор. Его уже не перехватить с помощью вызова pthread_join() , чтобы получить статус завершения и прочие плюшки. Также нельзя отменить его отсоединенное состояние. Вопрос на засыпку. Что будет, если завершение потока не перехватить вызовом pthread_join() и чем это отлично от сценария, при котором завершился отсоединенный поток? В первом случае мы получим зомбо-поток, а во втором — все будет норм.

Потоки versus процессы

Напоследок предлагаю рассмотреть несколько соображений на тему, следует ли проектировать приложение многопоточным или запускать его в несколько процессов с одним потоком? Сперва выгоды параллельных множественных потоков.

В начальной части статьи мы уже указывали на эти преимущество, поэтому вкратце их просто перечислим.

  • Потоки довольно просто обмениваются данными по сравнению с процессами.
  • Создавать потоки для ОС проще и быстрее, чем создавать процессы.

Теперь немного о недостатках.

  • При программировании приложения с множественными потоками необходимо обеспечить потоковую безопасность функций — т. н. thread safety. Приложения, выполняющиеся через множество процессов, не имеют таких требований.
  • Один бажный поток может повредить остальные, так как потоки делят общее адресное пространство. Процессы более изолированы друг от друга.
  • Потоки конкурируют друг с другом в адресном пространстве. Стек и локальное хранилище потока, захватывая часть виртуального адресного пространства процесса, тем самым делает его недоступным для других потоков. Для встроенных устройств такое ограничение может иметь существенное значение.

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

Читайте также: