Qt 이야기: 쓰레드를 만드는 세 가지 방법

멀티 코어가 기본인 요즘에 멀티 쓰레드는 기본이나 다름없다. Qt 에서도 멀티 쓰레드를 훌륭히 지원하고 있는데, 이번 글에서는 Qt 에서 멀티 쓰레드를 사용하는 방법에 대해서 알아보도록 하자.

Qt 는 쓰레드를 생성하기 위해 크게 두 가지 방법을 제공한다. 하나는 QThread 클래스를 이용하는 것이고, 다른 하나는 QtConcurrent 클래스를 이용하는 것이다. 이 중에서 QThread 클래스의 경우 QThread 를 상속하는 경우와 그렇지 않은 경우로 나뉜다.

1. QThread 이용하기

1.1 QThread 상속하기

QThread 를 상속하는 것은 가장 직관적인 방법이다. QThread 를 상속하고 QThread::run() 을 재정의한 후에, QThread::start() 를 실행시키면 된다. 다음은 간단한 예이다.

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
#include <QCoreApplication>

#include <QThread>
#include <QDebug>

class MyThread : public QThread
{
private:
    void run() Q_DECL_OVERRIDE
    {
        qDebug() << __func__ << "in MyThread" << currentThread();
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << __func__ << "in a main thread" << QThread::currentThread();

    MyThread myThread;
    QObject::connect(&myThread, SIGNAL(finished()), &a, SLOT(quit()));

    myThread.start();

    return a.exec();
}



주의할 점은 쓰레드를 실행하는 것은 QThread::run() 이 아니라, QThread::start() 이다. 실제로 위의 코드를 보면 run() 은 외부에서 실행되지 않도록 private 으로 선언되어 있다.

1.2. QThread 와 QObject::moveToThread() 사용하기

QThread 는 자체의 이벤트 루프를 가지고 있다. 따라서 QThread::start() 를 호출하면 자동으로 QThread::run() 이 실행되어 이벤트 루프에 진입하게 된다. 따라서 이를 이용하면 QThread 를 상속하지 않고, 쓰레드를 생성할 수 있다. 다음은 그 예이다.

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
#include <QCoreApplication>

#include <QThread>
#include <QDebug>

class MyThreadWorker : public QObject
{
    Q_OBJECT

public:
    MyThreadWorker(QObject *parent = 0) : QObject(parent) {}

signals:
    void finished();

public slots:
    void body()
    {
        qDebug() << __func__ << QThread::currentThread();

        emit finished();
    }
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << __func__ << QThread::currentThread();

    QThread myThread;
    MyThreadWorker myWorker;
    myWorker.moveToThread(&myThread);

    QObject::connect(&myThread, SIGNAL(started()), &myWorker, SLOT(body()));
    QObject::connect(&myWorker, SIGNAL(finished()), &myThread, SLOT(quit()));
    QObject::connect(&myThread, SIGNAL(finished()), &a, SLOT(quit()));

    myThread.start();

