Корутины? Простейшая имплементация на 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.
Попробуем это сделать на псевдо С.
нам нужно как-то понимать куда мы должны возвращаться. У нас есть уникальные … номера строк.
нам нужно где-то это хранить. Пускай будет структура вида
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)
И тогда даже получится переключатся между режимами работы из короутин в многопоточную…, но это уже совсем другая история.
*иногда хочется уйти от управляемых будней