Русский
Русский
English
Статистика
Реклама

Coroutine

Из песочницы Корутины в C20

26.09.2020 14:08:40 | Автор: admin

Введение


Данная статья является переводом главы из книги Райнера Гримма Concurrency with Modern C++, которая является более доработанной и обширной версией статьи на его сайте. Так как весь перевод не умещается в рамках данной статьи, в зависимости от реакции на публикацию, выложу оставшуюся часть.


Корутины


Корутины это функции которые могут приостановить или возобновить свое выполнение при этом сохраняя свое состояние. Эволючия функций в C++ сделала шаг вперед. Корутины с наибольшей вероятностью войдут вошли в C++20.


Идуя корутин, представленная как новая в C++20, довольно стара. Понятие корутины было предложено Мелвином Конвеем. Он использовал данное понятие в публикации о разработке компиляторов от 1963. Дональд Кнут называл процедуры частным случаем корутин. Иногда должно пройти время чтобы та или иная идея была принята.


Посредством новых ключевых слов co_await и co_yield C++20 расширяет понятие выполнения функций в C++ при помощи двух новых концепций.


Благодаря co_await expression появляется возможность приостановки и возобновления выполнения expression. В случае использования co_await expression в функции func вызов auto getResult = func() не является блокирующим, если результат данной функции недоступен. Вместо потребляющей ресурсы блокировки (resourse-consuming blocking) осуществляется экономящее ресурсы ожидание (resource-friendly waiting).


co_yield expression позволяет реализовывать функции генераторы. Генераторы функции, которые возвращают новое значение с каждым последующим вызовом. Функция генератор является подобием потоков данных (data stream) из которых можно получать значения. Потоки данных могут быть бесконечными. Таким образом, данные концепции являются основополагающими ленивых вычислений в C++.


Функции генераторы


Ниже представленный код упрощён до невозможности. Функция getNumbers возвращает все целые числа от begin до end с шагом inc. begin должно быть меньше end, а inc должен быть положительным.


Жадный генератор
// greedyGenerator.cpp#include <iostream>#include <vector>std::vector<int> getNumbers(int begin, int end, int inc = 1) {    std::vector<int> numbers; // (1)    for (int i = begin; i < end; i += inc) {        numbers.push_back(i);    }    return numbers;}int main() {    const auto numbers = getNumbers(-10, 11);    for (auto n : numbers) {        std::cout << n << " ";    }    std::cout << "\n";    for (auto n : getNumbers(0, 101, 5)) {        std::cout << n << " ";    }    std::cout << "\n";}

Конечно, реализация getNumbers является велосипедом, потому что может быть заменена std::iota с C++11.


Для более полного представления, вывод программы:


$ ./greedyGenerator-10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 

В данной программе есть два наиболее важных аспекта. Во-первых, вектор numbers (см. комментарий (1) в коде) всегда хранит весь набор данных. Это будет происходить даже если пользователя интересуют первые 5 из 1000 элементов вектора. Во-вторых, достаточно легко преобразовать функцию getNumbers в ленивый генератор.


Ленивый генератор
// lazyGenerator.cpp#include <iostream>#include <vector>generator<int> generatorForNumbers(int begin, int inc = 1) {    for (int i = begin; ; i += inc) { // (4)        co_yield i; // (3)    }}int main() {    const auto numbers = generatorForNumbers(-10); // (1)    for (int i = 1; i <= 20; ++i) { // (5)        std::cout << numbers << " ";    }    std::cout << "\n";    for (auto n : generatorForNumbers(0, 5)) { // (2)        std::cout << n << " ";    }    std::cout << "\n";}

Примечание переводчика: данный код не скомпилируется, т.к. является лишь наглядным примером использования концепций. Рабочие примеры генератора будут далее.
Для сравнения, функция getNumbers из примера greedyGenerator.cpp возвращает std::vector<int>, тогда как корутина generatorForNumbers из файла lazyGenerator.cpp возвращает. Генератор numbers в строке с меткой (1) или генератор generatorForNumbers(0, 5) с пометкой (2) возвращают новые значения по запросу. Range-based for инициирует запрос. Если точнее, то запрос к корутине возвращает значение i посредством co_yield i (см. метку (3)) и немедленно приостанавливает выполнение. Если запрашивается новое значение, корутина продолжает выполнение с данного конкретного места.


