1+1=10

扬长避短 vs 取长补短

C++ Qt 协程学习笔记(一)——QTimer与协程

简单学习了 C++ 协程,但是感觉心里空的很。不妨用Qt来试试

目标

使用QTimer 和 C++协程实现如下效果:

qt coroutine

期望的代码:

MyCoroutine updateText(QPlainTextEdit *textEdit)
{
    QTimer timer;
    timer.setInterval(1s);
    timer.start();

    for (int i = 0; i < 10; ++i) {
        co_await timer;
        textEdit->appendPlainText(QString("[%1] Hello coroutine %2")
                                   .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
                                   .arg(i));
    }
}

int main(int argc, char** argv)
{
    QApplication a(argc, argv);

    QPlainTextEdit textEdit;
    textEdit.setWindowTitle("1+1=10");
    textEdit.resize(400, 300);
    textEdit.show();

    updateText(&textEdit);

    return a.exec();
}

注意:在本文中,updateText()共出现有三个稍有区别的版本。请注意区分。

如何实现?一

上面的代码是无法通过编译的,需要完善一下

首先,定义协程函数返回值类型

要定义协程函数,首先要定义一个满足特定要求的类,作为协程返回值类型。

这个简单:

struct MyCoroutine {
    struct promise_type {
        std::suspend_never initial_suspend() {return {}; }
        std::suspend_never final_suspend() noexcept {return {}; }

        void return_void() {}

        MyCoroutine get_return_object() { return {}; }
        void unhandled_exception() { std::terminate(); }
    };
};

因为只是为了满足协程运行基本要求,所以:

  • 这个类里面我们没有放置个性化的内容
  • 使用std::suspend_never 避免手动驱动协程

注意:在本文中,MyCoroutine共出现有两个稍有区别的版本,依据是否包含await_transform进行区分。

其次,使 co_await 工作

由于QTimer自身尚不支持协程(不是Awaitable或Awaiter),所以我们需要封装一下,为其创建一个Awaiter类:

class MyCoroTimer
{
    QTimer &m_timer;
    QMetaObject::Connection m_conn;
public:
    MyCoroTimer(QTimer &timer)
        : m_timer(timer) {}

    bool await_ready() const noexcept {
        return !m_timer.isActive();
    }

    void await_suspend(std::coroutine_handle<> coro) {
        if (m_timer.isActive()) {
            m_conn = QObject::connect(&m_timer, &QTimer::timeout, [this, coro]() {
                QObject::disconnect(m_conn);
                coro.resume();
            });
        } else {
            coro.resume();
        }
    }

    void await_resume() const {}
};

这样一来,下面写法是有效的:

QTimer timer;
co_await MyCorotimer(timer);

放到一块

放置到一块就是一个合法的可直接编译运行的程序:

#include <QPlainTextEdit>
#include <QApplication>
#include <QDateTime>
#include <QTimer>

#include <chrono>
#include <coroutine>

struct MyCoroutine {
    struct promise_type {
        std::suspend_never initial_suspend() {return {}; }
        std::suspend_never final_suspend() noexcept {return {}; }

        void return_void() {}

        MyCoroutine get_return_object() { return {}; }

        void unhandled_exception() { std::terminate(); }
    };
};

class MyCoroTimer
{
    QTimer &m_timer;
    QMetaObject::Connection m_conn;
public:
    MyCoroTimer(QTimer &timer)
        : m_timer(timer) {}

    bool await_ready() const noexcept {
        return !m_timer.isActive();
    }

    void await_suspend(std::coroutine_handle<> coro) {
        if (m_timer.isActive()) {
            m_conn = QObject::connect(&m_timer, &QTimer::timeout, [this, coro]() {
                QObject::disconnect(m_conn);
                coro.resume();
            });
        } else {
            coro.resume();
        }
    }

    void await_resume() const {}
};

using namespace std::chrono_literals;

MyCoroutine updateText(QPlainTextEdit *textEdit)
{
    QTimer timer;
    timer.setInterval(1s);
    timer.start();

    for (int i = 0; i < 10; ++i) {
        co_await MyCoroTimer(timer);
        textEdit->appendPlainText(QString("[%1] Hello coroutine %2")
                                   .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
                                   .arg(i));
    }
}

int main(int argc, char** argv)
{
    QApplication a(argc, argv);

    QPlainTextEdit textEdit;
    textEdit.setWindowTitle("1+1=10");
    textEdit.resize(400, 300);
    textEdit.show();

    updateText(&textEdit);

    return a.exec();
}

不完美之处就是,使用时需要手动转成MyCoroTimer,而不能

co_await timer;

如何实现?二

要解决上面的问题,只需要给promise_type实现如下成员:

        MyCoroTimer await_transform(QTimer &timer) {
            return MyCoroTimer(timer);
        }

而后就可以直接

co_await timer;

完整代码

可编译运行的完整代码如下:

#include <QPlainTextEdit>
#include <QApplication>
#include <QDateTime>
#include <QTimer>

#include <chrono>
#include <coroutine>

class MyCoroTimer;
struct MyCoroutine {
    struct promise_type {
        std::suspend_never initial_suspend() {return {}; }
        std::suspend_never final_suspend() noexcept {return {}; }

        void return_void() {}

        MyCoroutine get_return_object() { return {}; }

        void unhandled_exception() { std::terminate(); }

        MyCoroTimer await_transform(QTimer &timer) {
            return MyCoroTimer(timer);
        }
    };
};

