简单学习了 C++ 协程,但是感觉心里空的很。不妨用Qt来试试
目标
使用QTimer 和 C++协程实现如下效果:
期望的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | 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类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | 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);
|
放到一块
放置到一块就是一个合法的可直接编译运行的程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 | #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,而不能
如何实现?二
要解决上面的问题,只需要给promise_type
实现如下成员:
| MyCoroTimer await_transform(QTimer &timer) {
return MyCoroTimer(timer);
}
|
而后就可以直接
完整代码
可编译运行的完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 | #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);
}
|
完整代码
可编译运行的完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 | #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不就好了??
如下所示,干净直接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | 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/