разработка и программирование электронных устройств

POSIX программирование для Raspberry Pi : многопоточность

Для тех разработчиков , которые никогда не слышали о стандарте POSIX сделаем небольшое вступление. POSIX( Portable Operating System Interface) — переносимый интерфейс Unix — подобных операционных систем. Что же на самом деле означает поддержка интерфейса POSIX? Это означает, что ваша программа может быть скомпилирована и выполнена на таких операционных системах как Unix, MacOS, Linux, Solaris, FreeBSD и т.д. Вот вам еще ссылка из Wikipedia.

Многие RTOS предоставляют для программирование POSIX API. Это имеет свои преимущества. Большое количество программистов под Linux/Unix владеют интерфейсом POSIX и им не нужно тратить свое время на изучение других интерфейсов программирования , например, таких как эксцентричный интерфейс FreeRTOS, CMSIS_OS интерфейс разных версий и т.д.

POSIX pthread — библиотека , реализующая разделение программы на отдельные потоки, кроме того в эту библиотеку входят различные примитивы синхронизации, такие как мьютексы, семафоры, условные переменные и т.д.

Для использования данной библиотеки необходимо подключить заголовочный файл pthread.h , а также использовать опцию компоновки -lpthread для компоновщика GCC.
Потоком или нитью выполнения называется отдельная функция выполнения программы, которая использует то-же адресное пространство , что и родительский код, но может распараллелить выполнение программы на многоядерных архитектурах процессоров.

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

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

Потоки похожи на дочерние процессы в UNIX — подобных операционных системах, которые создаются как копии породивших их родительских процессов с помощью системного вызова fork(), а образ новой программы загружается на место этой копии с помощью системного вызова exec(). Но в отличии от потоков новые дочерние процессы совершенно независимы от родительского процесса и имеют собственные копии программных ресурсов (дескрипторы открытых файлов и т.д).

Жизненный цикл нового процесса, вызываемого из командной оболочки shell, изображен на следующем рисунке.

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

Компилировать наши простые примерчики мы будет прямо на борту Raspberry Pi.
Этот метод имеет некоторые неудобства. К примеру для редактирования исходного кода придется использовать установленный на RPi текстовый редактор.
Тем разработчикам, кто привык работать в тяжеловесных IDE придется испытать дискомфорт, переключаясь на работу в другом текстовом редакторе, таком как nano или gedit. Или же , не дай Бог, vim или emacs. Я не собираюсь подвергать вас таким испытаниям , поэтому мы будем работать на своем рабочем ПК в привычной IDE, синхронизируя нашу файловую структуру со структурой на Raspberry Pi с помощью утилиты rsync.

Для этого напишем простой Bash — скрипт sync_with_rpi.sh :

В переменной local находиться путь к вашей локальной копии проекта на ПК, соответственно в переменной remote находиться синхронизируемая копия для Raspberry Pi.

Для синхронизации внесенных вами изменений на вашем рабочем ПК с проектом на Raspberry Pi просто запустите скрипт на выполнение, не забыв дать ему соответствующие права:


$ sudo chmod u+x ./sync_with_rpi.sh
$ ./sync_with_rpi.sh

Для первого копирования своих исходников можете воспользоваться командой :


$ scp -r /home/$USER/POSIX/examples/ pi@192.168.0.11:/home/pi/POSIX/examples/

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


$ ssh pi@192.168.0.111
pi@raspberrypi:~ $ mkdir -p /home/pi/POSIX

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

Для того, чтобы скомпилировать данный пример используйте командную строку :


$ gcc -Wall simple_thread.c -o simple_thread -lpthread

Или создайте простейший Makefile:

Все это нужно поместить в нашу структуру директорий на ПК, которую мы будем синхронизировать с файлами на Raspberry Pi.

Компилируем и запускаем программу :


pi@raspberrypi:~/POSIX/examples/pthread $ make all
pi@raspberrypi:~/POSIX/examples/pthread $ make run

Результат выполнения выглядит следующим образом :


./simple_thread "Hello" -10
Hello
Returned value is -1
pi@raspberrypi:~/POSIX/examples/pthread $

Теперь расскажу, что вообще происходит в этой программе. В программе создается два потока , один присоединяемый ( joinableThread), а второй автономный ( detachedThread ) .

Оба потока создаются с помощью pthread_create . Для удобства добавления новых потоков я поместил параметры функции pthread_create в структуру с типом thread_create_param_t . Если вы захотите добавить свой новый поток, просто допишите его параметры как новую запись в массив thread_create_param_t params[] .