    return a.exec();
}


35 번째 줄: QObject::moveToThread() 는 생성된 객체의 소속 쓰레드를 바꿔주는 함수이다. myWorker 는 메인 쓰레드에서 생성되었기 때문에, 생성 직후에는 메인 쓰레드에 속한다. 하지만, QObject::moveToThread() 를 통해 소속이 myThread 로 바뀌었다.

만약 35 번째줄을 주석처리한다면, myWorker::body() 에서 출력되는 쓰레드 ID 와 main() 에서 출력되는 쓰레드 ID 가 동일하게 된다.

37 번째 줄: myThread 가 시작되었을 때, myWorker::body() 를 실행하도록 하는 것이다.

38 번째 줄: myWorker::body() 가 끝났을 때, 쓰레드의 이벤트 루프(QThread::run() 에서 실행됨)가 끝나도록 한다.

39 번째줄은 쓰레드가 끝났을 때 메인 이벤트 루프가 끝나도록 한다

1.3 정리

QThread 를 이용해서 쓰레드를 만드는 두 가지 방법을 알아보았다. 하지만 이 두가지 방법에서 추천하는 방법은 두 번째 방법이다. 왜냐하면, 첫번째 방법의 경우 쓰레드 내부에서 이벤트를 사용하게 되면, 그 이벤트는 새로운 쓰레드에서 처리되는 것이 아니라 쓰레드를 생성한 부모 쓰레드에서 처리된다. 따라서 만일 부모 쓰레드가 막힌다면(blocked) 새로운 쓰레드의 이벤트 처리도 멈추게 된다. 이 때문에 자칫 잘못하면 프로그램 전체가 멈추는 사태가 벌어질 수도 있다.

그리고 새로운 쓰레드는 부모 쓰레드에서 만들어지므로, 쓰레드 클래스에 속해 있는 모든 멤버 변수와 멤버 함수는 부모 쓰레드에 속하게 된다. 따라서 이벤트로 호출되는 멤버 함수들은 부모 쓰레드의 맥락에서 실행된다. 반면 QThread::run() 에서 실행되는 함수는 새로운 쓰레드의 맥락에서 실행되므로, 쓰레드 클래스의 변수를 조작할 때 동기화에 주의해야 한다.

그럼에도 불구하고 QThread 를 상속하는 방법을 쓰고 싶다면, 이벤트 루프를 사용하지 않을 경우에만 쓰기 바란다.

보다 자세한 것은 다음 블로그를 보기 바란다.



2. QtConcurrent 사용하기

Qt 는 QThread 같은 전통적인 쓰레드 방식 말고도 쓰레드 풀이라는 임시 쓰레드를 이용해서 쓰레드를 실행하는 방법을 제공한다. QtConcurrent 클래스가 그러한 기능을 수행한다. 다음 예를 보자.

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
#include <QCoreApplication>

#include <QThread>
#include <QtConcurrent>

static void worker(const QString &msg)
{
    qDebug() << __func__ << QThread::currentThread() << msg;
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << __func__ << QThread::currentThread();

    QFutureWatcher<void> watcher;
    QObject::connect(&watcher, SIGNAL(finished()), &a, SLOT(quit()));

    QFuture<void> result = QtConcurrent::run(worker,
                                             QObject::tr("Hello, world"));
    watcher.setFuture(result);

    return a.exec();
}



QThread 를 사용할 때와는 다르게 쓰레드 코드가 상당히 간소해졌다. 일단 클래스가 사라졌으니까. 대신에 추가작업이 있는데, QtConcurrent 를 쓰기 위해서는 .pro 파일에

QT += current 

를 추가해주어야 한다.

QFutureQtConcurrent::run() 으로 실행되는 함수의 결과를 제어하는 클래스이다. 결과를 기다리기도 하고(QFuture::waitForFinished()), 결과 값을 확인하기도(QFuture::result()) 한다. 하지만 QFuture 는 이벤트를 지원하지 않는다. 따라서 기다리거나 결과를 확인할 때 해당 쓰레드는 멈춘다(blocked). 이를 피하려면 QFutureWatcher 를 쓰면 된다.

보다 자세한 것은 Qt 도움말이나 Assistant 를 보도록 하자.

3. 마무리하면서...

간단하게나마 Qt 에서 쓰레드를 생성하는 방법에 대해 알아보았다. 크게 QThread 와 QtConcurrent 를 이용하는 것이 있고, QThread 는 다시 QThread 를 상속하는 것과 그렇지 않는 것으로 나뉜다. QThread 를 상속하는 것은 가장 직관적이고 간단한 방법이기는 하지만, 이벤트 처리에 문제가 생길 수 있다. 게다가 QtConcurrent 를 이용하면 훨씬 간단하게 쓰레드를 생성할 수 있기 때문에, 되도록이면 QThread 를 상속하는 것은 피하도록 하자. 이벤트 처리가 필요하다면 QThread + QObject::moveToThread() 조합을 쓰자.

시대가 시대인 만큼 쓰레드에 익숙해지는 것은 이제는 기본 소양이다. 그리고 Qt 는 멀티 쓰레드 기능을 강력하게 지원하고 있다. 이제 남는 것은, 어느 코어 하나 놀게 하지 않고, 모든 멀티 코어를 활용하는 프로그램을 만드는 것 뿐이다.



댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