【问题标题】:Invoke a callback in a boost asio GUI loop exactly once per frame每帧只调用一次 boost asio GUI 循环中的回调
【发布时间】:2020-06-15 19:00:08
【问题描述】:

以下问题源自https://github.com/cycfi/elements/issues/144,这是我努力在元素 GUI 库中找到一种方法来每帧调用一次回调。

到目前为止,在我见过的每个库中,都有一些回调/显式循环持续处理用户输入、测量自上一帧以来的时间并执行渲染。

在元素库中,这样的循环是特定于平台的实现细节,相反,使用库的代码可以访问boost::asio::io_context 对象,任何可调用对象都可以是posted。 poll 在特定于平台的事件循环中被调用。

将代码从典型的瀑布式 update(time_since_last_frame) 更改为 posting 执行此操作的函子没有问题,但这是真正问题开始的地方:

  • 发布的函子只被调用一次。图书馆作者的回答是“再发一次”。
  • 如果我立即从函子再次发布,我会创建一个无休止的繁忙循环,因为一旦来自poll 的函子完成,boost asio 就会运行新发布的函子。由于无限的自我转发回调循环,这完全冻结了运行 GUI 的线程。库作者的回答是“使用计时器发布”。
  • 如果我使用计时器发帖,我不会修复任何问题:
    • 如果时间太短,会在回调结束前用完,所以再次调用新发布的回调副本……再次带来无限循环。
    • 如果时间太大导致无限循环,但小到可以在一帧内多次放置,则每帧运行多次...这是一种浪费,因为计算 UI/ 没有意义每帧多次动画/输入状态。
    • 如果时间过长,则不会在每一帧上调用回调。应用程序多次渲染而不处理用户生成的事件……这是一种浪费,因为每次逻辑更新都会多次渲染相同的状态。
    • 无法计算 FPS,因为使用库的代码甚至不知道在发布的回调之间(如果有)渲染了多少帧。

换句话说:

  • 在典型的更新+输入+渲染循环中,循环会尽可能快地运行,产生尽可能多的帧(或者由于休眠而产生指定的上限)。如果代码很慢,那只是 FPS 损失。
  • 在元素库中,如果回调太快,则每帧会重复多次,因为注册的计时器可能会在一帧内完成多次。如果代码太慢,那就是一个“死锁”的回调循环,永远无法摆脱 asio 的poll

我不希望我的代码每 X 次都被调用(或者因为 OS 调度程序而超过 X 次)。我希望我的代码每帧调用一次(最好使用 time delta 参数,但我也可以从之前的调用中自己测量它)。

在元素库中这样使用 asio 是一个糟糕的设计吗? 我发现“带有计时器的帖子”解决方案是一种反模式。对我来说,这就像通过在其中一个线程中添加一个睡眠来修复两个线程之间的死锁,并希望它们在这样的更改之后永远不会发生冲突——如果元素我发布了一个定时回调并希望它不会太快浪费 CPU 但是也不要慢到导致无限的定时回调循环。理想时间太难计算了,因为影响它的因素太多,包括用户操作——基本上是双输的情况。

额外说明1:我试过defer而不是poll,没有区别。

额外说明 2:我已经为图书馆创建了 100 多个问题/PR,因此很有可能一个激励性的答案将在另一个 PR 中结束。换句话说,尝试修改库的解决方案也很好。

额外说明 3:MCVE(这里没有计时器,这会导致几乎无限循环,直到计数器完成,在计算期间 GUI 线程被冻结):

#include <elements.hpp>

using namespace cycfi::elements;

bool func()
{
        static int x = 0;
        if (++x == 10'000'000)
                return true;

        return false;
}

void post_func(view& v)
{
        if (!func())
                v.post([&v](){ post_func(v); });
}

int main(int argc, char* argv[])
{
    app _app(argc, argv);
    window _win(_app.name());
    _win.on_close = [&_app]() { _app.stop(); };

    view view_(_win);
    view_.content(box(rgba(35, 35, 37, 255)));

    view_.post([&view_](){ post_func(view_); });

    _app.run();
    return 0;
}

【问题讨论】:

  • “自我转发”和“每帧一次”绝对是相互矛盾的目标。你能否澄清一下(也许用更少的词?)
  • 我希望我的代码每帧调用一次,通过“自我转发”我的意思是一个可调用对象,当完成时,将其自身的另一个副本添加到 asio 的 io 上下文队列中。更正了标题。
  • 这意味着您最终将每帧调用无数次。 (因为您每帧添加一个,但之前发布的每个处理程序都会自行重新发布)
  • 这就是我要避免的。我可以添加一个计时器,以便在 X 时间之后调用它,但它不能保证在帧之间只调用一次。

标签: c++ boost-asio event-loop


【解决方案1】:

所以,终于有时间看看这个了。

在后端,Elements 似乎已经与 Asio 集成。因此,当您将post 任务添加到视图时,它们将成为异步任务。

你可以给他们一个延迟,这样你就不必忙于循环。

让我们做一个演示

定义任务

让我们定义一个具有虚假进度和固定完成期限的任务:

#include <utility>
#include <chrono>
using namespace std::chrono_literals;
auto now = std::chrono::high_resolution_clock::now;

struct Task {
    static constexpr auto deadline = 2.0s;
    std::chrono::high_resolution_clock::time_point _start = now();
    bool _done = false;

    void reset() { *this = {}; }

    auto elapsed() const { return now() - _start; } // fake progress
    auto done() { return std::exchange(_done, elapsed() > deadline); }
};