Выражение generatorForNumbers(0, 5) (см. метку (2)) является генератором по месту использования (just-in-place usage).


Важно обратить внимание на один аспект. Корутина generatorForNumbers создает бесконечный поток данных, потому что цикл for в строке с меткой (4) не имеет условия завершения. Данный подход не является ошибочным, т.к., например, в строке (5) осуществляется запрос конечного числа элементов. Что, однако, не справедливо для выражения в строке (2) которое будет выполняться бесконечно.


Подробности


Типичные сценарии использования


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


Основополагающие концепции


Корутины в C++20 асимметричные, первого класса (first-class) и бесстековые (stackless).
Асимметричные корутины возвращают контекст выполнения вызывающей стороне. Напротив, симметричные корутины делегируют последующее выполнение другой корутине.
Корутины первого класса идентичны функциям первого класса потому что корутины могут вести себя как данные. Аналогичное данным поведение означает, что корутины могут быть аргументами или возвращаемыми значениями функций или храниться в переменных.


Бесстековые корутины позволяют приостанавливать или возобновлять работу корутин более высокого уровня. Выполнение корутин и приостановка в корутине возвращает выполнение вызывающей стороне. Бесстековые корутины часто называют возобновляющими работу функциями (resumable functions).


Цели проектирования


Гор Нишанов описал следующие цели проектирования корутин.
Корутины должны:


  • быть высоко масштабируемыми (до миллиардов одновременно работающих корутин).
  • высоко эффективно продолжать и приостанавливать работу, сравнимо с накладными расходами функций.
  • бесшовного взаимодействовать с существующими особенностями без дополнительных накладных расходов.
  • иметь открытый механизм взаимодействия для реализации библиотек c различными вариантами высокоуровневых семантик, например, генераторы, горутины, задачи и тому подобное.
  • иметь возможность использования в средах где исключения запрещены или невозможны.

В соответствии с такими пунктами как масштабирования и бесшовного взаимодействия с существующими особенностями, корутины являются бесстековыми. Напротив, стековые корутины резервируют для стека по-умолчанию 1MB в Windows и 2MB в Linux.


Формирование корутин


Функция сановится корутиной если использует


  • co_return
  • co_await
  • co_yield
  • co_await expression в range-based for циклах

Ограничения


Корутины не могут содержать выражение return или замещающие возвращаемые типы. Это относится как к неограниченным заместителям (auto), так и к неограниченным заместителям (концепты).


В дополнение, constexpr функции, конструкторы, деструкторы и функция main не могут быть корутинами.
Подробно про данные ограничения можно прочитать в proposal N4628.


co_return, co_yield и co_await


Корутина использует co_return для возврата значения.


Благодаря co_yield появляется возможность реализации генераторов бесконечных потоков данных из которых можно получать значения по запросу. Возвращаемый тип генератора generator<int> generatorForNumbers(int begin, int inc = 1) это generator<int>.generator<int> внутри которого специальный promise p такой, что вызов co_yield i является идентичным вызову co_await p.yield_value(i).co_yield i может быть вызван произвольное число раз. Мгновенно после вызова выполнение корутины приостанавливается.
co_await способствует тому, что выполнение корутины может быть приостановлено и возобновлено. Выражение exp в co_await exp должно являться, что называется, ожидающим выражением (далее awaitables). exp должно реализовывать специальный интерфейс, который состоит из трёх функций: await_ready, await_suspend и await_resume.
Стандарт C++20 уже имеет 2 определения awaitables: std::suspend_always и std::suspend_never.
std::suspend_always


struct suspend_always {    constexpr bool await_ready() const noexcept { return false; }    constexpr void await_suspend(coroutine_handle<>) const noexcept {}    constexpr void await_resume() const noexcept {}};

