コルーチンと Qt で遊んでみた

この記事は The Qt BlogPlaying with coroutines and Qt を翻訳したものです。
執筆: Jesús Fernández, 2018年05月29日

みなさん、こんにちは!

最近 C++ でのコルーチンの状況が気になって、いくつかの実装を調べていました。そして これ で実験することにしました。

これはシンプルで簡単に使え、Linux と Windows で動作します。

当面の目標は、シグナルが来るのを待ったり、QCoreApplication::processEvents を呼んだり、QEventLoop をスタック上に作るようなことをせずとも非同期で動くコードを探るということです。

最初のアプローチは、カスタムのイベントディスパッチャーの中の processEvent 関数をコルーチンに変換し、yield を使うことでした。いくつか失敗をして、この方法はやめることにしました。

次の取り組みは、スロットをコルーチンにすることでした。

QTimer::singleShot(0, std::bind(&coroutine::resume, coroutine::create([]() { ... });

このラムダ関数の中で、CPU は yield まで処理を実行し、アプリケーションのイベントループに飛びます。

実際のコードはこんな感じです。

#include "coroutine.h"
#include <QtCore>
#include <QtWidgets>

int main(int argc, char **argv)
{
QApplication app(argc, argv);
QPushButton fibonacciButton("0: 0");
fibonacciButton.show();
QObject::connect(&fibonacciButton, &QPushButton::pressed,
std::bind(&coroutine::resume, coroutine::create([&]() {
qulonglong f0 = 1, f1 = 0, n = 1;
fibonacciButton.setText(QString("1: 1"));
coroutine::yield();
fibonacciButton.setText(QString("2: 1"));
coroutine::yield();
forever {
auto next = f1 + f0;
f0 = f1;
f1 = next;
fibonacciButton.setText(QString("%0: %1").arg(n++).arg(f0 + f1));
coroutine::yield();
}
})));
return app.exec();
}

ボタンにラムダ関数が接続されていて、フィボナッチ数列の計算をしています。次の計算が終わった後で、yield を呼び、この関数からイベントループに処理を移しています。再度ボタンが押された際には yield の次の行に戻ってきます。

このサンプルは、コードの実行を再開するためにユーザーがボタンを再度押す必要があるために正常に動作しています。

ところが、処理の再開を自動的にしたい場合もあります。この場合は、処理を yield するのに加え再開を予約する必要があります。

void qYield()
{
const auto routine = coroutine::current();
QTimer::singleShot(0, std::bind(&coroutine::resume, routine));
coroutine::yield();
}

最初の行でコルーチンの ID を取得し、次の行では再開の予約をしています。yield で CPU は前のフレームに戻り、結果的にメインループで、キューに予約されたコードを無条件に再開します。

次のステップは、ある条件で再開をするというものです。Qt にはシグナルという何かが起こったことを知らせる仕組みがあるので、処理を譲渡する最適な方法は以下のようになります。

template <typename Func>
void qAwait(const typename QtPrivate::FunctionPointer::Object *sender, Func signal)
{
const auto routine = coroutine::current();
const auto connection = QObject::connect(sender, signal,
std::bind(&coroutine::resume, routine));
coroutine::yield();
QObject::disconnect(connection);
}

再開をキューに入れる代わりに、一時的に再開用の接続をスロットに対して行っています。

これを利用した例は以下のとおりです。

#include "coroutine.h"
#include <QtCore>
#include <QtWidgets>
#include <QtNetwork>

int main(int argc, char **argv)
{
QApplication app(argc, argv);
QPlainTextEdit textEdit;
textEdit.show();
QTimer::singleShot(0, std::bind(&coroutine::resume, coroutine::create([&]() {
QUrl url("http://download.qt.io/online/qt5/linux/x64/online_repository/Updates.xml");
QNetworkRequest request(url);
QNetworkAccessManager manager;
auto reply = manager.get(request);
qAwait(reply, &QNetworkReply::finished);
textEdit.setPlainText(reply->readAll());
reply->deleteLater();
})));
return app.exec();
}

QTextEdit がインターネット上のファイルを取得します。QNetworkReply が完了したタイミングでデータが QTextEdit に記載されます。

もうひとつのサンプル。

#include "coroutine.h"
#include <QtCore>
#include <QtWidgets>
#include <QtNetwork>

int main(int argc, char **argv)
{
QApplication app(argc, argv);
QPlainTextEdit edit;
edit.show();

QTimer::singleShot(0, std::bind(&coroutine::resume, coroutine::create([&]() {
auto previousText = edit.toPlainText();
forever {
if (edit.toPlainText() == QStringLiteral("quit")) {
qApp->quit();
} else if (previousText != edit.toPlainText()) {
qDebug() << previousText << "->" << edit.toPlainText();
previousText = edit.toPlainText();
}
qAwait(&edit, &QPlainTextEdit::textChanged);
qDebug() << "QPlainTextEdit::textChanged";
}
})));
return app.exec();
}

このアプリケーションはユーザーが文字列を変更した際にその文字列を出力し、’quit’ と書かれたら終了します。


Blog Topics:

Comments