Протопотоки (protothreads)

Опубликовано в рубрике "Статьи", 20 июля, 2010.

Protothreads – очень удобная библиотека, реализующая аналог кооперативной ОС с очень маленькими затратами ресурсов (2 байта на поток!). Особенно она актуальна для небольших контроллеров и, собственно снимает извечный вопрос – пользоваться ОС или нет.

threads

Основные “фишки”

  • Очень маленькие затраты – достаточно всего два байта на поток
  • Библиотека написана на чистом C и C-препроцессоре без ассемблера и, поэтому, очень легко переносится
  • Выпускается под  BSD — лицензией

 

Что такое и как работают протопотоки

В принципе, protothreads не представляют ничего нового. Это просто удобно завернутые машины состояний. Благодаря удачному выбору макросов создается впечатление, что используется кооперативная ОС.

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

 

#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);
}

void main()
{
  counter = 0;
  PT_INIT(&example_pt);
  
  while(1)
  {
    example(&example_pt);
    counter++;
  }
}
#include "pt.h"

static int counter;
static struct pt example_pt;

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

void main()
{
  counter = 0;
  (&example_pt)->lc = 0;
  
  while(1)
  {
    example(&example_pt);
    counter++;
  }
}

 

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

Итак, слева – протопоток. Состояние каждого протопотока хранится в структуре pt. После входа в main, программа инициализирует единственную переменную lc в структуре pt в ноль.

(&example_pt)->lc = 0;

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

Далее идет общеизвестный бесконечный цикл, который является аналогом планировщика ОС, адепты протопотоков часто его так и называют — планировщик.

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

Для генерирования состояний используется макрос __LINE__ препроцессора (этот макрос заменяется на номер текущей строчки в коде).

Итак, функция-поток объявляется так:

static PT_THREAD(example(struct pt *pt))


И превращается препроцессором в обычную си-функцию, возвращающую char. Возвращаемое значение можно использовать в “планировщике” для того, чтобы определить что произошло с потоком – он ожидает события или завершился.

Макрос PT_BEGIN(pt); начинает машину состояний и превращается в switch, а макрос PT_END(pt); завершает машину состояний.

Далее идет while. Да-да, while внутри switch’а является хоть и немного бредоватой, но совершенно правильной С-конструкцией. Такую конструкцию именуют “машина Дафа” (википедия знает).

Теперь, самое важное! Видно, что PT_WAIT_UNTIL превратилось в хитрую конструкцию с числом 11. 11 – это тот самый номер строки, который одновременно будет играть роль состояния. 11 сохраняется в переменной lc, и создается новое состояние (case 11:). Далее, проверяется условие перехода в новое состояние (counter == 1000). Если условие не выполняется, то поток выходит и дает работать другим потокам.

Посмотрим, что будет при следующем входе в поток. switch передаст управление на case 11, и там еще раз проверится условие (counter == 1000). Если условие опять не выполняется, поток возвратит управление планировщику. Таким образом, поток будет ожидать пока counter не станет равным 1000,  практически не потребляя процессорных ресурсов!

После того, как counter стал равным 1000, выполняется  printf("Threshold reached\n"); и while(1) начинает поток с начала.

 

Сервисы библиотеки

void PT_INIT(struct pt *pt);
Инициализирует структуру с описанием протопотока. Протопоток должен быть инициализирован до начала выполнения

void PT_BEGIN(struct pt *pt);
Объявляет начало протопотока внутри функции.

void PT_END(struct pt *pt);
Объявляет окончание протопотока внутри функции.


void PT_WAIT_UNTIL(struct pt *pt, condition);
Блокирует поток до тех пор, пока условие не станет верным

void PT_WAIT_WHILE(struct pt *pt, condition);
Блокирует поток до тех пор, пока условие верно

void PT_WAIT_THREAD(struct pt *pt, thread);
Блокирует поток до тех пор, пока дочерний поток не завершится

void PT_SPAWN(struct pt *pt, struct pt *child, thread);
Порождает дочерний протопоток и ждет, пока он завершится.

void PT_RESTART(struct pt *pt);
Сброс машины состояния потока.

void PT_EXIT(struct pt *pt);
Выходит из потока.

int PT_SCHEDULE(protothread);
Выполняет поток. Возвращает ноль, если поток завершился.

void PT_YIELD(struct pt *pt);
Возвращает управление планировщику.

 

Недостатки

Как-же без них? Первое и самое обидное – локальные переменные не сохраняются между вызовами сервисов библиотеки. Это – плата за отсутствие стека у каждого потока. Все переменные, которые должны сохранятся между вызовами должны быть помечены как static.

Второе – сервисы протопотоков нельзя вызывать из вызываемых функций (во как завернул). Хотя, это не такой уж и большой недостаток.

 

Локальное продолжение

Библиотека protothreads предоставляет два варианта переходов по машине состояний. Автор назвал это заумным выражением “local continuation”.

Первое – с помощью switch-case. Работает практически везде, но есть недостаток – внутри потока нельзя использовать switch. Этот метод используется по умолчанию.

Второе – с помощью меток. К сожалению, второй способ работает только в gcc. зато можно использовать switch.

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

#define LC_INCLUDE lc-addrlabels.h

 

Заключение

Протопотоки – очень удобное средство структурирования вашей программы. Особенно, если все ее модули вы пишете сами.

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

Протопотоки не требуют синхронизации между потоками. Все потоки и так выполняются синхронно (однако, про синхронизацию потоков и прерываний забывать не стоит!)

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

Протопотоки – не замена RTOS. У протопотоков нет временного детерминизма. Тоесть, невозможно предсказать, когда выполнится та или иная часть кода. Однако для многих систем это не очень актуально.

 

Ссылки

Естественно, намного больше, чем в этой статье, можно узнать на странице автора протопотоков. Там-же можно и скачать эту замечательную библиотеку.




Комментарии
  1. bialix написал(а) 15 ноября, 2010 в 13:17

    строго говоря, использовать switch внутри protothreads можно независимо от lc-*.h заголовка, только внутри такого switch нельзя использовать макросы PT_*

    BSVi Reply:

    Да, вы правы )

  2. mcsa написал(а) 15 мая, 2013 в 0:29

    День добрый.
    Одного не пойму, при каких условиях может выполнится та часть, что за пределами while(1) —— (pt->lc = 0; return 2;)

Создать новую ветку комментариев


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