Как указано в имени, awaitable std::suspend_always приостанавливает выполнение всегда, поэтому await_ready возвращает false. Противоположная идея лежит в основе std::suspend_never.
std::suspend_never


struct suspend_always {    constexpr bool await_ready() const noexcept { return true; }    constexpr void await_suspend(coroutine_handle<>) const noexcept {}    constexpr void await_resume() const noexcept {}};

Наиболее распространенный вариант использования co_await это сервер ожидающий событий.
Блокирующий сервер


Acceptor acceptor{443};while (true) {    Socket socket = acceptor.accept();          // blocking    auto request = socket.read();               // blocking    auto response = handleRequest(request);    socket.write(response);                     // blocking}

Описанный сервер достаточно прост ввиду последовательного ответа на каждый запрос в одном и том же потоке. Сервер слушает 443 порт, принимает соединения, читает входные данные от клиента и отправляет ответ клиенту. В комментариях обозначены строки являющиеся блокирующими.
Благодаря co_await блокирующие вызовы могут быть приостановлены и возобновлены.
Ожидающий сервер


Acceptor acceptor{443};while (true) {    Socket socket = co_await acceptor.accept();    auto request = co_await socket.read();    auto response = handleRequest(request);    co_await socket.write(response);}

Фреймворк


Фреймворк для написания корутин состоит из более чем 20 функций которые частично нужно реализовать, а частично могут быть переписаны. Таким образом корутины могут быть адаптированы под каждую конкретную задачу.
Корутина состоит из трех частей: promise объект, handle корутины и frame корутины.
Promise объект является объектом воздействия изнутри корутины и осуществляет доставку результата из корутины.
Handle корутины это не владеющий handle для продолжения работы или уничтожения frame корутины снаружи.
Frame корутины это внутреннее, обычно размещенное на куче состояние. Сосотоит из ранее упомянутого promise объекта, копий параметров корутины, представления точки приостановки (suspention point), локальных переменных, время жизни которых заканчивается до точки приостановки и локальных переменных, которые превышают время жизни точки приостановки.
Необходимо соблюсти два требования для оптимизации аллокации корутины:


  1. Время жизни корутины должно быть вложенным во время жизни вызывающей сущности.
  2. Вызывающая корутину сущность должна знать размер frame корутины.

Упрощенный workflow


При использовании в функции co_return или co_yield или co_await таковая становится корутиной и компилятор преобразует её тело в нечто похожее на представленный код.
Тело корутины


{    Promise promise;    co_await promise.initial_suspend();    try {        <тело функции>    } catch (...) {        promise.unhandled_exception();    }FinalSuspend:    co_await promise.final_suspend();}

Workflow состоит из следующих стадий:


  • Корутина начинает выполнение
    • аллоцирование frame корутины при необходимости.
    • копирование всех параметров функции в frame корутины.
    • создание promise объекта promise.
    • вызов promise.get_return_object() для создания handle корутины и сохранение такового в локальной переменной. Результат вызова будет возвращен вызывающей стороне при первой приостановке корутины.
    • вызов promise.initial_suspend() и ожидание co_await результата. Данный тип promise обычно возвращает suspend_never для корутин немедленного выполнения или suspend_always для ленивых корутин.
    • тело корутины выполняется начинает выполнение после co_await promise.initial_suspend()
  • Корутины достигают точки приостановки
    • возвращаемый объект promise.get_return_object() возвращается вызывающей сущности который инициирует продолжение выполнение корутины
  • Корутина достигает co_return
    • вызывается promise.return_void() для co_return или co_return expression, где expression имеет тип void
    • вызывается promise.return_value(expression) для co_return expression, где expression имеет тип отличный от void
    • удаляется весь стек созданных переменных
    • вызывается promise.final_suspend() и ожидается co_await результат
  • Корутина уничтожается (посредством завершения через co_return, необработанного исключения или через halde корутины)
    • вызывается деструктор promise объекта
    • вызывается деструктор параметров функции
    • освобождается память используемая frame корутины
    • передача выполнения вызывающей сущности

Когда корутина завершается посредством необработанного исключения происходит следующее:


  • ловится исключение и вызывается promise.unhandled_exception() из catch блока
  • вызывается promise.final_suspend() и ожидается co_await результата
Подробнее..
Категории: C++ , C++20 , Coroutine

Перевод Корутины в C20. Часть 2

28.09.2020 18:04:45 | Автор: admin

Введение


Данная статья является продолжением данной статьи.


Бесконечный поток данных при помощи co_yield


Код ниже реализует бесконечный поток данных. Корутина getNext использует co_yield для создания потока данных который начинается со start и выдает по запросу каждое новое значение с шагом step.


Бесконечный поток данных
//infiniteDataStream.cpp#include <coroutine>#include <memory>#include <iostream>template <typename T>struct Generator {    struct promise_type;    using handle_type = std::coroutine_handle<promise_type>;    Generator(handle_type h) : coro(h) {}                       // (3)    handle_type coro;    std::shared_ptr<T> value;    ~Generator() {        if (coro) {            coro.destroy();        }    }    Generator(const Generator &) = delete;    Generator& operator=(const Generator &) = delete;    Generator(Generator &&other) : coro(other.coro) {        other.coro = nullptr;    }    Generator& operator=(Generator &&other) {        coro = other.coro;        other.coro = nullptr;        return *this;    }    T getValue() {        return coro.promise().current_value;    }    bool next() {                                               // (5)        coro.resume();        return not coro.done();    }    struct promise_type {        promise_type() = default;                               // (1)        ~promise_type() = default;        auto initial_suspend() {                                // (4)            return std::suspend_always{};        }        auto final_suspend() {            return std::suspend_always{};        }        auto get_return_object() {                              // (2)            return Generator{handle_type::from_promise(*this)};        }        auto return_void() {            return std::suspend_never{};        }        auto yield_value(T value) {                             // (6)            current_value = value;            return std::suspend_always{};        }        void unhandled_exception() {            std::exit(1);        }        T current_value;    };};Generator <int> getNext(int start = 0, int step = 1) {    auto value = start;    for (int i = 0; ; ++i) {        co_yield value;        value += step;    }}int main() {    std::cout << "getNext():";    auto gen = getNext();    for (int i = 0; i <= 10; ++i) {        gen.next();        std::cout << " " << gen.getValue();                     // (7)    }    std::cout << "\ngetNext(100, -10):";    auto gen2 = getNext(100, -10);    for (int i = 0; i <= 20; ++i) {        gen2.next();        std::cout << " " << gen2.getValue();    }    std::cout << std::endl;}

Примечание переводчика: сборку осуществлял командой g++ -fcoroutines infiniteDataStream.cpp
В функции main создается 2 корутины. Первая, gen, возвращает значения от 0 до 10. Вторая, gen2, от 100 до -100 с шагом 10. Вывод программы:


$ ./infDSgetNext(): 0 1 2 3 4 5 6 7 8 9 10getNext(100, -10): 100 90 80 70 60 50 40 30 20 10 0 -10 -20 -30 -40 -50 -60 -70 -80 -90 -100

Метки с числами в комментариях в программе infiniteDataStream.cpp описывают первую итерацию в следующей последовательности:


  1. Создание promise объекта
  2. Вызов promise.get_return_object() и сохранение результата в локальной переменной
  3. Создание генератора
  4. Вызов promise.initial_suspend(), т.к. генератор "ленивый", следовательно, suspend_always
  5. Запрос следующего значения и возврат флага, если генератор исчерпал себя
  6. Действие на co_yield, после чего будет доступно следующее значение
  7. Получение следующего значения

В последующих итерациях выполняются только шаги 5 и 6.


Синхронизация потоков посредством co_await


Для синхронизации потоков рекомендуется использовать co_await. Пока один поток подготавливает обрабатываемый пакет, другой ожидает таковой. Условные переменные (condition variables), promises и futures, а так же атомарные флаги могут быть использованы для реализации модели отправитель-получатель. Благодаря корутинам достаточно легко синхронизировать потоки избегая присущие условным переменным риски, как ложные срабатывания (spurious wakeups) и игнорирование пробуждения (lost wakeups).


Синхронизация потоков
// senderReceiver.cpp#include <coroutine>#include <chrono>#include <iostream>#include <functional>#include <string>#include <stdexcept>#include <atomic>#include <thread>class Event {public:    Event() = default;    Event(const Event &) = delete;    Event(Event &&) = delete;    Event& operator=(const Event &) = delete;    Event& operator=(Event &&) = delete;    class Awaiter;    Awaiter operator co_await() const;    void notify();private:    friend class Awaiter;    mutable std::atomic<void *> suspendedWaiter{nullptr};    mutable std::atomic<bool> notified{false};};class Event::Awaiter {public:    Awaiter(const Event &e) : event(e) {}    bool await_ready() const;    bool await_suspend(std::coroutine_handle<> ch);    void await_resume() {}private:    friend class Event;    const Event &event;    std::coroutine_handle<> coroutineHandle;};bool Event::Awaiter::await_ready() const {    if (event.suspendedWaiter.load() != nullptr) {        throw std::runtime_error("More than one waiter is not valid");    }    return event.notified; // true - корутина выполняется как обычная функция, false - корутина приостановлена}bool Event::Awaiter::await_suspend(std::coroutine_handle<> ch) {    coroutineHandle = ch;    if (event.notified) {        return false;    }    // сохранить waiter для последующего уведомления    event.suspendedWaiter.store(this);    return true;}void Event::notify() {    notified = true;    // попытка загрузить waiter    auto *waiter = static_cast<Awaiter *>(suspendedWaiter.load());    // проверка доступен ли waiter    if (waiter != nullptr) {        // возобновить работу корутины        waiter->coroutineHandle.resume();    }}Event::Awaiter Event::operator co_await() const {    return Awaiter{*this};}struct Task {    struct promise_type {        Task get_return_object() { return {}; }        std::suspend_never initial_suspend() { return {}; }        std::suspend_never final_suspend() { return {}; }        void return_void() {}        void unhandled_exception() {}    };};Task receiver(Event &event) {    auto start = std::chrono::high_resolution_clock::now();    co_await event;    std::cout << "Got the notification!" << std::endl;    auto end = std::chrono::high_resolution_clock::now();    std::chrono::duration<double> elapsed = end - start;    std::cout << "Waited " << elapsed.count() << " seconds." << std::endl;}int main() {    std::cout << "Notification before waiting" << std::endl;    Event event1{};    auto senderThread1 = std::thread([&event1] { event1.notify(); });    auto receiverThread1 = std::thread(receiver, std::ref(event1));    receiverThread1.join();    senderThread1.join();    std::cout << "\nNotification after 2 seconds waiting" << std::endl;    Event event2{};    auto receiverThread2 = std::thread(receiver, std::ref(event2));    auto senderThread2 = std::thread([&event2] {                                         using namespace std::chrono_literals;                                         std::this_thread::sleep_for(2s);                                         event2.notify();                                     });    receiverThread2.join();    senderThread2.join();}

С точки зрения пользователя, синхронизация потоков посредством корутин достаточно проста. Стоит заметить, что в примере senderReceiver.cpp поток senderThread1 и senderThread2 используют событие event для отправки уведомлений (eventN.notify()). Функция обработки уведомлений receiver представляет собой корутину, которая выполняется в потоках receiverThread1 и receiverThread2. Внутри корутины осуществляется замер времени и вывод его на экран, что отображает как долго корутина осуществляла ожидание. Ниже представлен вывод программы.
Вывод программы senderReceiver


$ ./senderReceiverNotification before waitingGot the notification!Waited 3.7006e-05 seconds.Notification after 2 seconds waitingGot the notification!Waited 2.00056 seconds.

Примечание переводчика: сборку осуществлял командой g++ -pthread -fcoroutines senderReceiver.cpp
Если сравнить класс Generator в примере с бесконечным потоком данных и класс Event в предыдущем примере, то можно заметить некоторые различия. В первом случае, Generator одновременно и awaitable и awaiter; Event же использует operator co_await для возврата awaiter. Такое разделение awaitable и awaiter позволяет улучшить структуру кода.
Из вывода можно сделать вывод, что вторая корутина выполняется чуть больше, чем 2 секунды. Причина заключается в том, что event1 посылает уведомление до того, как корутина была приостановлена, однако event2 посылает уведомление после того, как прошло 2 секунды.
Принцип работы корутины в примере senderReceiver.cpp не так лёгок для понимания. Класс Event имеет пару интересных членов: suspendedWaiter и notified. Первый содержит waiter для посылки сигнала, второй же содержит состояние уведомления.
Более детально, event1 посылает уведомление до того как receiverThread1 был запущен. Вызов even1.notify() сначала устанавливает флаг notified после чего загружает потенциального waiter. В данном случае waiter является nullptr т.к. не был установлен ранее, что означает, что последующий waiter->coroutineHandle.resume() не будет выполнен. Впоследствии метод await_ready проверяет был ли установлен waiter и, если был, бросает исключение std::runtime_error. Наиболее важно тут обратить внимание на возвращаемое значение. Значение notified было ранее установлено в true в методе notify, что означает, в данном случае, что корутина не была приостановлена и выполняется как обычная функция.
В случае с event2 вызов co_await event выполняется до того, как посылается уведомление. Данный вызов инициирует выполнение await_ready. Следует заметить ключевое различие, что в данном случае флаг event.notified установлен в значение false что обуславливает приостановку корутины. Технически, вызывается метод await_suspend который получает handle корутины ch и сохраняет его для последующего вызова в переменную corotineHandle. Последующий вызов, в данном случае, означает возобновление работы. К тому же, waiter сохраняется в переменной suspendedWaiter. Когда затем срабатывает уведомление через event2.notify начинает выполнение соответствующий метод notify. Различие тут в том, что в условии где проверяется доступен ли waiter таковой уже не будет nullptr. В результате waiter использует coroutineHandle для возобновления работы корутины.

Подробнее..
Категории: C++ , C++20 , Coroutine

Чем опасен postDelayed

24.09.2020 08:13:47 | Автор: admin

Часто из-за особенностей работы android системы и sdk, нам необходимо подождать, когда определённая часть системы будет сконфигурирована или произойдёт какое-то необходимое нам событие. Зачастую это является костылём, но иногда без них никак, особенно в условиях дедлайнов. Поэтому во многих проектах для этого использовался postDelayed. Под катом рассмотрим, чем же он так опасен и что с этим делать.


Проблема


Для начала рассмотрим как обычно используют postDelayed():


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        view.postDelayed({            Log.d("test", "postDelayed")            // do action        }, 100)}

С виду всё хорошо, но давайте изучим этот код повнимательнее:


1) Это отложенное действие, выполнение которого мы будем ожидать через некоторое время. Зная насколько динамично пользователь может совершать переходы между экранами, данное действие должно быть отменено при смене фрагмента. Однако, этого здесь не происходит, и наше действие выполнится, даже если текущий фрагмент будет уничтожен.
Проверить это просто. Создаём два фрагмента, при переходе на второй запускаем postDelayed с большим временем, к примеру 5000 мс. Сразу возвращаемся назад. И через некоторое время видим в логах, что действие не отменено.


2) Второе "вытекает" из первого. Если в данном runnable мы передадим ссылку на property нашего фрагмента, будет происходить утечка памяти, поскольку ссылка на runnable будет жить дольше, чем сам фрагмент.


3) Третье и основное почему я об этом задумался:
Падения приложения, если мы обращаемся ко view после onDestroyView
synthitec java.lang.NullPointerException, поскольку кеш уже очищен при помощи _$_clearFindViewByIdCache, а findViewById отдаёт null
viewBinding java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null


