Qt에서 멀티 쓰레딩을 사용하기 위해서 QThread를 상속받아서 처리하는 예제를 소개한 적이 있습니다.
QtConcurrent를 사용하면 간단하게 비동기 멀티 쓰레딩을 할 수 있습니다. 이를 Qt에서 제공하는 Example을 같이 살펴보며 필요한 부분에 대해 같이 공부해보려 합니다.
Example은 Qt Creator > Examples에서 concurrent로 검색하여 첫 번째 예제인 Image Scaling을 사용하려 합니다.
전체 함수를 다 보는 것보다 필요한 부분만 순서대로 보겠습니다. 주석에 나와있는 번호를 따라가면 UI 생성 및 Connect 관계에 대해서는 쉽게 파악이 가능합니다.
그리고 중간에 비동기 처리를 확인하기 위해 Debug Message를 출력하는 함수를 추가로 사용하였습니다.
void DebugMsg(const QString& sMsg)
{
qDebug() << QTime::currentTime().toString("[mm:ss:zzz]") + sMsg;
}
[3] 부분인 DownloadDialog를 실행 후 확인을 눌렀을 때 진행되는 부분부터 보겠습니다. DownloadDialog에서는 이미지의 URL들을 관리하고 확인을 눌렀을 때 getUrls를 통해 Url 정보를 가져옵니다.
//! [3]
void Images::process()
{
// Clean previous state
replies.clear();
addUrlsButton->setEnabled(false);
if (downloadDialog->exec() == QDialog::Accepted) {
const auto urls = downloadDialog->getUrls();
if (urls.empty())
return;
cancelButton->setEnabled(true);
initLayout(urls.size());
downloadFuture = download(urls);
DebugMsg("Downloading");
statusBar->showMessage(tr("Downloading..."));
//! [3]
//! [4]
downloadFuture
.then([this](auto) {
cancelButton->setEnabled(false);
updateStatus(tr("Scaling..."));
DebugMsg("Scaling");
//! [16]
scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
downloadFuture.results()));
//! [16]
})
//! [4]
//! [5]
.onCanceled([this] {
updateStatus(tr("Download has been canceled."));
})
.onFailed([this](QNetworkReply::NetworkError error) {
updateStatus(tr("Download finished with error: %1").arg(error));
// Abort all pending requests
abortDownload();
})
.onFailed([this](const std::exception &ex) {
updateStatus(tr(ex.what()));
})
//! [5]
.then([this]() {
cancelButton->setEnabled(false);
addUrlsButton->setEnabled(true);
});
}
}
가져온 Url을 Donwload 함수로 넘기게 됩니다.
//! [8]
QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{
//! [8]
//! [9]
DebugMsg("DonwloadStart");
QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
promise->start();
//! [9]
//! [10]
for (const auto &url : urls) {
DebugMsg("Donwload " + url.toString());
QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
replies.push_back(reply);
//! [10]
//! [11]
QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
if (promise->isCanceled()) {
if (!promise->future().isFinished())
promise->finish();
return;
}
if (reply->error() != QNetworkReply::NoError) {
if (!promise->future().isFinished())
throw reply->error();
}
//! [12]
promise->addResult(reply->readAll());
DebugMsg("Get Data");
// Report finished on the last download
if (promise->future().resultCount() == urls.size()) {
DebugMsg("Finish promis");
promise->finish();
}
//! [12]
}).onFailed([promise] (QNetworkReply::NetworkError error) {
promise->setException(std::make_exception_ptr(error));
promise->finish();
}).onFailed([promise] {
const auto ex = std::make_exception_ptr(
std::runtime_error("Unknown error occurred while downloading."));
promise->setException(ex);
promise->finish();
});
}
//! [11]
//! [13]
DebugMsg("Return");
return promise->future();
}
//! [13]
여기서 QPromise와 QFuture을 사용하게 됩니다. C++에서 Promise와 Furture은 같이 사용하게 되는데 Furture(미래)에 데이터를 돌려주겠다는 Promise(약속)이라고 생각하면 됩니다. QPromise의 비동기 처리를 QFurture에 전달하는 방식입니다. QFurtuer의 Connect를 통해 Signal 처리를 하게 되고 혹은 QFurture::waitforFinish를 통해 비동기 처리가 완료될 때까지 기다릴 수도 있습니다.
for문으로 전달받은 Url들을 요청하게 되는데 이에 대한 결과는 QNetworkReply::Finish Signal을 QtFurture::connect로 묶어 처리가 됩니다. 아래 로그를 확인하면 Download 후 바로 Promise가 가지고 있는 QFurture을 Return하게 됩니다. 그 후 요청한 Url에 대한 결과가 도착하고 Url 개수만큼의 완료가 진행 됐다면 QPromise는 종료합니다.
"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"
그리고 다시 Process 함수로 돌아와서 QPromise가 finish 됨에 따라 QFurtuer의 then의 lambda 함수가 실행됩니다.
//! [3]
void Images::process()
{
// Clean previous state
replies.clear();
addUrlsButton->setEnabled(false);
if (downloadDialog->exec() == QDialog::Accepted) {
const auto urls = downloadDialog->getUrls();
if (urls.empty())
return;
cancelButton->setEnabled(true);
initLayout(urls.size());
downloadFuture = download(urls);
DebugMsg("Downloading");
statusBar->showMessage(tr("Downloading..."));
//! [3]
//! [4]
downloadFuture
.then([this](auto) {
cancelButton->setEnabled(false);
updateStatus(tr("Scaling..."));
DebugMsg("Scaling");
//! [16]
scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
downloadFuture.results()));
//! [16]
})
//! [4]
//! [5]
.onCanceled([this] {
updateStatus(tr("Download has been canceled."));
})
.onFailed([this](QNetworkReply::NetworkError error) {
updateStatus(tr("Download finished with error: %1").arg(error));
// Abort all pending requests
abortDownload();
})
.onFailed([this](const std::exception &ex) {
updateStatus(tr(ex.what()));
})
//! [5]
.then([this]() {
cancelButton->setEnabled(false);
addUrlsButton->setEnabled(true);
});
}
}
여기서 QFurtherWatcher라는 객체에 QtConcurrent::run의 Return을 넘겨주게 됩니다. QFurtherWatcher는 QFurture에 대한 상태를 체크하고 QFurture가 Singal 상태가 됐을 때 실행할 Slot을 connect 시켜주었습니다.
Images::Images(QWidget *parent) : QWidget(parent), downloadDialog(new DownloadDialog(this))
{
resize(800, 600);
...
//! [6]
connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished, this, &Images::scaleFinished);
//! [6]
}
//! [14]
Images::OptionalImages Images::scaled(const QList<QByteArray> &data)
{
DebugMsg("Scale Image");
QList<QImage> scaled;
for (const auto &imgData : data) {
QImage image;
image.loadFromData(imgData);
if (image.isNull())
return std::nullopt;
scaled.push_back(image.scaled(100, 100, Qt::KeepAspectRatio));
}
return scaled;
}
//! [14]
//! [15]
void Images::scaleFinished()
{
DebugMsg("Sacle Finished");
const OptionalImages result = scalingWatcher.result();
if (result.has_value()) {
const auto scaled = result.value();
showImages(scaled);
updateStatus(tr("Finished"));
} else {
updateStatus(tr("Failed to extract image data."));
}
addUrlsButton->setEnabled(true);
}
최종적으로 실행된 로그를 보면 아래와 같습니다.
"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"
"[23:11:724]Scaling"
"[23:11:724]Scale Image"
"[23:11:752]Sacle Finished"
정리해보자면 QThread를 사용하지 않으면서 비동기 처리를 위해선 QFurture, QPromise 등을 사용하면서 종료를 대기 혹은 신호를 받거나 혹은 QFurtureWatcher에 QFurture를 등록 후 사용도 가능합니다.
QFurture를 간단히 사용하기 위해서는 Qt::Concorrent::run을 통해 함수 포인터와 인자를 넘겨줌으로써 그 처리의 QFurture를 반환받아 위처럼 사용도 가능합니다.
'Programming > Qt' 카테고리의 다른 글
[Qt] QString::toInt 주의사항 (0) | 2024.06.24 |
---|---|
[Qt] Drag&Drop으로 QTreeWidget 제어하기 (1) | 2024.06.14 |
[Qt] Windows cmd 명령어 실행 및 결과 얻어오기 (0) | 2023.12.14 |
[Qt] 레이아웃에 있는 위젯 교체하기(replaceWidget) (2) | 2023.12.06 |
[Qt] TreeWidget 사용하기 (3) | 2023.12.01 |