Итак, после создания потоков вызывается еще две функции pthread_detach и pthread_join. Первая из них отделяет поток detachedThread от основного процесса (в нашем случае это функция main программы simple_thread). Теперь мы не сможем получить статус завершения потока detachedThread внутри функции main. Кроме того, если функция main завершиться раньше, чем порожденный из нее detachedThread , то этот поток не завершит свою работу, поскольку будет удален при освобождении ресурсов нашей программы. В начале публикации я писал, что потоки разделяют ресурсы порождаемых их процессов, если процесс завершается, то соответственно и общие ресурсы освобождаются. Именно по этой причине вы не увидели на экране сообщения “detachedThread finished”.

Вторая функция pthread_join дожидается завершения присоединяемого потока joinableThread, чтобы получить статус его выполнения.
Первый поток detachedThread выводит на экран текстовое сообщение, переданное как параметр командной строки при вызове ./simple_thread и приостанавливается, ожидая временного интервала, заданного в sleep(), после чего завершается .
Второй поток joinableThread получает в качестве параметра указатель на числовое значение, если это значение больше 0 , то будет возвращен из потока статус 1, если меньше 0 — статус -1 , и 0 будет возвращен в качестве статуса, если число равно нулю.
Эти значения благополучно выводятся на экран после завершения второго потока.
Теперь подумайте, почему же отсоединенный поток не успел вывести сообщение о своем завершении.

Обратите внимания на значения параметров sleep() внутри каждого потока, в joinableThread оно меньше, чем в detachedThread. Соответственно поток joinableThread завершается раньше, он возвращает статус своего выполнения, который выводится на экран и функция main завершается, не дожидаясь завершения потока detachedThread , поскольку он отсоединен и не отслеживается создавшим его процессом.

Если вы уменьшите значение задержки внутри sleep() из detachedThread , чтобы оно было меньше, чем в joinableThread, то сможете увидеть сообщение о завершении отсоединенного потока :


pi@raspberrypi:~/POSIX/examples/pthread $ make run
./simple_thread "Hello" -10
Hello
detachedThread finished
Returned value is -1
pi@raspberrypi:~/POSIX/examples/pthread $

Рассмотрим функцию создания нового потока pthread_create :

Передаваемые параметры:

  • *thread — указатель на дескриптор потока
  • *attr — указатель на атрибуты потока
  • *start_routine — указатель на функцию-обработчик потока
  • *arg — указатель на передаваемые в поток параметры

Возвращаемое значение:
В случае успешного создания потока будет возвращен код 0, в противном случае будет возвращено значение кода ошибки. Смотрите документацию в man-страницах руководства по программированию Unix/Linux:


$ man 3 pthread_create

Теперь , когда вы умеете создавать собственные потоки, остановимся более подробно на атрибутах потока.
В своем простом примере мы инициализировали атрибуты значениями по-умолчанию с помощью вызова pthread_attr_init(&thread_attributes) и удаляли дескриптор атрибутов с помощью pthread_attr_destroy(&thread_attributes).
На самом деле атрибутов у потока достаточно много и все их можно инициализировать по-отдельности. Для этого даже существуют различные функции. Постараемся рассмотреть самые полезные из них:

Функция pthread_attr_setstacksize позволяет задать размер стека для нового потока.

Функция pthread_attr_setshedparam устанавливает параметры планировщика для нового потока, такие как приоритет потока.
В структуре struct sched_param есть только один параметр — это приоритет:

Также для задания приоритета вы можете воспользоваться другой функцией :

Установить политику планирования можно с помощью функции :

Получить политику планирования можно с помощью :

Что же из себя представляет политика планирования, — это числовое значение:
SCHED_FIFO — политика планирования реального времени первый вошёл, первый вышел (First-In First-Out).
SCHED_RR — циклическая (Round-Robin) политика планирования реального времени.
SCHED_OTHER — стандартный планировщик Linux с разделением времени для процессов, работающих не в реальном времени.
Наследовать атрибуты планировщика :

Допустимые значения параметра inheritsched:
PTHREAD_INHERIT_SCHED — наследуються атрибуты планирования из создавшего поток процесса
PTHREAD_EXPLICIT_SCHED — атрибуты планирования берутся из объекта атрибутов
Установить политику планирования и параметры потока можно с помощью функции :

Среди параметров планирования, как указано выше, присутствует только приоритет.
Установить атрибут потока PTHREAD_CREATE_DETACHED или PTHREAD_CREATE_JOINABLE можно с помощью функции:

Назначить потоку имя в виде текстовой строки можно с помощью следующей функции:

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

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

Viewed 27409 times by 4398 viewers

Leave a Reply

You must be logged in to post a comment.