class MyCoroTimer
{
    QTimer &m_timer;
    QMetaObject::Connection m_conn;
public:
    MyCoroTimer(QTimer &timer)
        : m_timer(timer) {}

    bool await_ready() const noexcept {
        return !m_timer.isActive();
    }

    void await_suspend(std::coroutine_handle<> coro) {
        if (m_timer.isActive()) {
            m_conn = QObject::connect(&m_timer, &QTimer::timeout, [this, coro]() {
                QObject::disconnect(m_conn);
                coro.resume();
            });
        } else {
            coro.resume();
        }
    }

    void await_resume() const {}
};

using namespace std::chrono_literals;

MyCoroutine updateText(QPlainTextEdit *textEdit)
{
    QTimer timer;
    timer.setInterval(1s);
    timer.start();

    for (int i = 0; i < 10; ++i) {
        co_await timer;
        textEdit->appendPlainText(QString("[%1] Hello coroutine %2")
                                   .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
                                   .arg(i));
    }
}

int main(int argc, char** argv)
{
    QApplication a(argc, argv);

    QPlainTextEdit textEdit;
    textEdit.setWindowTitle("1+1=10");
    textEdit.resize(400, 300);
    textEdit.show();

    updateText(&textEdit);

    return a.exec();
}

如何实现?三

如果不用上面的promise_type方法,也可以用co_await重载的方式来解决

auto operator co_await(QTimer &timer) {
    return MyCoroTimer(timer);
}

完整代码

可编译运行的完整代码如下:

#include <QPlainTextEdit>
#include <QApplication>
#include <QDateTime>
#include <QTimer>

#include <chrono>
#include <coroutine>
#include <future>

class MyCoroTimer;
struct MyCoroutine {
    struct promise_type {
        std::suspend_never initial_suspend() {return {}; }
        std::suspend_never final_suspend() noexcept {return {}; }

        //std::suspend_never yield_value(int value) {return {}; }
        void return_void() {}

        MyCoroutine get_return_object() { return {}; }

        void unhandled_exception() { std::terminate(); }
    };
};

class MyCoroTimer
{
    QTimer &m_timer;
    QMetaObject::Connection m_conn;
public:
    MyCoroTimer(QTimer &timer)
        : m_timer(timer) {}

    bool await_ready() const noexcept {
        return !m_timer.isActive();
    }

    void await_suspend(std::coroutine_handle<> coro) {
        if (m_timer.isActive()) {
            m_conn = QObject::connect(&m_timer, &QTimer::timeout, [this, coro]() {
                QObject::disconnect(m_conn);
                coro.resume();
            });
        } else {
            coro.resume();
        }
    }

    void await_resume() const {}
};

auto operator co_await(QTimer &timer) {
    return MyCoroTimer(timer);
}

using namespace std::chrono_literals;

MyCoroutine updateText(QPlainTextEdit *textEdit)
{
    QTimer timer;
    timer.setInterval(1s);
    timer.start();

    for (int i = 0; i < 10; ++i) {
        co_await timer;
        textEdit->appendPlainText(QString("[%1] Hello coroutine %2")
                                   .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
                                   .arg(i));
    }
}

int main(int argc, char** argv)
{
    QApplication a(argc, argv);

    QPlainTextEdit textEdit;
    textEdit.setWindowTitle("1+1=10");
    textEdit.resize(400, 300);
    textEdit.show();

    updateText(&textEdit);

    return a.exec();
}

如何实现?四

前面一直在围绕QTimer打转: * Qt 的 QTimer 将底层的Event封装成了信号槽机制 * 我们的 MyCoroTimer 将信号槽机制封装成了 co_await所需要的Awaitable机制

既然这样的话,跳过QTimer,直接 定义一个MyTime,一步到位将Event封装成Awaitable不就好了??

如下所示,干净直接:

using namespace std::chrono_literals;

class MyTimer : QObject
{
    std::coroutine_handle<> m_coro;
public:
    MyTimer(QObject *parent=nullptr)
        : QObject(parent) {
        startTimer(1s);
    }

    bool await_ready() const noexcept {
        return false;
    }

    void await_suspend(std::coroutine_handle<> coro) {
        m_coro = coro;
    }

    void await_resume() const { }

protected:
    void timerEvent(QTimerEvent *event) override {
        m_coro.resume();
    }
};

使用同样方便:

MyCoroutine updateText(QPlainTextEdit *textEdit)
{
    MyTimer timer;

    for (int i = 0; i < 10; ++i) {
        co_await timer;
        textEdit->appendPlainText(QString("[%1] Hello coroutine %2")
                                   .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
                                   .arg(i));
    }
}

其他部分和前面例子一样,放到一块就可以编译运行了。此处不再重复

小结

从几个例子可以看到,对于C++ Qt 的协程,流程跑通没问题,不过每个类都这么写的话——实在太烦琐了。期待C++标准库和Qt能提供更好的支持。

Qt的信号槽本身是为了解决回调函数耦合问题,C++协程很大程度也是解决回调函数的回调地狱问题。Qt的后续版本如何更好地支持协程,和或者与信号槽如何进行融合或分工,估计也会比较有意思。

第三方的qcoro为Qt5和Qt6提供了协程支持,值得进一步了解。但是后续Qt原生支持的话肯定会更简洁或高效。

参考

  • https://en.cppreference.com/w/cpp/language/coroutines
  • https://qcoro.dvratil.cz/reference/core/qtimer/

Comments