728x90

Qt에서 멀티 쓰레딩을 사용하기 위해서 QThread를 상속받아서 처리하는 예제를 소개한 적이 있습니다.

 

Qt QThread 사용하기(화면 실시간 갱신하기)

센서값을 수집해서 실시간으로 실행창에서 최신화하고 갱신하는 방법에 대해 소개해 드리겠습니다. 먼저 QThread를 상속받는 클래스를 하나 만들어 줍니다. 생성된 qTh.h 파일에 소스코드를 아래

1d1cblog.tistory.com

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);
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("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();
}

여기서 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);
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("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]
}

//! [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를 반환받아 위처럼 사용도 가능합니다.

.

728x90

+ Recent posts