如何自上链?

正如您所注意到的,这很棘手。你可以弯下腰,只是键入擦除你的处理程序:

std::function<void()> cheat;
cheat = [&cheat]() {
    // do something
    cheat(); // self-chain
};

不过,为了逗你开心,让我介绍一下函数式编程称为Y combinator

#包括

template<class Fun> struct ycombi {
    Fun fun_;
    explicit ycombi(Fun fun): fun_(std::move(fun)) {}
    template<class ...Args> void operator()(Args &&...args) const {
        return fun_(*this, std::forward<Args>(args)...);
    }
};

这样,我们可以创建一个通用的处理程序发布链接器:

auto chain = [&view_](auto f) {
    return ycombi{ [=, &view_](auto self) {
        view_.post(10ms, [=] {
            if (f())
                self();
        });
    } };
};

我选择了 10 毫秒延迟,但您不必这样做。不做延迟意味着“尽快”,这相当于给定资源的每一帧。

记者任务

让我们更新一个进度条:

auto prog_bar = share(progress_bar(rbox(colors::black), rbox(pgold)));

auto make_reporter = [=, &view_](Task& t) {
    static int s_reporter_id = 1;
    return [=, id=s_reporter_id++, &t, &view_] {
        std::clog << "reporter " << id << " task at " << (t.elapsed() / 1.0ms) << "ms " << std::endl;

        prog_bar->value(t.elapsed() / Task::deadline);
        view_.refresh(*prog_bar);

        if (t.done()) {
            std::clog << "done" << std::endl;
            return false;
        }
        return true;
    };
};

现在。让我们添加一个按钮来开始更新进度条。

auto task_btn = button("Task #1");
task_btn.on_click = [=,&task1](bool) {
    if (task1.done())
        task1.reset();
    auto progress = chain(make_reporter(task1));
    progress();
};

让我们将按钮和栏放在视图中并运行应用程序:

view_.content(task_btn, prog_bar);
view_.scale(8);

_app.run();

完整列表

使用当前元素大师 (a7d1348ae81f7c)

  • 文件test.cpp

     #include <utility>
     #include <chrono>
     using namespace std::chrono_literals;
     auto now = std::chrono::high_resolution_clock::now;
    
     struct Task {
         static constexpr auto deadline = 2.0s;
         std::chrono::high_resolution_clock::time_point _start = now();
         bool _done = false;
    
         void reset() { *this = {}; }
    
         auto elapsed() const { return now() - _start; } // fake progress
         auto done() { return std::exchange(_done, elapsed() > deadline); }
     };
    
     #include <functional>
    
     template<class Fun> struct ycombi {
         Fun fun_;
         explicit ycombi(Fun fun): fun_(std::move(fun)) {}
         template<class ...Args> void operator()(Args &&...args) const {
             return fun_(*this, std::forward<Args>(args)...);
         }
     };
    
     #include <elements.hpp>
     #include <iostream>
    
     using namespace cycfi::elements;
    
     constexpr auto bred   = colors::red.opacity(0.4);
     constexpr auto bgreen = colors::green.level(0.7).opacity(0.4);
     constexpr auto bblue  = colors::blue.opacity(0.4);
     constexpr auto brblue = colors::royal_blue.opacity(0.4);
     constexpr auto pgold  = colors::gold.opacity(0.8);
    
     int main(int argc, char* argv[]) {
         app _app(argc, argv);
         window _win(_app.name());
         _win.on_close = [&_app]() { _app.stop(); };
    
         view view_(_win);
    
         Task task1;
    
         auto chain = [&view_](auto f) {
             return ycombi{ [=, &view_](auto self) {
                 view_.post(10ms, [=] {
                     if (f())
                         self();
                 });
             } };
         };
    
         auto prog_bar = share(progress_bar(rbox(colors::black), rbox(pgold)));
    
         auto make_reporter = [=, &view_](Task& t) {
             static int s_reporter_id = 1;
             return [=, id=s_reporter_id++, &t, &view_] {
                 std::clog << "reporter " << id << " task at " << (t.elapsed() / 1.0ms) << "ms " << std::endl;
    
                 prog_bar->value(t.elapsed() / Task::deadline);
                 view_.refresh(*prog_bar);
    
                 if (t.done()) {
                     std::clog << "done" << std::endl;
                     return false;
                 }
                 return true;
             };
         };
    
         auto task_btn = button("Task #1");
         task_btn.on_click = [=,&task1](bool) {
             if (task1.done())
                 task1.reset();
             auto progress = chain(make_reporter(task1));
             progress();
         };
    
         view_.content(task_btn, prog_bar);
         view_.scale(8);
    
         _app.run();
     }
    

【讨论】:

  • 感谢您提供详细示例,但这不是一个有效的答案。 10ms 除了在 10ms 之后被调用(或更多,取决于操作系统调度程序)之外,不保证任何东西。 “不延迟意味着“尽快”,这相当于每一帧” - 这是错误的 - 试试吧。如果您不设置延迟,则发布的处理程序会立即一遍又一遍地调用,因为使用 ycombinator,asio 队列永远不会耗尽任务。没有延迟和重新发布函数基本上是一个无限循环。为了节省您一些时间-我已经找到了方法-它是从view继承并覆盖poll
  • 我的评论基于库开发人员的声明:"view::post is async and posted tasks are handled once every 60fps in the main thread"。不要射击信使。此外,将答案称为“无效”是很苛刻的。另外,不要为我节省一些时间。通过发布您的答案,您可以节省人们的时间!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多