Что же делать?


1 Если нам нужные размеры view использовать doOnLayout или doOnNextLayout


2 Перенести ожидание в компонент, ответственный за бизнес-логику отображения (Presenter/ViewModel или что-то другое). Он в свою очередь должен устанавливать значения во фрагмент в правильный момент его жизненного цикла или отменять действие.


3 Использовать безопасный стиль.


Необходимо отписываться от нашего действия перед тем, как view будет отсоединено от window.


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)         Runnable {            // do action        }.let { runnable ->            view.postDelayed(runnable, 100)            view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {                override fun onViewAttachedToWindow(view: View) {}                override fun onViewDetachedFromWindow(view: View) {                    view.removeOnAttachStateChangeListener(this)                    view.removeCallbacks(runnable)                }            })        }    }

Обычный doOnDetach нельзя использовать, поскольку view может быть ещё не прикреплено к window, как к примеру в onViewCreated. И тогда наше действие будет сразу же отменено.


Где то во View.kt:


inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {    if (!ViewCompat.isAttachedToWindow(this)) { // выполнится это условие        action(this)  // и здесь мы сразу же отпишемся от действия    } else {        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                action(view)            }        })    }}

Или же обобщим в extension:


fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {        val runnable = Runnable { block() }        postDelayed(runnable, delayMillis)        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                view.removeCallbacks(runnable)            }        })}

