Корутины? Простейшая имплементация на C, protothread и Arduino

*иногда хочется уйти от управляемых будней

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

для привлечения внимания от топ сео

для привлечения внимания от топ сео

И тут начинается обычное бла-бла про как писать енти самые короутины на языке, проблемы и костыли для их решения.

Но нет, цель статьи, копнуть в сторону «как оно там устроено» на уровне близком к железу.  В качестве примера железа возьмем Arduino с 1 потоком.

Начнем издалека, начнем c Duff’s Device.

Том работал над вопросом как ускорить, казалось бы, простой код.

send(to, from, count)
	register short *to, *from;
	register count;
	{
		do{
			*to = *from++;
        }while(--count>0);
	}

Естественным ходом было думать в сторону снижения количества итераций в while. Потому что… описание всего этого займет много и долго.  В общем, если посмотреть наиболее глупым образом то, в сгенерированном ассемблере слишком много переходов, как раз из-за while.  Надо только заменить цикл от 0 до n строками вида *to = *from++; (запахло векторизацией, но у нас всего-то чип arduino, поэтому забудем это умное слово на время). И тогда Тому пришла гениальная идея, а что, если я просто поменяю сравнения на goto (звуки фу и закатывания глаз). Но используя интересную фичу С компилятора switch case, это как syntax sugar вокруг GOTO, то есть вы свободно и не парясь можете писать код логически разорванный этими переходами и он скомпилируется. И вот что у него вышло:

send(to, from, count)
	register short *to, *from;
	register count;
	{
		register n=(count+7)/8;
		switch(count%8){
		case 0:	do{	*to = *from++;
		case 7:		*to = *from++;
		case 6:		*to = *from++;
		case 5:		*to = *from++;
		case 4:		*to = *from++;
		case 3:		*to = *from++;
		case 2:		*to = *from++;
		case 1:		*to = *from++;
			}while(--n>0);
		}
	}

Как он сам написал: It amazes me that after 10 years of writing C there are still little corners that I haven’t explored fully.  (Actually, I have another revolting way to use switches to implement interrupt driven state machines but it’s too horrid to go into.) (думаю перевод не нужен)

Итого он значительно снизил количество проверок и переходов, и смог разогнать код. Кроме того, это код показателен для управления состояниями.

Реализация короутин с простейшим Round Robin

Давайте рассмотрим реализацию короутин с простейшим Round Robin. За малым исключением мы не будем усложнять дело, управление квантованием времени и т.п. откинем, а возьмем только: Циклический порядок (задачи обрабатываются в порядке их поступления и без возможности прерывания без спец синтаксиса. После завершения времени выполнения задачи, она помещается в конец очереди, и процессор переходит к следующей задаче).

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

Возвращаемся к коду машины Duff-а, пусть у нас есть 2 потока. 1 поток пишет «привет» и ждет пока второй поток поставит переменную global на 1. А второй ждет, когда tickcount увеличиться на 50 и поставит global равным 1.

Попробуем это сделать на псевдо С.

  1. нам нужно как-то понимать куда мы должны возвращаться. У нас есть уникальные … номера строк.

  2. нам нужно где-то это хранить. Пускай будет структура вида

struct context { unsigned short line_number; };
enum { WAITING, YIELDED, EXITED };

Тогда поток будет выглядеть так:

int thread1(struct context * context) {
    switch(context -> line_number) {
        case 0:;
            printf("Привет\n");
            context -> line_number  = __LINE__;  // Запоминаем текущую строку
            case __LINE__: //при следующем заходе switch сможет перекинуть сюда
            if (global !=1){  // Проверяем условие
                return WAITING;  // Возвращаемся, если условие не выполнено
            }
            printf("Мир\n");
}//закрываем switch 
            context -> line_number // Обнуляем line_number в конце
 return EXITED;
}

Поток 2:

int thread1(struct context * contex, int *start_tick_count) {
    switch(context -> line_number) {
        case 0:;
            context -> line_number  = __LINE__;  // Запоминаем текущую строку
            case __LINE__: //при следующем заходе switch сможет перекинуть сюда
            printf("waiting");
            if ((current_ticks() - *start_tick_count) < 50){  // Проверяем условие
                return WAITING;  // Возвращаемся, если условие не выполнено
            }
    }//закрываем switch 
    context -> line_number // Обнуляем line_number в конце
    return EXITED;
}

И наш супер продвинутый round robin scheduler:

int global = 0;
int start_ticks=0;
void main(…){
  context c1;
  context c2;
  start_ticks = current_ticks();
  while(true){
  	thread1(c1);
    thread2(c2, & start_ticks)
  }
}

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

Как это работает: внутри функций, перед выходом return WAITING; сохраняется состояние, куда нужно вернутся. При следующем вызове switch просто перекинет на нужный нам case LINE и продолжит выполнение.

26569700aeaea8ae3a88ce933a3d21b9.png

Осталось обернуть это все в макросы, и мы изобрели простые легковесные короутины которые подойдут даже для arduino. Но… за нас это все уже сделали.

5281ed51fabe8b1ed2b861f334888369.png

Protothreads

https://dunkels.com/adam/pt/ великолепный по своей простоте проект.

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

Идея простая: крутим колеса пока датчик сонара не покажет, что расстояние меньше 25 см. тогда пытаемся повернуть пока расстояние не увеличится.

Итого нам нужно 3 короутины

      1. Читает датчик сонар расстояние

static PT_THREAD(read_distance(struct pt *pt)) {
    PT_BEGIN(pt);

    while(1) {
        int currentDistance = sonarForward.ping_cm();
        if(currentDistance == 0) {
            currentDistance = 100;
        }
#ifdef DEBUG_DISTANCE
        printDistance(currentDistance);
#endif


        distance = currentDistance;
        PT_SLEEP(pt, 100);
    }

    PT_END(pt);
}
  1. Двигается вперед пока расстояние до обьекта больше 25 см

static PT_THREAD(move_forward(struct pt *pt)) {
    PT_BEGIN(pt);

    while(1) {
        PT_WAIT_UNTIL(pt, distance > MIN_DISTANCE_FORWARD);
        printText("forward");
#ifndef EMULATION
        motor1.run(FORWARD);
        motor2.run(FORWARD);
        motor1.setSpeed(200);
        motor2.setSpeed(200);
#endif
        PT_SLEEP(pt, 100);
    }

    PT_END(pt);
}
  1. Разворачивает если расстояние меньше 25 см

static PT_THREAD(try_rotate(struct pt *pt)) {
    PT_BEGIN(pt);

    while(1) {
        PT_WAIT_UNTIL(pt, distance < MIN_DISTANCE_FORWARD);
        printText("rotate");
#ifndef EMULATION
        motor1.run(BACKWARD);
        motor2.run(FORWARD);
        motor1.setSpeed(150);
        motor2.setSpeed(150);
#endif
        PT_SLEEP(pt, 100);
    }

    PT_END(pt);
}

Полный код примера

Для переносимости решения, например, на phreads можно обернуть вызовы в похожие макросы, для примера:

#define PT_THREAD(name) void *name(void *arg)

#define PT_WAIT_UNTIL(pt, condition) \
    do { \
        if (!(condition)) { \
            (pt)->condition = false; \
            pthread_cond_wait(&(pt)->cond, &(pt)->mutex); \
        } \
    } while (0)

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

*иногда хочется уйти от управляемых будней

*иногда хочется уйти от управляемых будней

© Habrahabr.ru