*иногда хочется уйти от управляемых будней
Корутины — это функции, которые могут приостанавливать своё выполнение и возобновлять его позже, сохраняя своё состояние между вызовами. Это позволяет выполнять несколько задач одновременно без необходимости создания отдельных потоков или процессов.
И тут начинается обычное бла-бла про как писать енти самые короутины на языке, проблемы и костыли для их решения.
Но нет, цель статьи, копнуть в сторону «как оно там устроено» на уровне близком к железу. В качестве примера железа возьмем 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.
Попробуем это сделать на псевдо С.
-
нам нужно как-то понимать куда мы должны возвращаться. У нас есть уникальные … номера строк.
-
нам нужно где-то это хранить. Пускай будет структура вида
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 и продолжит выполнение.
Осталось обернуть это все в макросы, и мы изобрели простые легковесные короутины которые подойдут даже для arduino. Но… за нас это все уже сделали.
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); }
-
Двигается вперед пока расстояние до обьекта больше 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); }
-
Разворачивает если расстояние меньше 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)
И тогда даже получится переключатся между режимами работы из короутин в многопоточную… но это уже совсем другая история.
ссылка на оригинал статьи https://habr.com/ru/articles/841472/
Добавить комментарий