В принципе на этом можно остановится. Все проблемы решены. Но этим мы добавляем ещё один тип асинхронного выполнения к нашему проекту, что несколько усложняет его. Сейчас в мире Native Android есть 2 основных решения для асинхронного выполнения кода Rx и Coroutines.
Попробуем использовать их.
Сразу оговорюсь, что не претендую на 100% правильность по отношению к вашему проекту. В вашем проекте это может быть по другому/лучше/короче.


Coroutines


Обычно во всех проектах есть базовый фрагмент для инжекта зависимостей, настройки di и так далее. Используем его для обобщения работы с отменой отложенных действий:


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {    override fun onDestroyView() {        super.onDestroyView()        coroutineContext[Job]?.cancelChildren()    }    override fun onDestroy() {        super.onDestroy()        cancel()    }}

Нам необходимо отменять все дочерние задачи в onDestroyView, но при этом не закрывать scope, поскольку после этого возможно вновь создание View без пересоздания Fragment. К примеру при роутинге вперёд на другой Fragment и после этого назад на текущий.


В onDestroy уже закрываем scope, так как далее никаких задач не должно быть запущено.


Все подготовительные работы сделаны.
Перейдём к самой замене postDelayed:


fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {    view ?: return null    return launch {        delay(delayMillis)        action()    }}

Так как нам не нужно, чтобы наши отложенные задачи выполнялись, если view уже уничтожено, просто не запускаем ничего и возвращаем null. Иначе запускаем наше действие с необходимой задержкой. Но теперь при уничтожении view, задача будет отменена и ресурсы будут отпущены.


RX


В RX за отмену подписок отвечает класс Disposable, но в RX нет Structured concurrency в отличии от coroutine. Из-за этого приходится прописывать это всё самому. Выглядит обычно это примерно так:


interface DisposableHolder {    fun dispose()    fun addDisposable(disposable: Disposable)}class DisposableHolderImpl : DisposableHolder {    private val compositeDisposable = CompositeDisposable()    override fun addDisposable(disposable: Disposable) {        compositeDisposable.add(disposable)    }    override fun dispose() {        compositeDisposable.clear()    }}

Также аналогично отменяем все задачи в базовом фрагменте:


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),    DisposableHolder by DisposableHolderImpl() {    override fun onDestroyView() {        super.onDestroyView()        dispose()    }    override fun onDestroy() {        super.onDestroy()        dispose()    }}

И сам extension:


fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {    view ?: return null    return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {        block()    }.also {        addDisposable(it)    }}

В заключении


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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru