Протопотоки И Кооперативная Многозадачность

Продолжаем изучать планирование малых потоков.

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

Сегодня мы поговорим совсем о другой сущности – протопоток Адама Данкелса, которые хоть и из ряда вон выходящие, но совершенно не лишние в контексте рассматриваемой темы.

И:

  1. Многозадачность в ядре Linux: прерывания и тасклеты
  2. Многозадачность в ядре Linux: рабочая очередь
  3. Протопотоки и кооперативная многозадачность


Что это такое и зачем все это нужно?

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

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

Их объединяет главным образом вопрос планировки.

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

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



Кооперативная многозадачность и протопотоки

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

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

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

Статья сопровождается интересными и наглядными примерами.

Подробнее о том, как это работает, будет здесь.

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

Основные особенности и преимущества протопотоков:

  • Реализация последовательного потока управления без использования сложных конечных автоматов и полной многопоточности,
  • Используя условную блокировку внутри функций C,
  • Протопотоки очень легкие, поскольку у них нет стека.

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

    Таким образом, используется только явная блокировка.

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

Здесь нужно быть особенно внимательным — локальные переменные нужно использовать очень осторожно! Протопотоки не имеют выделенного стека; локальные переменные хранить негде.

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

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

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

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

Управление происходит с помощью switch(), аналогично тому, как это происходит в Метод Даффа И программы совместного осуществления Саймон Тэтэм .

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

Важное ограничение реализации: код внутри самого протопотока не может в полной мере использовать оператор switch().

Однако такое ограничение возможно, используя возможности некоторых компиляторов, например, gcc. Вся библиотека реализована в макросах, поскольку макросы, в отличие от простых функций, могут изменять поток управления только с помощью стандартных конструкций C. Основной API включает в себя следующие макросы:

  • Макрос инициализации PT_INIT,
  • Макросы PT_BEGIN и PT_END, которые объявляют начало и конец протопотока,
  • Макросы ожидания условий PT_WAIT_UNTIL и PT_WAIT_WHILE,
  • Макросы ожидания выполнения дочерних протопотоков PT_WAIT_THREAD и PT_SPAWN,
  • Перезапустите макрос PT_RESTART и выйдите из PT_EXIT,
  • Макрос планирования (по сути запуска) PT_SCHEDULE,
  • Макрос для произведения значения PT_YIELD,
  • Макросы для использования семафоров PT_SEM_INIT, PT_SEM_WAIT и PT_SEM_SIGNAL.
Давайте теперь разберемся, как работают макросы.

Сначала давайте посмотрим на программу, использующую протопотоки.

В примере Protothread выполняется вечный цикл, в котором он сначала ожидает достижения счетчиком определенного значения, затем печатает сообщение и сбрасывает счетчик.

  
  
  
   

#include "pt.h" static int counter; static struct pt example_pt; static PT_THREAD(example(struct pt *pt)) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter == 1000); printf("Threshold reached\n"); counter = 0; } PT_END(pt); } int main(void) { counter = 0; PT_INIT(&example_pt); while(1) { example(&example_pt); counter++; } return 0; }

Теперь давайте посмотрим на упрощенную версию макросов, использованных в примере:

struct pt { unsigned short lc; }; #define PT_THREAD(name_args) char name_args #define PT_BEGIN(pt) switch(pt->lc) { case 0: #define PT_WAIT_UNTIL(pt, c) pt->lc = __LINE__; case __LINE__: \ if(!(c)) return 0 #define PT_END(pt) } pt->lc = 0; return 2 #define PT_INIT(pt) pt->lc = 0

Структура pt состоит из одного поля lc (сокращение от локального продолжения).

Обратите внимание на PT_BEGIN и PT_END, которые соответственно открывают и закрывают оператор переключения, а также на немного более сложный макрос PT_WAIT_UNTIL. Он использует встроенный макрос __LINE__, который возвращает текущий номер строки программного файла.

Давайте сравним исходную версию примера и версию, развернутую на препроцессоре:



static PT_THREAD(example(struct pt *pt)) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter == 1000); printf("Threshold reached\n"); counter = 0; } PT_END(pt); }



static char example(struct pt *pt) { switch(pt->lc) { case 0: while(1) { pt->lc = 12; case 12: if(!(counter == 1000)) return 0; printf("Threshold reached\n"); counter = 0; } } pt->lc = 0; return 2; }

Пример протопотока теперь выглядит как обычная функция C. Возвращаемое значение используется для определения статуса протопотока: заблокирован ли он, ожидает чего-либо, завершен, завершился или сгенерировал другое значение.

Макрос PT_BEGIN содержит инструкцию регистра 0, поэтому код сразу после этого макроса будет выполнен первым, поскольку начальное значение pt-> lc равно 0. Посмотрите, во что превратился макрос PT_WAIT_UNTIL. Полю pt-> lc теперь присвоено значение 12, это номер строки, и сразу появляется регистр 12 — благодаря этому коммутатор точно знает, куда переходить.

Если условие не выполнено, функция возвращает 0, что означает, что поток ожидает (все эти константы включены в саму библиотеку).

В следующий раз, когда example() будет вызвана в main, выполнение соответственно продолжится со случая 12, то есть будет проверяться, удовлетворено ли условие ожидания.

Как только счетчик достигнет 1000, условие станет истинным, и функция example() больше не будет возвращать 0, а напечатает сообщение и обнулит счетчик.

Затем, как и ожидалось, переходим к началу тела цикла.

В одна из предыдущих статей Я предоставил код примитивного кооперативного планировщика (раздел «Невытесняющий планировщик с сохранением порядка вызовов»).

Внеся небольшие изменения, вы сможете адаптировать его к протопотокам.

Это довольно просто, поэтому код показывать не буду.

Но предлагаю поиграть желающим.



Сравнение

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

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

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

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

А вот сравнительная таблица:

Тасклет Рабочая очередь Протопотоки
Наличие собственного стека Нет — они обрабатываются как softirq (в специально выделенном стеке, общем для всех процессоров, по крайней мере, в Linux на x86) Да — они выполняются на стеке рабочих потоков, количество которых намного меньше количества задач Нет
Скорость Быстро – минимальная дополнительная обработка Быстрый, но не такой как тасклеты, требует переключения контекста, когда рабочие заменяют друг друга.

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

Примитивы синхронизации Нет (кроме спин-блокировки) Полностью присутствует Псевдосемафоры; примитивное ожидание событий
Концепция планирования Кооперативный планировщик, такой как softirq; тасклеты вытесняются только аппаратными прерываниями Рабочие играют роль планировщика работ и сами контролируются главным планировщиком.

Кооперативный планировщик, реализованный пользователем
У каждого из этих подходов есть свои плюсы и минусы; они используются для разных задач, в контексте которых отвечают на запросы пользователей.

Например, в Эмбокс у нас возникла идея реализовать собственные легковесные потоки, которые не будут иметь своего стека, как protothread и Tasklet, но будут управляться основными планировщиками, как это реализовано в workqueue, а также будут хоть в какой-то форме поддерживать механизм для ожидания событий (даже более того, используйте подмножество того же API, что и полноценные потоки).

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

  • Замена механизма обработки отложенных прерываний;
  • Использование только легких потоков в планировщике позволит обеспечить практически полную многозадачность даже для встраиваемых систем с ограниченными ресурсами;
  • Предыдущий сценарий хорош другим: при портировании системы на новую процессорную архитектуру в первую очередь хочется получить хоть сколько-нибудь работоспособную систему.

    Реализация полноценного context_switch в новом ассемблере в этот момент — лишняя головная боль.

    Чтобы использовать только облегченные потоки, это не требуется.

Расскажу в следующий раз, через некоторое время, как мы это сделали, каких результатов добились.

Теги: #ОС #ядро #многопоточность #протопотоки #Системное программирование #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.