View Categories

Polling и события

Существует два основных подхода к организации логики программы: опрос в суперцикле (superloop polling) и событийно-ориентированный подход (event-based, event-driven).

Polling #

Опрос - это когда мы вручную опрашиваем в суперцикле какие-то функции или методы объектов и ожидаем от них результат. Например - опрос кнопки или доступность Serial для чтения:

void loop() {
    if (buttonClick()) {
        // обработка клика по кнопке
    }

    if (Serial.available()) {
        // чтение данных
    }
}

Это самый простой вариант, у него очень компактная реализация - нет лишнего кода и лишних действий в программе. В то же время он визуально занимает много места - если писать всю программу в loop таким образом - она превратится в нечитаемое полотно из опросов и реакций на их результат.

Функция-обработчик #

Одно из решений - вынести всю логику обработки в отдельную функцию, которую потом можно будет перенести в отдельный файл для удобства:

void processClick() {
    // обработка клика по кнопке
}

void loop() {
    if (buttonClick()) processClick();
}

Получилась функция для обработки события (event) - клика по кнопке, функция будет вызвана, когда произойдёт событие. Такая функция называется обработчиком.

Callback #

Здесь мы всё ещё вручную опрашиваем кнопку и вручную вызываем обработчик, но во многих библиотеках предусмотрено подключение обработчика: можно указать, какую функцию нужно вызвать при наступлении события. Такая функция-обработчик, переданная объекту, называется callback - объект запоминает её к себе в память и вызывает при наступлении события.

Существует также термин event listener - это тоже функция-обработчик события, но событие в данном случае рассылается всем, кто его ожидает. Грубо говоря, callback - это когда начальник попросил подчинённого лично сообщить ему о выполненной работе. А event listener - когда работник сделал работу и написал об этом публично, и все кому это интересно - прочитают данную информацию, "подпишутся на рассылку"

Это может выглядеть примерно так (используется выдуманный класс кнопки, но мы напишем его позже в другом уроке):

// создание кнопки
Button btn;

void processClick() {
    // обработка клика по кнопке
}

void setup() {
    // подключение обработчика
    btn.onClick(processClick);
}

void loop() {
    // опрос кнопки, может отсутствовать
    btn.tick();
}

Тикер #

В примере выше btn.tick() - некий системный метод, который занимается обработкой кнопки, он должен вызываться в суперцикле как можно чаще - такие штуки часто называют тикерами. Это неблокирующая функция, которая занимается обработкой данных, переключением таймеров и машин состояний внутри себя и обеспечивает работу объекта. Тикера может и не быть - если опрос кнопки реализован на прерываниях (о них - в следующих уроках). Во многих библиотеках используется подобный механизм "тикер + обработчики" - в суперцикл помещается тикер, подключаются обработчики и оно работает.

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

Некоторые языки, например JavaScript, изначально событийно-ориентированные - в нём нет суперцикла и не нужно его делать: всё работает на событиях и обработчиках. Но "под капотом" цикл всё же имеется (и даже называется event loop) - в нём точно так же проверяются события и вызываются подключенные обработчики, просто это полностью скрыто от программиста.

Свой класс с callback #

Напишем свой класс с поддержкой подключения обработчика:

class Foo {
  public:
    // подключить обработчик
    void attach(void (*callback)()) {
        _callback = callback;
    }

    // вызвать обработчик
    void call() {
        if (_callback) _callback();  // вызвать если подключен
    }

  private:
    void (*_callback)() = nullptr;  // указатель на обработчик
};
// обработчик
void func() {
    // код...
}

int main() {
    Foo foo;
    foo.attach(func);   // подключаем
    foo.call();         // вызовется func

    // можно через лямбду
    foo.attach([]() {
        // код...
    });
    foo.call();         // вызовется лямбда
}

Функции также могут быть с параметрами, тогда объект сможет отправить нам какие-то данные или даже себя - через указатель или ссылку. Для удобства и сокращения кода указатель на функцию можно обернуть в новый тип данных через typedef или using, это можно сделать прямо в объявлении класса:

class Foo {
    // тип Callback - указатель на функцию (Foo&, int)
    typedef void (*Callback)(Foo&, int);
    //using Callback = int (*)(Foo&, int);  // или так

  public:
    // подключить обработчик
    void attach(Callback callback) {
        _callback = callback;
    }

    // вызвать обработчик
    void call() {
        if (_callback) _callback(*this, 123);  // отправляем себя и какое-то число например
    }

  private:
    Callback _callback = nullptr;
};
// обработчик
void func(Foo& f, int val) {
    // здесь f - объект foo, который вызвал обработчик
    // val в данном случае придёт 123
}

int main() {
    Foo foo;
    foo.attach(func);
    foo.call();
}

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

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх