한국어
Qt
 

Qt 6 Qt 6의 비동기 API

makersweb 2020.10.19 22:13 조회 수 : 1983

이 글에서는 Qt 6에 도입 된 상위 수준의 비동기 API와 변경 사항에 대해 설명한다.

 

Qt의 상위 수준 병렬 처리 API

Qt Concurrent는 저수준 동기화 (뮤텍스 및 잠금과 같은 기본 요소)의 필요성을 제거하고 여러 스레드를 수동으로 관리함으로써 다중 스레드 프로그래밍을 더 쉽게 만든다.

반복 가능한 컨테이너의 병렬 처리를 위해 맵, 필터 및 축소 알고리즘 (함수 프로그래밍에서 더 잘 알려져 있음)을 제공한다. 또한 QFuture, QFutureWatcher 및 QFutureSynchronizer와 같은 클래스가있어 비동기 계산 결과에 액세스하고 모니터링 할 수 있다.

대체적으로 매우 유용하지만 Qt Concurrent 외부에서 QFuture를 사용할 수 없고, 더 간단하고 깔끔한 코드를 위한 다중 계산 결합에 대한 지원 부족, Qt Concurrent API의 유연성 부족 등과 같은 몇 가지 단점이 있다.

Qt 6의 경우 지난 몇 년 동안 받은 피드백을 고려하여 Qt의 멀티 스레드 프로그래밍을보다 즐겁고 재미있게 만들려고 노력했다.

 

QFuture에 연속 연결

멀티 스레드 프로그래밍의 일반적인 시나리오는 비동기 연산을 수행하는 것이다. 연산은 다시 다른 비동기 명령을 호출하고 다른 비동기 연산에 의존하는 데이터를 전달해야 한다.

각 단계에는 이전 단계의 결과가 필요하므로 이전 단계가 완료 될 때까지 (블로킹 또는 폴링하여) 기다렸다가 결과를 사용하거나 "콜백"스타일로 코드를 구성해야 한다.

이러한 옵션 중 어느 것도 이상적이지 않다. 대기중인 리소스를 낭비하거나 유지관리할 수 없는 복잡한 코드로 끝난다. 새로운 단계 또는 로직 (오류 처리 등)을 추가하면 복잡성이 더욱 증가할 것이다.

문제를 더 잘 이해하기 위해 다음의 예를 들어보자. 웹에서 큰 이미지를 다운로드하고 그에 대해 무거운 처리를 하고 결과 이미지를 애플리케이션에 표시하려고 한다고 가정 해 보겠다. 즉, 다음 단계를 수행해야 한다.

  • 네트워크 요청을 하고 모든 데이터가 수신 될 때까지 대기.
  • 원시 데이터에서 이미지를 생성.
  • 이미지 처리.
  • 이미지를 장치로 출력.

 

순차적으로 호출해야 하는 각 단계에 대해 다음 메서드가 있다.

QByteArray download(const QUrl &url);
QImage createImage(const QByteArray &data);
QImage processImage(const QImage &image);
void show(const QImage &image);

QtConcurrent를 사용하여 이러한 작업을 비동기 적으로 수행하고 QFutureWatcher 를 사용하여 진행 상황을 모니터링 할 수 있다.

void loadImage(const QUrl &url) {
    QFuture data = QtConcurrent::run(download, url);
    QFutureWatcher dataWatcher;
    dataWatcher.setFuture(data);
    
    connect(&dataWatcher, &QFutureWatcher ::finished, this, [=] {
        // handle possible errors
        // ...
        QImage image = createImage(data);
        // Process the image
        // ...
        QFuture processedImage = QtConcurrent::run(processImage, image);
        QFutureWatcher<QImage> imageWatcher;
        imageWatcher.setFuture(processedImage);

        connect(&imageWatcher, &QFutureWatcher::finished, this, [=] {
            // handle possible errors
            // ...
            show(processedImage);
        });
    });
}

좋아보이는가? 응용 프로그램 로직은 프로그램 구성 요소를 함께 묶는 데 필요한 표준 코드와 혼합된다. 그리고 더 많은 단계를 추가할수록 더 악화된다는 것을 알고 있다.

QFuture는 QFuture::then() 메서드를 통해 연속 연결 지원을 지원하여 이 문제를 해결하는 데 도움이 된다.

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show);

확실히 훨씬 좋아 보인다! 그러나 에러처리가 누락된 것을 알 수있다. 다음과 같이 할 수 있다.

auto future = QtConcurrent::run(download, url)
            .then([](QByteArray data) {
                // handle possible errors from the previous step
                // ...
                return createImage(data);
            })    
            .then(...)    
            ...

이 코드는 작동하지만 에러 처리 코드는 여전히 프로그램 로직과 혼합되어 있다. 또한 단계 중 하나가 실패하면 전체 체인을 실행할 이유가 없을 것이다. 이 문제는 QFuture::onFailed() 메서드로 해결할 수 있으며, 이를 통해 각 에러 유형에 대해 특정 에러 처리기를 연결할 수 있다.

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show)
            .onFailed([](QNetworkReply::NetworkError) {
                // handle network errors
            })
            .onFailed([](ImageProcessingError) {
                // handle image processing errors
            })
            .onFailed([] {
                // handle any other error
            });

.onFailed()를 사용하려면 예외를 활성화해야한다. 예외로 인해 단계가 실패하면 체인이 끊어지고 throw 된 예외 유형과 일치하는 에러 처리기가 호출된다.

.then() 및 onFailed()와 마찬가지로 Future 가 취소될 것을 대비하여 핸들러를 연결하는 .onCanceled()도 있다.

 

시그널에서 QFuture 생성

시그널 또한 미래에 언젠가 사용할 수 있는 무언가를 나타내기 때문에 future와 같이 작업하고 연속, 실패 처리기 등을 첨부 할 수 있는 것이 자연스러워 보인다. void mySignal(int) 시그널이 있는 QObject 기반 클래스 MyObject가 주어지면 이 시그널을 다음과 같은 방법으로 future로 사용할 수 있다.

QFuture intFuture = QtFuture::connect(&object, &MyObject::mySignal);

이제 결과 future에 연속, 실패 또는 취소 처리기를 연결할 수 있다.

결과 미래의 유형은 시그널의 인수 유형과 일치한다. 인수가 없으면 QFuture<void>가 반환되며 인수가 여러 개인 경우 결과는 std::tuple에 저장된다.

이미지 처리 예제의 첫 번째 (즉, 다운로드) 단계로 돌아가서 이것이 실제로 어떻게 유용 할 수 있는지 살펴 보자. 이를 구현하는 방법에는 여러 가지가 있다. QNetworkAccessManager 를 사용하여 네트워크 요청을 보내고 데이터를 가져온다.

QNetworkAccessManager manager;    
...

QByteArray download(const QUrl &url) {        
    QNetworkReply *reply = manager.get(QNetworkRequest(url));
    QObject::connect(reply, &QNetworkReply::finished, [reply] {...});
    
    // wait until we've received all data
    // ...    
    return data;        
}

그러나 잠금을 기다리는 것은 좋지 않다. 이를 제거하고 대신 "QNetworkAccessManager가 데이터를 수신하면 이미지를 생성 한 다음 처리하고 표시"라고 표현하면 더 좋을 것이다. 네트워크 액세스 관리자의 finished() 시그널을 QFuture에 연결하여 이를 수행 할 수 있다.

QNetworkReply *reply = manager.get(QNetworkRequest(url));

auto future = QtFuture::connect(reply, &QNetworkReply::finished)
        .then([reply] {
            return reply->readAll();
        })
        .then(QtFuture::Launch::Async, createImage)
        .then(processImage)
        .then(show)        
        ...

이제 QtConcurrent::run()을 사용하여 데이터를 비동기적으로 다운로드하고 새 스레드에 반환하는 대신, 단순히 QNetworkAccessManager::finished() 시그널에 연결하여 연산 체인을 시작한다는 것을 알 수 있다. 또한 다음 행의 추가 매개 변수에 주목하자.

        .then(QtFuture::Launch::Async, createImage)

기본적으로 .then()에 의해 첨부된 연속성은 부모가 실행해 온 동일한 스레드(우리의 경우 메인 스레드)에서 호출된다. QtConcurrent::run()을 사용하여 체인을 비동기 적으로 시작하지 않았으므로 추가 매개 변수(QtFuture::Launch::Async)를 전달하여 별도의 스레드에서 연속 체인을 시작하고 UI 차단을 방지해야 한다.

번호 제목 글쓴이 날짜 조회 수
공지 Qt프로그래밍(QtQuick) Beginner를 위한 글 읽는 순서 운영자 2019.01.05 91648
140 싱글 샷(Single-Shot) 시그널/슬롯 연결 makersweb 2021.05.12 1499
139 응용프로그램 자동실행 설정 (on Windows) makersweb 2021.05.08 1304
138 Qt 6 에서 프로퍼티 바인딩 makersweb 2021.04.03 1480
137 QML과 JavaScript 의 숫자 관련 내장된 함수 makersweb 2021.03.28 2281
136 Qt 5 코드를 Qt 6로 포팅하기 위해 도움이 되는 Clazy Framework file makersweb 2021.03.01 1725
135 C++로 작성한 클래스를 QML에서 생성 file makersweb 2021.02.10 6356
134 Qt MQTT의 pus/sub 튜토리얼 file makersweb 2021.02.06 2753
133 Qt 를 사용하거나 기반으로 하는 응용프로그램 file makersweb 2021.01.30 4929
132 Loader를 사용하여 동적으로 QML 로드 makersweb 2021.01.19 2650
131 QThread 및 QMutex 예제 makersweb 2021.01.12 2227
130 그래픽 소프트웨어에서 디자인 내보내기 (Exporting Designs from Graphics Software) j2doll 2020.12.25 1206
129 Qt5Compat 라이브러리를 사용하여 Qt5에서 Qt6로 포팅 [2] makersweb 2020.12.05 1440
128 Qt Quick Controls 2에 네이티브 데스크탑 스타일 추가 file makersweb 2020.11.23 1257
127 QML 바인딩 끊김 진단 makersweb 2020.11.08 1664
126 QML과 코루틴(Coroutines) makersweb 2020.11.03 1328
» Qt 6의 비동기 API makersweb 2020.10.19 1983
124 QRandomGenerator 클래스를 사용하여 난수(random values) 생성 makersweb 2020.10.17 2411
123 Qt 6에서 QList 변경사항 makersweb 2020.10.08 1760
122 Qt 6.0의 개발 호스트 및 대상 플랫폼 makersweb 2020.09.16 1765
121 main함수 명령줄 옵션 해석 makersweb 2020.09.01 3107