Qt wiki will be updated on October 12th 2023 starting at 11:30 AM (EEST) and the maintenance will last around 2-3 hours. During the maintenance the site will be unavailable.
Threads Events QObjects/ko
[toc align_right="yes" depth="2"]
STILL WORKING ON 원본 영문버젼의 내용이 업데이트가 되었는지 확인해 이 문서도 함께 갱신되어야 합니다(번역기준으로 원본의 문서리비젼: Last edit: March, 13, 2012)
쓰레드, 이벤트, 그리고 QObject객체(Threads, Events and QObjects)
경고 : 베타버젼입니다
이 문서는 거의 다 작업되었느나, 아직까지는 좀 더 다듬어야 합니다. 이 문서에 대한 논의는 "여기":http://developer.qt.nokia.com/forums/viewthread/2423/ 에서 진행되고 있습니다.
서론
어! 잘못하고 있는데요!. — Bradley T. Hughes
"#qt IRC channel":irc://irc.freenode.net/#qt 에서 가장 많이 논의되는 주제중 하나가 쓰레딩과 관련한 것이다. 많은 사람들이 채널에 들어와 자신들의 문제를 "다른 쓰레드에서 행되는 코드"를 가지고 해결하려하고 이것에 대해 질문한다.
이 사람들의 코드를 들여다 보고 빨리 알아 챌 수 있는 문제들중 열에 아홉은 우선 쓰레드를 쓰고보자는 식으로 접근하고 나서 병렬 프로그래밍의 끝없는 해악에 빠져버린다는 것이다.
쓰레드의 생성과 실행이 Qt 에서 쉬워진 고로, 프로그래밍 스타일(특히, 비동기 네트워크 프로그래밍이나, Qt 의 시그널 슬롯 아키텍쳐)에 대한 지식 없이 혹은 다른 툴킷이나 언어를 사용할때 생긴 버릇 등과 버무려져 사람들은 종종 잘못된 길로 들어서게 되는 경우가 많다. 더군다나, Qt 의 쓰레드관련 지원은 양날의 검과도 같다. 멀티쓰레딩의 구현이 아주 쉽게 되었지만, 꼭 알고 사용해야 하는 사항들이 상당수 추가되어 있기 때문이다(특히 QObject 객체들과 상호작용을 하게 되는 경우).
이 문서는 쓰레드를 어떻게 쓰는지, 적합한 locking방법은 무엇인지, 병렬성(parallelism)의 활용은 어떻게 하는 것인지, 확장성 있는 프로그램을 작성하는 방법은 무엇인지를 말하기 위한 것이 아니다. 이 주제들에 대한 많은 책/문서들이 이미 많다(이를 테면 이 "page":http://doc.qt.nokia.com/latest/threads.html 와 같은 문서들). 대신, 이 작은 문서를 통해, 사용자들로 하여금 Qt 4 에서의 쓰레딩을 소개하여 가장 일반적인 문제점들을 짚고, 견고한 동시에 나은 구조를 가지는 코드를 개발할 수 있도록 돕고자 한다.
필요사항
이렇게 생각해봐요. 쓰레드는 소금과 같지만, 파스타 같지는 않죠. 소금은 당신도 좋아하고, 나도 좋아하고, 우리 모두 좋아하지만, 사실 파스타를 더 많이 먹죠.
— 래리 맥보이(Larry McVoy)
쓰레드 프로그래밍에 대한 범용 소개문서로, 다음과 같은 사전 지식을 필요로 한다.
- C++ 기초 (사실, C++ 뿐만 아니라 많은 다른 언어들과 상관이 있다);
- Qt 기초: QObjects, signals 및 slots, 이벤트 핸들링);
- 쓰레드가 무엇이며, 쓰레드,프로세스 및 운영체제간의 상호관계;
- 적어도 하나 이상의 주요한 운영체제에서, 쓰레드를 어떻게 시작하고 멈추는지 그리고 쓰레드가 끝날 때 까지 기다리는 방법;
- 뮤텍스(mutex), 세마포어(semaphore) 및 대기조건(wait condition)을 사용하여 쓰레드 안전하고 재진입이 가능한 함수, 자료구조, 클래스를 만드는 법;
본 문서에서는 Qt 의 "명명규칙(naming convention)":http://doc.qt.nokia.com/latest/threads-reentrancy.html 인 다음의 사항을 따를것이다.
- 재진입 한번에 최대 하나의 스레드에서 동일 인스턴스에 접근하는 경우를 가정하여, 하나 이상의 쓰레드로 부터 어떤 클래스의 인스턴스를 사용하는 것이 안전하다면, 그 클래스 재진입이 가능한 클래스이다. 각각의 호출에 고유한 데이터만을 참조한 다면, 하나 이상의 쓰레드에서 동시에 호출되어도 안전한 함수 역시 재진입 가능 함수이다. 다시 말해, 사용자는 이 클래스/함수에서 모든 인스턴스/공유데이터에 대한 접근을 어떠한 외부 락킹 메커니즘(external locking mechanism) 을 사용해 직렬화(serialize) 해야만 한다는 것을 의미한다.
- 쓰레드안전 만일, 하나 이상의 쓰레드에서 동시에 인스턴스들을 사용하는 것이 안전하다면, 그 인스턴스의 클래스는 쓰레드 안전하다. 어떤 함수가, 호출을 통해 공유데이터를 참조하더라도, 한번에 하나 이상의 쓰레드에서 실행해도 안전하다면, 그 함수는 쓰레드 안전하다.
이벤트와 이벤트 루프
이벤트 구동방식의 툴킷으로, 이벤트들과 이벤트의 전송은 Qt 아키텍쳐에 있어 가장 중요한 역할을 담당하고 있다. 이 문서에서는 이 주제와 관련하여 자세한 언급은 하지 않도록 한다( "여기":http://doc.qt.nokia.com/latest/eventsandfilters.html , 그리고 "여기":http://doc.qt.nokia.com/qq/qq11-events.html 를 참조하여, Qt 이벤트 시스템에 대한 좀 더 자세한 정보를 참조).
Qt 에서 이벤트 는 현재 발생하는 사건에 대한 어떤 내용을 담고 있다. 이벤트와 시그널의 주요한 차이는 이벤트는 응용프로그램내의 특정 객체를 "대상" 으로 하고 있는 것과 달리, 시그널은 "주변으로" 퍼져나간다(emitted된다). 코드의 관점에서 보면, 어떤 객체를 위한 모든 이벤트는 "QEvent":http://doc.qt.nokia.com/latest/qevent.html 의 파생클래스이며, 모든 QObject 파생클래스들은 QObject::event() 가상함수를 오버라이딩하여 자신의 인스턴스들을 대상으로 들어오는 이벤트들을 핸들링할 수 있다.
이벤트는 응용프로그램의 내부 및 왜부에서 모두 발생할 수 있다. 예를 들자면,
- QKeyEvent 와 QMouseEvent 객체들은 키보드 및 마우스 상호작용을 표현하며, 윈도우 메니저로 부터 수신된다.
- QTimerEvent 객체들은 는 자신의 타이머가 동작할 때 어떤 QObject 로 보내지며, (대개는) 운영체제로 부터 오는 경우가 많다.
- QChildEvent 객체는 자식이 추가/삭제되는 경우 QObject 로 보내지며, Qt 응용프로그램 내부에서 온다.
이벤트와 관련하여 중요한 점은 발생하는 즉시 전달되지 않는 다는 점이다. 대신, 이벤트 큐(event queue) 에 추가되어 나중에 전달된다. 전달자의 역할을 하는 디스패쳐는 스스로 이 이벤트큐에 대해 순회하며 각각의 추가된 이벤트들을 전달 대상 객체로 전송하며, 이를 이벤트 루프(event loop) 라 한다. 개념적으로 이벤트 루프는 이렇다(Qt Quarterly 문서에 대한 위 링크를 참조)
while (is_active)
{
while (!event_queue_is_empty)
dispatch_next_event();
wait_for_more_events();
}
QCoreApplication::exec();<code> 을 실행하면 Qt 의 메인 이벤트 루프로 들어간다. 이 호출은 <code>QCoreApplication::exit();<code> 혹은 <code>QCoreApplication::quit()<code> 이 호출되면 루프가 종료된다.
"wait_for_more_events()" 함수는 이벤트가 생성되기 전까지는 리턴되지 않는다(이것은 소위 CPU 자원을 잡아먹는 "바쁜 대기" 는 아니다). 이를 놓고 생각해 볼 때, 특정 시점에 이벤트들을 생성할 수 있는 모든 것은 ''외부'' 에 있다( 모든 내부 이벤트들에 대한 전달(dispatching)은 이제 종료되어 이벤트 큐에는 더이상 전달할 지연 이벤트들이 없기 때문이다). 따라서, 이벤트 루프는 다음에 의해 깨어날 수 있다.
* 윈도우 매니저의 동작(키/마우스 누름, 윈도우 상호작용 …등);
* 소켓 동작(읽어들여야 할 데이터가 수신되었거나, 대기없이 쓰기동작이 가능해 졌거나, 새로운 접속요청이 들어오거나…등)
* 타이머 (즉, 타이머가 걸리면)
* 다른 쓰레드로 부터 큐에 추가(posted)된 이벤트들
UNIX계열의 시스템에서는 윈도우 메니저 동작(즉 X11)은 응용프로그램으로 소켓을 통해 이루어진다(Unix Domain 혹은 TCP/IP), 따라서 클라이언트는 이를 사용해 X 서버와 통신할 수 있다. 만일, 쓰레드간 이벤트 포스팅(event posting)을 내부 socketpair(2)를 통해 구현하려 한다면, 이벤트 루프를 깨울 수 있는 활동은 다음의 2가지로 압축된다.
* 소켓
* 타이머
이 것들은 '''select(2)''' 시스템 호출이 하는 것과 정확히 일치한다. 일련의 디스크립터들을 지켜보면서 어떤 동작이 발생하거나 타임아웃(설정가능한 타임아웃)이 발생하는지를 한동안 주시하고 있는 것이다. Qt 가 필요로 하는 것은 select 가 반환해주는 내용을 옳바른 QEvent 파생클래스로 바꾸어 이벤트 큐에 넣는 것이다. 이제, 이벤트 루프에 어떤 것들이 있는지 알겠는가? :)
== 이벤트 루프 구동에 필요한 것들은? ==
정말 많다. 하지만, 전체적인 그림을 그려볼 수 있다면, 어떤 클래스들이 있어야 하는지 추정할 수 있을 것이다.
* '''위젯 페인팅(Widgets painting) 및 상호작용(interaction)''' : QWidget::paintEvent() 는 QPaintEvent 객체를 전달될 때 호출되는 함수 이며, 이는 QWidget::update() 을 호출함으로 생성된다(즉 '내부적으로' 생성된다). 이처럼 다른 모든 종류의 상호작용(키보드, 마우스 등)도 이벤트 루프가 있어야 전달될 수 있다.
* '''타이머(Timer)''': 간단히 말해, select(2) 혹은 이와 유사한 호출에서 타임아웃이 발생하면 이 이벤트가 발생한다. 따라서, Qt 로 하여금 이러한 호출을 우리를 대신해 이벤트 루프로 반환하도록만 하면 된다.
* '''네트워킹(Networking)''' : 모든 저수준 Qt 네트워킹 클래스들(QTcpSocket, QUdpSocket, QTcpServer, 등)은 비동기적으로 설계되었다. read() 를 호출하면 읽어들일 수 있는 만큼만 읽고 반환된다. write() 역시 나중에 쓰기동작을 예약만한다. 유의할 점은 동기 메소드(synchronous method)들이 제공된다는 점이다(waitFor* 계열의 메소드들). 하지만, 이것들을 사용하게 되면, 대기시 이벤트 루프가 먹통이 되기 때문에 곤란하다. QNetworkAcessManager와 같은 고수준 클래스들은 그 어떤 동기 API를 제공하지 않으며, 이벤트 루프를 필요로 한다.
== 이벤트 루프의 블록킹 ==
왜 '''절대로 이벤트 루프를 블록킹하면 안되는지''' 에 대해 논의하기 전에, 이 "블록킹" 의 의미에 대해 알아보기로 하자. 만일 어떤 버튼 위젯이 있는 클릭했을 때 시그널을 발생하도록 되어있다고 치자. 이 시그널에 연결된 다수의 작업을 진행하는 작업 객체의 슬롯이 있다면, 버튼을 누르고 나면, 스택 트레이스는 다음과 같은 것이다(스택은 아래 방향으로 증가한다고 가정하자).
# main(int, char ''')
# QApplication::exec();
# […]
# QWidget::event(QEvent ''')
# Button::mousePressEvent(QMouseEvent''')
# Button::clicked()
# […]
# Worker::doWork()
보통, main() 함수에서 QApplication::exec(); 을 호출하여 이벤트 루프가 시작된다(2행). 윈도우 매니저가 마우스 클릭을 보내주면, Qt 커널이 이를 받아서 QMouseEvent 로 변환해 QApplication::notify()함수(위 코드에서는 보이지 않음)를 통해 위젯의 event() 메소드로 전달한다(4행). 버튼 클래스의 구현이 event()를 따로 오버라이드 하지는 않았기 때문에, 이 이벤트는 QWidget::event() 에 의해 검출되어 실제 마우스 클릭임을 인식하고, 좀 더 구분된 이벤트 핸들러인 Button::mousePressEvent()를 호출한다(5행). 버튼 클래스는 이 메소드를 오버라이드 하고 있으며, 내부에서 Button::clicked() 시그널을 발생시키며, 최종적으로 작업 객체클래스의 Worker::doWork 슬롯이 호출된다(7행)
작업 객체가 바쁘게 작업을 진행하고 있는 동안, 이벤트 루프에는 무슨일이 생길까? 알 수 있을것이다. 아무일도 안생긴다! 마우스 눌림 이벤트를 전달하고 나서 이벤트 핸들러가 반환될 때까지만을 기다리면서 멈추어 있다. 어쩔 수 없이 '''이벤트 루프를 블록킹''' 한 것이다.즉, doWork() 슬롯이 반환되어 스택이 위쪽으로 줄어들어 이벤트 루프까지 간 다음 지연된 이벤트를 처리하게 되기 전까지는 그 어떤 이벤트도 더이상 전송되지 않는다.
이벤트 전송이 멈추게 되면, '''모든 위젯이 화면 갱신을 멈추며''' (QPaintEvent 객체는 여전히 큐에 머물게 된다), '''타이머는 발생되지 않으며''', '''네트워크 통신도 느려지고, 수신/전송 큐가 차다가, 시간이 지나면 멈추게 된다'''. 더구나 대부분의 윈도우 매니저들은 사용자의 응용프로그램이 더이상 이벤트를 처리하지 않는 것을 검출하여 사용자에게 *응용프로그램이 응답하지 않음"을 알려준다. 이 때문에 이벤트에 반응하여 가능한 최대한 빨리 이벤트 루프로 반환되도록 하는 것이 중요하다.
h2. 이벤트 강제 디스패칭(Forcing event dispatching)
그럼, 오랜 시간이 걸리는 작업을 하면서도 이벤트 루프가 블록킹되지 않도록 하려면 어떻게 해야 할까? 한가지 가능한 해답은 이 작업을 다른 쓰레드로 옮기는 것이다. 다음 섹션에서 어떻게 하면 되는지 알게 될 것이다. 또 다른 방법은 강제로 이벤트 루프가 실행되도록 하는 것이다. 이는 (반복적으로) QCoreApplication::processEvents() 를 블록킹하고 있는 작업 내에서 호출하는 것이다. 이 함수는 이벤트 큐에 있는 모든 이벤트를 처리하고 난 다음에서야 호출자(즉, 작업 객체의 작업 함수)로 반환된다.
또 다른 방법은 "QEventLoop":http://doc.qt.nokia.com/latest/qeventloop.html 클래스를 사용하여 강제로 이벤트 루프로 재진입하는 것이다. QEventLoop::exec() 를 호출하여, QEventLoop::quit() 슬롯에 시그널을 연결하여 동작을 중간에 멈추게 할 수 있다.
QNetworkAccessManager qnam; QNetworkReply reply = qnam.get(QNetworkRequest(QUrl(…))); QEventLoop loop; QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit())); loop.exec(); / reply has finished, use it */
QNetworkReply 는 블록킹 API 를 제공하지 않으며, 이벤트 루프가 돌고 있어야만 동작을 한다. 임시로 만든 QEventLoop 로 들어간 후, 응답이 완료되면, 이 이벤트 루프가 종료된다.
이벤트 루프로 재진입할 때는 "샛길로 빠지는" 경우를 조심해야 한다. 원치 않는 재귀 호출이 일어나기 때문이다. 만일, QCoreApplication::processEvents() 를 doWork() 슬롯 내에서 호출하면, 사용자는 버튼을 또 누를 수 있게 될 것이고, 그렇게 되면 doWork() 슬롯이 '''또 다시''' 호출되기 때문이다.
# main(int, char''')
# QApplication::exec()
# […]
# QWidget::event(QEvent ''')
# Button::mousePressEvent(QMouseEvent''')
# Button::clicked()
# […]
# Worker::doWork() // '''맨 처음의 내부 호출'''
# QCoreApplication::processEvents() // '''수동으로 이벤트를 전달하게 되어..'''
# […]
# QWidget::event(QEvent * ) // '''다른 마우스 클릭이 버튼으로 또 전달 된다'''
# Button::mousePressEvent(QMouseEvent *)
# Button::clicked() // '''clicked() 시그널이 또 발생하고..'''
# […]
# Worker::doWork() // '''아앗! 슬롯으로 또 들어와 버린다!'''
이 문제에 대한 아주 간단한 해결방법은 QEventLoop::ExcludeUserInputEvents 값을 QCoreApplication::processEvents() 로 전달하는 것이다. 이렇게 하면, 사용자의 입력과 관련한 이벤트들은 전달되지 않는다(그냥 큐에 남아있는다).
다행스럽게도, 이와 같은 문제가 '''이벤트 제거'''(QObject::deleteLater())를 위해 이벤트 큐에 들어간 이벤트에 대해서는 적용되지 '''않는다''' 는 것이다(다행스럽게도 삭제가 이루어질 것이다).
QObject *object = new QObject; object->deleteLater(); QDialog dialog; dialog.exec();
'''will not''' make object a dangling pointer (the event loop entered by QDialog::exec() is more nested than the deleteLater call). The same thing applies to local event loops started with QEventLoop. The only notable exception I've found to this rule (as of Qt 4.7.3) is that if deleteLater is called when NO event loop is running, then the first event loop entered will pick up the event and delete the object. This is pretty much reasonable, since Qt does not know about any "outer" loop that will eventually perform the deletion, and therefore deletes the object immediately.
위 코드는 길잃은 포인터(dangling pointer) 문제를 유발하지 '''않는다'''(QDialog::exec()에 의해 들어간 이벤트 루프는 deleteLater 호출 보다 더 안쪽에 있다). 유사한 내용이 QEventLoop 로 시작된 임시 이빈트 루프에도 적용될 수 있다. 눈여겨 볼 점은 (Qt 4.7.3버젼 기준) 이벤트 루프가 실행되지 않늘 때 deleteLater 를 호출하게 되면, 맨 처음 이벤트 루프로 들어갈 때 이 이벤트가 수집되어 객체를 삭제한다는 것이다. Qt 가 그 어떤 최종적으로 삭제를 수행하는 "외부" 루프에 대해 모르기 때문에 객체의 삭제는 곧바로 일어나게 되므로, 꽤 논리적이라 할 수 있다.
.
= Qt 쓰레드 클래스들 =
<blockquote>컴퓨터는 상태기계이다. 쓰레드는 상태기계를 프로그래밍 할 줄 모르는 사람들을 위한 것이다.
— 앤론 콕스(Alan Cox)
</blockquote>
Qt 는 많은 세월동안 쓰레드를 지원해 왔다(2000년 10월 22일 릴리즈된 Qt 2.2 부터 QThread 클래스가 등장하였다). 4.0 이 릴리즈되면서는 모든 플랫폼에서 쓰레드 지원이 이루어졌다(하지만, 이 기능은 끌 수가 있다. "여기":http://doc.qt.nokia.com/latest/fine-tuning-features.html 를 참조). Qt 는 이제 쓰레드와 관련한 몇가지 클래스를 더 지원하고 있다. 개괄 부터 시작해 보자.
== QThread ==
"QThread":http://doc.qt.nokia.com/latest/qthread.html 는, Qt 쓰레드 지원의 핵심이 되는 저수준 클래스이다. QThread 객체는 하나의 쓰레드 실행을 표현한다. Qt 가 cross-platform 이기 때문에, QThread 는 모든 플랫폼 종속적인 코드들을 숨긴 상태로 서로 다른 운영체제의 사용이 가능하다.
QThread를 사용해 코드를 어떤 쓰레드에서 실행하려면, 먼저 이 클래스를 상속 받아 QThread::run() 메소드를 오버라이드해야 한다.
class Thread : public QThread { protected:
void run() { /* 여기 사용자의 실행코드가 들어간다. */ }
};
그런 다음 아래와 같이
Thread t = new Thread; t->start(); // start(), not run()!
하여 새로운 쓰레드를 시작할 수 있다. Qt 4.4 부터는 QThread 가 더이상 가상 클래스가 아님에 유의하자. 가상 메소드 QThread::run() 은 내부적으로 QThread::exec() 을 호출하여 ''쓰레드의 이벤트 루프'' 를 시작하도록 되었다.
h2. QRunnable 과 QThreadPool
"QRunnable":http://doc.qt.nokia.com/latest/qrunnable.html 은 경량의 추상 클래스이며 "실행 한 다음 잊어 버리기" 방식으로 임의 작업을 시작하는 데 사용될 수 있다. 이를 위해서 모든 QRunnable 의 파생클래스는 run() 순수 가상 메소드를 오버라이딩 해야 한다.
class Task : public QRunnable { public:
void run() { / 사용자의 실행할 구현이 여기에 온다 */ }
};
실제로 QRunnable 객체를 실행하기 위해서는, 쓰레드의 풀(pool)을 관리하는 "QThreadPool":http://doc.qt.nokia.com/latest/qthreadpool.html 클래스를 사용해야 한다. QThreadPool::start(runnable) 식으로 호출하여 QRunnable 객체를 QThreadPool의 실행큐에 집어 넣는다. 어떤 쓰레드라도 사용가능한 상태가 되자마자, 이 QRunnable 객체는 쓰레드로 전달되어 실행된다. 모든 Qt 응용프로그램은 전역 쓰레드 풀을 가지고 있으며, QThreadPool::globalInstance() 을 호출해 이를 얻을 수 있지만, 언제라도 QThread 인스턴스를 직접 만들어 명시적으로 사용할 수 도 있다.
주의 할 점은 QRunnable 은 QObject 파생 클래스가 아니므로 시그널/슬롯을 사용할 수 없고, 다른 컴포넌트와 무언가를 주고 받기 위해서는 저수준 쓰레딩 프리미티브(결과를 수집하기 위한 뮤텍스에 의해 보호되는 큐와 같은..)을 사용해 직접 통신코드를 구현해야 한다.
== QtConcurrent ==
"QtConcurrent":http://doc.qt.nokia.com/latest/threads-qtconcurrent.html 은 고수준 API 로서, QThreadPool 위에 구현되었으며, 대부분의 "map":http://en.wikipedia.org/wiki/Map_(higher-order_function), "reduce":http://en.wikipedia.org/wiki/Fold_(higher-order_function), 및 "filter":http://en.wikipedia.org/wiki/Filter_(higher-order_function) 과 같은 병렬 컴퓨팅 패턴을 다루는 데 있어 유용하다. 이 역시, QtConcurrent::run() 메소드를 통해서 다른 쓰레드에서 임의 함수를 쉽게 실행 할 수 있다.
QThread 나 QRunnable 과는 달리 QtConcurrent 는 그 어떤 저수준 동기화 객체를 사용할 필요가 없다. 대신, 모든 QtConcurrent 메소드들은 "QFuture":http://doc.qt.nokia.com/latest/qfuture.html 을 반환하는데, 이를 사용하여 작업 상황(진행 상태)를 질의할 수 있으며, 진행 중인 작업을 pause/resume/cancel 할 수 있다. 또한 이것은 작업의 ''결과'' 도 포함하고 있다.
== 기능 비교 ==
{|
!
!QThread
!QRunnable
!QtConcurrent<ref>QtConcurrent::run 은 예외. QRunnable 을 사용해 구현되었으므로 장/단점이 있다.
</ref>
|-
|고수준 API
|✘
|✘
|✔
|-
|Job 지향
|✘
|✔
|✔
|-
|pause/resume/cancel 기본 지원
|✘
|✘
|✔
|-
|서로 다른 우선순위에서 실행 가능
|✔
|✘
|✘
|-
|이벤트 루프를 실행 가능
|✔
|✘
|✘
|}
= 쓰레드와 QObject 객체 =
== 쓰레드 별 이벤트 루프 ==
지금까지 우리는 "''소위'' 이벤트 루프 라는 것" 에 대해 얘기 했었다. Qt 응용프로그램에는 오직 하나의 이벤트 루프만이 있는 것처럼 여겨지는 경우가 많다. 하지만, 실제로는 아니다. QThread 객체는 자기 자신만의 이벤트 루프를 자신의 쓰레드에서 실행한다. 그러므로 '''메인 이벤트 루프''' 라고 하는 것만이 main()이 호출되면서 생성된것이고, QCoreApplication::exec() 에 의해서 시작된다고 할 수 있다. 이는 다른 말로 '''GUI 쓰레드''' 라고도 하는데, 이는 GUI 와 관련한 작업이 허용되는 유일한 쓰레드이기 때문이다. 쓰레드별 이벤트 루프는 QThread::exec(); 에 의해 실행된다.
class Thread : public QThread { protected:
void run() { /* … 초기화 작업 … */
exec();
}
};
앞서 언급한 바, Qt 4.4 부터는 QThread::run()가 더 이상 순수 가상 메소드가 아니라 QThread::exec() 을 실행하는 보통 메소드가 되었다.
A thread event loop delivers events for all QObjects that are '''living''' in that thread; this includes, by default, all objects that are created into that thread, or that were moved to that thread (more info about this later). We also say that the '''thread affinity''' of a QObject is a certain thread, meaning that the object is living in that thread. This applies to objects which are built in the constructor of a QThread object:
쓰레드의 이벤트 루프는 이벤트를 그 쓰레드에서 '''운용되는(혹은 살아가는)''' 모든 QObject 객체들에 전달된다. 여기에는 디폴트로 그 쓰레드에서 생성된 모든 객체와 다른 쓰레드에서 생성되었다가 옮겨진 객체가 포함된다(옮겨지는 경우에 대해서는 아래에서 좀 더 상세한 정보를 다룬다). 어떤 QObject 객체의 '''쓰레드 친화도(thread affinity)''' 가 특정 쓰레드다.. 라고 하기도 하는데, 이는 이 객체가 그 쓰레드에서 살아가고 있다는 것을 의미한다. 이는 QThread 객체의 생성자에서 생성된 객체들에 적용된다.
class MyThread : public QThread { public:
MyThread() { otherObj = new QObject; }
private:
QObject obj; QObject *otherObj; QScopedPointer<QObject> yetAnotherObj;
};
What's the thread affinity of obj, otherObj, yetAnotherObj after we create a MyThread object? We must look at the thread that created them: it's the thread that ran the MyThread constructor. Therefore, all three objects are '''not''' living in the MyThread thread, but in the thread that created the MyThread instance (which, by the way, is where the instance is living as well).
위에서, MyThread 객체를 생성한 다음 obj, otherObj, 및 yetAnotherObj 객체 각각의 쓰레드 친화도라는 것이 어떤 의미일까? 각각을 생성하고 있는 쓰레드에 주목해야 한다. 이는 MyThread 생성자를 실행하는 가 된다. 따라서, 이 3개의 객체는 모두 MyThread 쓰레드에서 '''살아가지 않는다''', 대신 MyThread 인스턴스를 생성한 쓰레드에서 '''살아간다''' (그런데, 이 MyThread 객체 자체도 자신을 생성한 쓰레드에서 살아간다고 할 수 있다!).
We can query anytime the thread affinity of a QObject by calling QObject::thread(). Notice that QObjects created before a QCoreApplication object have '''no thread affinity''', and therefore no event dispatching will be done for them (in other words, QCoreApplication builds up the QThread object that represents the main thread).
어떤 QObject 객체의 쓰레드 친화도는 언제라도 QObject::thread() 를 호출하여 질의할 수 있다. QCoreApplication 객체보다 앞서 생성된 그 어떤 QObject 객체들은 '''그 어떤 쓰레드 친화도도 없다'''. 그리고, 이런 이유로 이들 객체에는 그 어떤 이벤트도 전달되지 않는다(다시 말해, QCoreApplication 은 메인 쓰레드를 담당하는 QThread 객체를 직접 구축한다).
[[Image:http://doc.qt.nokia.com/4.7/images/threadsandobjects.png|http://doc.qt.nokia.com/4.7/images/threadsandobjects.png]]
We can use the thread-safe QCoreApplication::postEvent() method for posting an event for a certain object. This will enqueue the event in the event loop of the thread the object is living in; therefore, the event will not be dispatched unless that thread has a running event loop.
쓰레드 안전한 QCoreApplication::postEvent() 를 사용해 이벤트를 임의 객체의 이벤트 규에 추가(posting)할 수 있다. 이렇게 하면 이벤트를 객체가 살아가는 쓰레드의 이벤트 루프에 집어넣게 된다. 하지만, 쓰레드가 이벤트 루프를 실행하고 있지 않다면 이벤트는 전달되지 않을 것이다.
It is very important to understand that QObject and all of its subclasses '''are not thread-safe''' (although they can be reentrant); therefore, you can not access a QObject from more than one thread at the same time, unless you serialize all accesses to the object's internal data (for instance, by protecting it with a mutex). Remember that the object may be handling events dispatched by the event loop of the thread it is living in while you're accessing it from another thread! For the same reason, you can't delete a QObject from another thread, but you must use QObject::deleteLater(), which will post an event that will ultimately cause its deletion by the thread the object is living in.
Object 및 이의 파생클래스들은 *쓰레드 안전하지 않다*는 점을 반드시 이해하여야 한다. 따라서, 객체의 내부 데이터로의 접근을 직렬화 하지 않는다면(이를 테면, 뮤텍스로 보호하는 …), 하나 이상의 쓰레드로 부터 QObject 객체로 접근해서는 안된다. 다른 쓰레드로 부터 객체에 접근하는 동안 객체가 자신이 살아가는 쓰레드의 이벤트 루프에 의해 전달된 이벤트를 처리할 수 도 있음을 반드시 기억해야 한다! 같은 이유로 객체가 살아가는 쓰레드 이외의 다른 쓰레드에서 객체를 할당해제 할 수 없다. 이를 위해서는 QObject::deleteLater() 을 호출해서 객체가 살아가는 쓰레드로 할당 해제에 관한 이벤트를 큐에 집어넣고(posting), 최종적으로 자신의 쓰레드에서 할당해제가 이루어지도록 해야만 한다.
Moreover, QWidget and all of its subclasses, along with other GUI-related classes (even not QObject-based, like QPixmap) '''are not reentrant''' either: they can be used exclusively from the GUI thread.
더구나, QWidget 및 이의 모든 파생클래스는, 다른 GUI 관련 클래스들(심지어 QObject 기반이 아닌 QPixmap과 같은 클래스들)과 마찬가지로 '''재진입가능하지 않다'''. 이들은 오직 GUI 쓰레드에서만 배타적으로 사용되어져야만 한다.
We can change a QObject's affinity by calling QObject::moveToThread(); this will change the affinity of the object and of its children. Since QObject is not thread-safe, we must use it from the thread the object is living in; that is, you can only '''push''' objects from the thread they're living in to other threads, and not '''pull''' them or move them around from other threads. Moreover, Qt requires that the child of a QObject must live in the same thread where the parent is living. This implies that:
QObject의 진화도는 QObject::moveToThread() 를 호출하여 수정할 수 있다. 이렇게 하면, 객체 자신 뿐 아니라, 자식들 까지 자신이 살아갈 쓰레드를 바꾸게 된다. 즉, 이 객체들을 자신이 살아가는 쓰레드로 부터 전혀 다른 쓰레드로 '''집어넣는''' 것이지만, 그 반대로 다른 쓰레드로 부터 '''끌어오는*일은 불가능 하다. 더구나 Qt 는 QObject 의 자식들이 부모가 살아가는 쓰레드와 동일한 쓰레드에서 살아가도록 하는 것을 요구한다. 이는 암묵적으로 다음을 의미한다:
''' you can't use QObject::moveToThread() on a object which has a parent;
* you must not create objects in a QThread using the QThread object itself as their parent:
* QObject::moveToThread()를 부모를 가지고 있는 객체에 대해 사용할 수 없다;
* QThread 객체 자신을 부모로 하는 객체를 QThread 내에서 만들 수 없다.
class Thread : public QThread {
void run() { QObject obj = new QObject(this); // 잘못된것[[Image:|Image:]]! }
};
This is because the '''QThread object is living in another thread''', namely, the one in which it was created.
그 이유는 '''QThread 객체는 다른 쓰레드에서 살아가고 있기 때문*이다. 즉 자신을 생성한 쓰레드말이다.
Qt also requires that all objects living in a thread are deleted before the QThread object that represents the thread is destroyed; this can be easily done by creating all the objects living in that thread on the QThread::run() method's stack.
Qt 에서는 QThread 객체가 파괴된기 전에 이 객체가 나타내는 쓰레드에서 살아가던 모든 객체들이 파괴되는 것을 요구한다. 이는 QThread::run() 메소드의 스택에서 그 쓰레드에서 살아가는 모든 객체들을 생성하기만 하면 쉽게 구현된다.
h2. 쓰레드간의 시그널과 슬롯
Given these premises, how do we call methods on QObjects living in other threads? Qt offers a very nice and clean solution: we post an event in that thread's event queue, and the handling of that event will consist in invoking the method we're interested in (this of course requires that the thread has a running event loop). This facility is built around the method introspection provided by moc: therefore, only signals, slots and methods marked with the Q_INVOKABLE macro are invokable from other threads.
어떻게 하면 다른 쓰레드에서 살아가는 QObject 객체들의 메소드를 호출할 수 있을까? Qt 는 깔끔한 해결책을 제공하고 있다. 즉, 처리되기 원하는 쓰레드의 이벤트 큐에 이벤트를 추가하고(posting), 그 쓰레드에서 이벤트가 처리될 때 원하는 메소드가 호출되도록 하는 것이다(이렇게 하려면 물론, 해당 쓰레드의 QThread가 이벤트 루프를 실행하고 있어야만 한다). 이는 moc 에 의해 자동 생성되는 메소드로 구현되기 때문에, 오직 시그날과 슬롯 및 Q_INVOKABLE 매크로로 표기된 메소들만이 다른 쓰레드에서 호출가능하게 된다(역자주: Qt 4.x기준으로 QMetaCallEvent 라는 유형의 이벤트가 사용되고 있다).
The QMetaObject::invokeMethod() static method does all the work for us:
QMetaObject::invokeMethod() 정적 메소드는 이런일을 가능하게 해 준다.
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection, Q_ARG(type1, arg1), Q_ARG(type2, arg2));
Notice that since the arguments need to be copied in the event which is built behind the scenes, their types need to provide a public constructor, a public destructor and a public copy constructor, and must be registered within Qt type system by using the qRegisterMetaType() function.
이벤트가 처리되는 과정에서 암시적으로 위 함수의 인자들이 복사되어질 필요가 있음에 유의하자. 따라서, 이 인자들의 형(type)은, Qt의 형 체계(type system)하에서, qRegisterMetaType()을 사용해 공용 복사생성자, 공용 소멸자 및 공용 복사생성자가 반드시 등록되어져 있어야 한다.
Signals and slots across threads work in a similar way. When we connect a signal to a slot, the fifth argument of QObject::connect is used to specify the connection type:
쓰레드간 시그널 슬롯 역시 이와 유사한 방식으로 동작한다. 어떤 시그널을 슬롯에 연결할 때, QObject::connect 함수의 5 번째 인자는 연결유형(connection type)의 지정에 사용된다.
''' a '''direct connection''' means that the slot is always invoked directly by the thread the signal is emitted from;
* a '''queued connection''' means that an event is posted in the event queue of the thread the receiver is living in, which will be picked up by the event loop and will cause the slot invocation sometime later;
* a '''blocking queued connection''' is like a queued connection, but the sender thread blocks until the event is picked up by the event loop of the thread the receiver is living in, the slot is invoked, and it returns;
* an '''automatic connection''' (''the default'') means that if the thread the receiver is living in is the same as the current thread, a direct connection is used; otherwise, a queued connection is used.
* '''직접 연결(direct connection)''' 은 시그널이 발생되어진 쓰레드에서 항상 직접 슬롯을 호출하는 것을 의미한다(슬롯의 실행쓰레드는 시그널 발생 쓰레드)
* '''큐 연결(queued connection)''' 은 수신측 쓰레드의 이벤트 큐에 이벤트가 추가되고 나중에 이벤트 루프에서 빠질 때 슬롯이 호출되는 것을 의미한다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드)
* '''블록킹 큐 연결(blocking queued connection)''' 은 '''큐 연결*유형과 유사하지만, 시그널 발생측이 슬롯의 호출종료시까지 블록킹되는 것이 다르다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드)
''' '''자동 연결(automatic connection)''' (_디폴트_값임!) 은 슬롯이 시그널 발생측과 같은 쓰레드면 직접 연결되고, 그 외의 경우에는 큐 연결이 사용된다(슬롯의 실행쓰레드는 시그널 발생시 결정된다)
In every case, keep in mind ''the thread the emitting object is living in'' has no importance at all! In case of an automatic connection, Qt looks at the thread that invoked the signal and compares it with the thread the receiver is living in to determine which connection type it has to use. In particular, the "current Qt documentation":http://doc.qt.nokia.com/4.7/threads-qobject.html (4.7.1) '''is simply wrong''' when it states:
그 어떤 경우에라도 유념해야 할 사항은 _시그널을 발생시키는 객체가 살고 있는 쓰레드_는 전혀 중요하지 않다는 점이다! 자동 연결의 경우에도, Qt 는 시그널을 실행하는 쓰레드를 기준으로 이를 수신측 객채가 살고 있는 쓰레드와 비교하여 어떤 연결 유형을 쓸 것인지를 결정한다. "current Qt documentation":http://doc.qt.nokia.com/4.7/threads-qobject.html (4.7.1) 문서에 나와 있는 내용은 잘못되어있으므로 주의해야 한다.
''Auto Connection (default) The behavior is the same as the Direct Connection, if the emitter and receiver are in the same thread. The behavior is the same as the Queued Connection, if the emitter and receiver are in different threads.''
''자동 연결(auto connection, 기본값)유형의 동작은 시그널 발생측과 수신측이 동일 쓰레드이면(즉, 쓰레드 친화도가 동일하다면) 직접 연결(direct connection)의 동작과 동일하지만, 다른 쓰레드라면 큐 연결(queued connection)과 같아진다''
because the emitter object's thread affinity does not matter. For instance:
위의 문장이 잘못된 이유는, 시그널 발생 객체의 쓰레드 친화도는 동작의 차이와는 아무 상관이 없기 때문이다, 예를 들면,
class Thread : public QThread {
Q_OBJECT
signals:
void aSignal();
protected:
void run() { emit aSignal(); }
};
/* … */ Thread thread; Object obj; QObject::connect(&thread, SIGNAL (aSignal()), &obj, SLOT (aSlot())); thread.start();
The signal aSignal() will be emitted by the new thread (represented by the Thread object); since it is not the thread the Object object is living in (which, by the way, '''is the same thread the Thread object is living in''', just to stress that the sender's thread affinity doesn't matter), a '''queued connection''' will be used.
위에서 aSignal() 시그널은 Thread 객체로 표현되는 새로운 쓰레드에서 발생하게 된다. 이 쓰레드 자체는 Object형의 obj객체가 살아가는 쓰레드가 아니다(한편, thread 객체의 쓰레드 친화도와는 상관이 없다는 점을 강조하자면 이 쓰레드는 Thread형의 thread 객체가 살아가는 쓰레드이며, 이는 친화도를 가지는 쓰레드와는 다르다). 따라서, '''큐 연결(queued connection)''' 이 사용된다.
Another common pitfall is the following one:
또 다른 범하기 쉬운 오류는 다음과 같은 것이다.
class Thread : public QThread {
Q_OBJECT
slots:
void aSlot() { /* … */ }
protected:
void run() { / … */ }
};
/ … */ Thread thread; Object obj; QObject::connect(&obj, SIGNAL (aSignal()), &thread, SLOT (aSlot())); thread.start(); obj.emitSignal();
When "obj" emits its aSignal() signal, which kind of connection will be used? You should've guessed it: a '''direct connection'''. That's because the Thread object is living in the thread that emits the signal. In the aSlot() slot we could then access some Thread's member variable while they're being accessed by the run() method, which is running concurrently: this is the perfect recipe for disaster.
"obj"가 aSignal() 시그널을 발생시킬 때 어떤 종류의 연결이 사용되게 될까? 한번 맞추어 보기 바란다.
바로 '''직접 연결(direct connection)''' 이다. 그 이유는 Thread 객체는 시그널을 발생시키는 쓰레드에서 살아고 있기 때문이다. 이 경우, run() 메모드에 의해 Thread의 멤버변수에 대한 접근이 이루어지는 동안 , aSlot() 슬롯에서도 역시 동일 변수들에 대한 접근이 이루어지게 될 수도 있게 되며, 이런 식으로 작업하면 큰 재앙으로 곧잘 이어지게 된다.
Yet another example, probably the ''most important'' one:
또 다른 다음 예는 어쩌면 ''가장 중요한'' 것일지도 모르겠다.
class Thread : public QThread {
Q_OBJECT
slots:
void aSlot() { /* … */ }
protected:
void run() { QObject obj = new Object; connect(obj, SIGNAL (aSignal()), this, SLOT (aSlot())); / … */ }
};
In this case a '''queued connection''' is used, therefore you're required to run an event loop in the thread the Thread object is living in.
이 경우, '''큐 연결(queued connection)''' 이 사용되는데, 그 이유는 Thread 객체가 살아가는 쓰레드(Thread가 표현하는 쓰레드, 즉 run()의 쓰레드와는 다르다)에 있는 이벤트 루프에 의해서 연결이 처리되기 때문이다.
A solution you'll often found in forums, blog posts etc. is to add a moveToThread(this) to the Thread constructor:
계시판, 블로그 글 등에서 종종 볼 수 있는 답변들 가운데에는 Thread 의 생성자에 moveToThread(this) 를 추가하라는 것이 많이 있다.
class Thread : public QThread {
Q_OBJECT
public:
Thread() { moveToThread(this); // WRONG }
/* … */ };
which indeed ''will work'' (because now the affinity of the Thread object changed), but it's a very bad design. What's wrong here is that we're misunderstanding the purpose of a thread object (the QThread subclass): ''QThread objects are not threads''; they're control objects around a thread, therefore meant to be used from another thread (usually, the one they're living in).
위와 같이 해도 사실은 ''동작을 한다''. (왜냐하면 Thread 객체의 쓰레드 친화도가 변경되기 때문이다). 하지만, 이는 잘못된 설계이다. 이런 방식의 문제는 thread객체(즉 QThread 파생클래스의 객체)의 목적을 잘못 이해하고 있다는 데 있다. ''QThread 객체들은 쓰레드가 아니다''. 이들은 쓰레드를 감싸고 있는 객체들을 제어하기 위한 것이며, 다른 쓰레드(대개 그 객체가 살아가는 쓰레드)로 부터 사용되어질 목적으로 존재한다.
'''A good way to achieve the same result''' is splitting the "working" part from the "controller" part, that is, writing a QObject subclass and using QObject::moveToThread() to change its affinity:
'''옳바른 결과를 얻기 위한 바람직한 방법*은 "제어부"로 부터 "동작부"를 떼어네어서 QObject의 파생클래스를 작성하고, QObject::moveToThread() 를 호출하여 쓰레드 친화도를 변경하는 것이다.
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() { / … */ }
};
/ … */ QThread *thread = new QThread; Worker worker = new Worker; connect(obj, SIGNAL (workReady()), worker, SLOT (doWork())); worker->moveToThread(thread); thread->start();
h2. 해야 할 것과 하지말아야 할 것
h3. 허용되는 것들…
''' … add signals to a QThread subclass. It's perfectly safe and they'll do the "right thing" (see above; the sender's thread affinity does not matter).
* QThread의 서브클래스에 시그널 추가하기 : 이는 완벽히 안전하면서도 "제대로" 일을 한다고 할 수 있다. (위를 보면 시그널 전송부의 쓰레드 친화도는 아무런 문제가 되지 않는 것을 알 수 있다).
=== 할 수는 있지만, 피해야 하는 것. ===
* … use moveToThread(this).
* … force the connection type: this usually means that you're doing something wrong, like mixing the control interface of QThread with the program logic (which should stay in a separate object which lives in that thread).
* … add slots to a QThread subclass: they'll be invoked from the "wrong" thread, that is, not the one the QThread object is managing, but the one that object is living in, forcing you to specify a direct connection and/or to use moveToThread(this).
* … use QThread::terminate.
* moveToThread(this) 사용하기
* 시그널 슬롯의 연결시 연결 유형 강제 적용하기 : 이런 구현은 QThread 의 제어 인터페이스와 프로그램의 로직을 섞는 것과 같은(로직은 자신이 살고 있는 쓰레드와는 다른 별도의 객체에 존재하는 것이 바람직하다) 오류를 범하는 경우가 많다.
* QThread 서브클래스에 슬롯 추가하기 : 호출되면 안되는 쓰레드로 부터 슬롯 호출이 이루어지게 될 수 있다. 이 허용되지 않는 쓰레드는 QThraed 객체가 관리하는 쓰레드가 아닌 다른 객체가 살아가는 쓰레드이며, QThread가 표현하는 쓰레드에서 호출이 이루어지게 하기 위해 직접 연결(direct connection)을 지정하게 되거나, moveToThread(this) 를 사용하게 되고 만다.
* QThread::terminate 사용하기
=== 절대 하면 안되는 것들 ===
* … quit your program when threads are still running. Use QThread::wait to wait for their termination.
* … destroy a QThread while the thread that it's managing is still running. If you want some kind of "self-destruction", you can connect the finished() signal with the deleteLater() slot.
* 쓰레드가 구동하고 있는 중 프로그램을 종료하는 행위 : QThread::wait 을 사용해 종료를 대기해야 만 한다.
* 자신이 관리하고 있는 쓰레드가 아직 구동중일때 이 쓰레드를 관리하던 QThread 객체를 파괴하는 행위 : "자동 파괴"와 같은 기능을 원한다면, QThread::finished() 시그널을 QObject::deleteLater() 슬롯과 연결만 하면 된다.
= 언제 쓰레드를 써야만 할 까? =
== 쓸 수 있는 API가 블록킹 유형의 것들 밖에 없을 때 ==
If you need to use a library or other code that doesn't offer a non-blocking API (by means of signals and slots, or events, or callbacks, etc.), then the only viable solution in order to avoid freezing the event loop is to spawn a process or a thread. Since creating a new worker process, having it doing the job and communicating back the results is definetely harder and more expensive than just starting a thread, the latter is the most common choice.
사용하고자 하는 라이브러리나 다른 이의 코드가 (시그널/슬롯, 이벤트, 혹은 콜백등의) 논블록킹 API 를 제공하지 않는다면, 이벤트 루프의 동작 중단을 방지하기 위한 유일한 방안은 프로세스 혹은 쓰레드를 만드는 것이다. 새로운 작업 프로세스의 생성은, 작업 지시와 통보등의 방법에 있어 쓰레드를 생성하는 방법보다 훨씬 어렵고 비용도 비싸므로, 가능한 쓰레드를 사용하는 편이 나을 수 있다.
A good example of such an API is '''address resolution''' (just to show you that we're not talking about 3rd-party crappy API. This is something included in every C library out there), which is the process of taking an host name and converting it into an address. This process involves a query to a (usually remote) system — the Domain Name System, or DNS. While, usually, the response is almost instantaneous, the remote servers might fail, some packet might get lost, the network connection might break, and so on; in short, it might take dozens of seconds before we get a reply from our query.
이런 종류의 API 중 가장 좋은 예는 '''주소 확인(address resolution)''' API 이다(3rdparty 라이브러리로 나온 것이 아니라도, 모든 C 라이브러리에 포함되어 있다). 이 API 는 호스트 이름을 가져다 주소 값으로 변환해 준다. 그 일련의 과정은 대개 원격지에 있는 시스템(Domain Name System 즉 DNS)에 대한 질의로 이루어진다. 응답이 거의 순간적으로 오기는 하겠지만, 원격지 서버가 응답을 못하거나 패킷유실이 이루어지거나, 네트워크 연결이 끊어지는 듯듯의 사고 가 있을 수 있다. 어쨌든 질의에 대한 응답이 오기 까지는 몇초정도가 걸린다.
The only standard API available on UNIX systems is ''blocking'' (not only the old-fashioned gethostbyname(3), but also the newer and better getservbyname(3) and getaddrinfo(3)). "QHostInfo":http://doc.qt.nokia.com/latest/qhostinfo.html, the Qt class that handles host name lookups, uses a QThreadPool to enable the queries to run in the background (see "here":http://qt.gitorious.com/qt/qt/blobs/master/src/network/kernel/qhostinfo.cpp ; if thread support is turned off, it switches back to a blocking API).
UNIX 시스템에 존재하는 표준 API는 ''블록킹'' 방식이다(오래된 gethostbyname(3) 이 아니더라도, 새로운 getservbyname(3) 이나 getaddrinfo(3) 도 마찬가지다). Qt 클래스인 "QHostInfo":http://doc.qt.nokia.com/latest/qhostinfo.html 은 호스트명칭의 검색에 있어, QThreadPool 을 사용하여 백그라운드로 이를 처리한다( "여기":http://qt.gitorious.com/qt/qt/blobs/master/src/network/kernel/qhostinfo.cpp 를 확인).
Other simple examples are '''image loading''' and '''scaling'''. "QImageReader":http://doc.qt.nokia.com/latest/qimagereader.html and "QImage":http://doc.qt.nokia.com/latest/qimage.html only offer blocking methods to read an image from a device, or to scale an image to a different resolution. If you're dealing with very large images, these processes can take up to (tens of) seconds.
또 다른 간단한 예는 '''이미지 로딩''' 이나 '''스케일링(scaling)''' 이다. "QImageReader":http://doc.qt.nokia.com/latest/qimagereader.html 와 "QImage":http://doc.qt.nokia.com/latest/qimage.html 는 디바이스로 부터 이미지를 읽어들이는 블록킹 메소드만 제공한다. 아주 큰 이미지들을 다루거나 하게 되면, 이들 기능들은 수십초 이상의 시간이 걸리게 될 것이다.
== CPU의 갯수에 따라 구현 규모를 조정하고자 할 때 ==
Threads allow your program to take advantage from multiprocessor systems. Since each thread is scheduled independently by the operating system, if your application is running on such a machine the scheduler is likely to run each thread on a different processor '''at the same time'''.
쓰레드는 사용자의 프로그램으로 하여금 다중프로세서 시스템의 이점을 활용할 수 있게 해준다. 각각의 쓰레드가 운영체제에 의해 독립적으로 스케쥴링되므로, 응용프로그램이 이러한 다중 프로세서 시스템에서 구동된다면, 스케쥴러는 각 쓰레드를 독립적인 프로세서에서 '''동시에''' 구동될 수 있게된다.
For instance, consider an application that generates thumbnails from a set of images. A '''thread farm''' of ''n'' threads (that is, a thread pool with a fixed number of threads), one per each CPU available in the system (see also QThread::idealThreadCount() ), can spread the work of scaling down the images into thumbnails on all the threads, effectively gaining an almost linear speedup with the number of the processors (for simplicity's sake, we consider the CPU being the bottleneck).
예를 들어, 일련의 이미지들에 대한 썸네일을 생성하는 응용프로그램이 있다하자. ''n'' 개의 쓰레드로 구성된 '''쓰레드 팜(thread farm)''' (즉, 고정된 갯수의 쓰레드로 구성된 쓰레드 풀) 은 시스템에 존재하는 CPU 하나당 하나씩의 쓰레드를 가지며(QThread::idealThreadCount() 를 참조), 이미지를 썸네일로 스케일 다운하는 작업을 모든 쓰레드에 분산시켜 작업할 수 있게 할 수 있다. 이런식으로 프로세서 갯수에 대해 거의 선형적인 승능향상을 얻게 된다(단순화하기 위해 CPU 자체가 병목임을 가정하면).
== 다른 작업에 방해받지 않기를 원할 때 ==
음. 예를 들어 설명한다.
This is quite an advanced topic, so feel free to skip it for now. A nice example of this use case comes from QNetworkAccessManager usage inside WebKit. WebKit is a modern browser engine, that is, a set of classes to lay out and display web pages. The Qt widget that uses WebKit is QWebView.
이 주제는 조금 난해할 수도 있으므로, 일단 넘어가도 상관은 없다. 이 경우에 대한 가장 좋은 예는 QNetworkAccessManager가 WebKit 에서 어떻게 사용되는지와 관련해서 이다. WebKit 은 최신의 브라우저 엔지이며, 웹페이지를 배치하고 표시하는 일련의 클래스들이다. WebKit 을 사용한는 Qt widget 은 QWebView 이다.
QNetworkAccessManager is a Qt class that deals with HTTP requests and responses for all purposes, we can consider it to be the networking engine of a web browser. Its current design does not make use of any worker threads; all networking is handled in the same thread QNetworkAccessManager and its QNetworkReplys are living in.
QNetworkAccessmanager 는 모든 HTTP 요청과 응답을 처리하는 Qt 클래스이다, 웹 브라우저의 네트워킹 엔진을 생각해 볼 수 있겠다. 현재의 설계는 그 어떤 작업 쓰레드도 사용하고 있지 않고 있다. 모든 네트워크 작업은 QNetworkAccessManager 와 이의 QNetworkReply 객체들이 살아가고 있는 쓰레드와 동일한 쓰레드에서 처리된다.
While not using threads for networking is a very good idea, it has also a major drawback: if you don't read data from the socket as soon as possible, the kernel buffers will fill up, packets will begin to be dropped, and the transfer speed will decrease considerably.
네트워킹과 관련하여 쓰레드를 사용하지 않는 것 자체는 매우 좋은 생각이지만, 치명적인 단점이 있다. 소켓으로 부터 데이터를 최대한 빨리 읽지 않게 되면, 커널 버퍼가 차게 되어 패킷이 드랍되며, 전송속도가 극단적으로 줄어들 수 있다는 점이다.
Socket activity (i.e., availability of some data to read from a socket) is managed by Qt's event loop. Blocking the event loop will therefore lead to a loss of transfer performance, because nobody will be notified that there are data to read (and thus nobody will read them).
소켓의 동작(소켓으로 부터 읽어들일 데이터의 존재여부)은 Qt 의 이벤트 루프에 의해서 관리된다. 이벤트 루프를 블록킹하게 되면, 읽어야할 데이터 거기 있다는 사실을 아무도 통지 받지 못헤게 되므로, 전송 성능이 떨어진다.
But what could block the event loop? The sad answer is: WebKit itself! As soon as some data are received, WebKit uses them to start laying out the web page. Unfortunately, the layout process is quite complicated and expensive, therefore it blocks the event loop for a (short) while, enough to impact on ongoing transfers (broadband connections play their role here, filling up kernel buffers in a small fraction of second).
하지만, 이벤트 루프를 누가 블록킹하게 되는 것일까? 슬프지만, 그 답은 WebKit 바로 자기 자신이다. 일련의 데이터를 수신하게 되면, WebKit 은 이를 사용해 웹 페이지를 구성하기 시작한다. 불운하게도, 이 구성 과정은 매우 복잡하고 비용도 크기 때문에 드문 드문 이벤트 루프를 블록킹하게 되고, 결국 추가적으로 들어오는 전송에 영향을 점점 더 많이 주기 시작한다.
To sum it up, what happens is something like this:
요약하자면, 다음과 같은 일들이 벌어진다고 할 수 있다.
* WebKit issues a request;
* some data from the reply begin to arrive;
* WebKit starts to lay out the web page using the incoming data, blocking the event loop;
* without a running event loop, data are received by the OS, but not read from QNetworkAccessManager sockets;
* kernel buffers will fill up, and the transfer will slow down.
* WebKit 이 요청을 발생시킨다.
* 응답으로 일련의 데이터가 도착한다.
* WebKit 이 웹 페이지를 들어온 데이터를 사용해 구성하기 시작하면서 이벤트 루프가 블록킹된다.
* 이벤트 루프가 돌지 않게 되면서 OS에 의해 수신되 데이터가 QNetworkAccessManager 의 소켓으로 부터 읽혀지지 않게된다.
* 커널 버퍼가 차게되어 전송이 느려진다.
The overall page loading time is therefore worsened by this self-induced transfer slowness.
전체적인 페이지 로딩 속도는 결국 자기 자신으로 인해 발생한 전송 속도 저하로 느려지게 된다.
Notice that since QNetworkAccessManagers and QNetworkReplys are QObjects, they're not thread-safe, therefore you can't just move them to another thread and continue using them from your thread, because they may be accessed at the same time by two threads: yours and the one they're living in, due to events that will be dispatched to them by the latter thread's event loop.
QNetworkAccessManager 와 QNetworkReply 객체들은 모두 QObject 들이다. 이들은 쓰레드 안전(thread safe)하지 않다. 따라서 단순히 이 객체들을 다른 쓰레드로 옮긴 다음 현재 쓰고 있던 쓰레드에서 사용할 수 는 없다. 왜냐하면, 후자 쓰레드의 이벤트 루프에 의해 전달되어진 이벤트로 인해, 두개의 쓰레드로 부터 동시에 사용될 수도 있기 때문이다.
As of Qt 4.8, QNetworkAccessManager now handles HTTP requests in a separate thread by default, so the result of unresponsive GUI and OS buffers filling up too quickly should be cured.
Qt 4.8 버젼부터 QNetworkAccessManager는 HTTP 요청을, 기본적으로, 독립적인 쓰레드에서 처리하도록 수정되었다. 따라서 GUI가 무응답하거나, OS 버퍼가 너무 빨리 차 버리는 문제가 해결되었다.
= 쓰레드를 사용하지 않는편이 좋은 경우는? =
<blockquote>쓰레드를 사용하려고 생각하고 있다면, 당신의 작업이 너무 방만하기 때문일것이오.
— 롭 파이크(Rob Pike)
</blockquote>
== 타이머 ==
This is perhaps the worst form of thread abuse. If we have to invoke a method repeatedly (for instance, every second), many people end up with something like this:
쓰레드 남용의 가장 심각한 유형에 대해 알아보자. 만일 어떤 메소드를 반복적으로 수행해야 한다면(예를 들어, 매 초마다), 대다수 사람들은 다음과 같은 구현을 떠올 릴 것이다.
// 아주 잘못 된 것 while (condition) {
doWork(); sleep(1); // C라이브러리의 sleep(3)함수
}
Then they figure out that this is '''blocking the event loop''', therefore decide to bring in threads:
그리고 나서, 이 구현은 *이벤트 루프를 블록킹*하고 있다는 것을 곧 깨닫고, 다음과 같이 쓰레드를 도입하기로 마음먹게 된다.
// 잘못된 것 class Thread : public QThread { protected:
void run() { while (condition) { // "condition" 은 volatile 이나 mutex 보호가 필요할지도 모른다. // 다른 쓰레드에서 이를 변경할 수 도 있기 때문이다(!) doWork(); sleep(1); // 이것은 QThread::sleep() 이다. } }
};
A much '''better and simpler way''' of achieving the same result is simply using timers, i.e. a "QTimer":http://doc.qt.nokia.com/latest/qtimer.html object with a 1s timeout, and make the doWork() method a slot:
이러한 작업을 하는 데 있어 '''훨씬 더 낳고 간단한 방법*은 타이머(즉, "QTimer":http://doc.qt.nokia.com/latest/qtimer.html )객체를 1초의 타임아웃을 주고 doWork()메소드를 슬롯으로 하여 사용하는 것이다.
class Worker : public QObject {
Q_OBJECT
public:
Worker() { connect(&timer, SIGNAL (timeout()), this, SLOT (doWork())); timer.start(1000); }
private slots:
void doWork() { / … */ }
private:
QTimer timer;
};
All we need is a running event loop, then the doWork() method will be invoked each second.
이제 필요한 것은 이벤트 루프를 실행하기만 하면 된다. 그러면 doWork() 메소드는 매 초마다 호출 될 것이다.
h2. 네트워킹과 상태기계(state machines)
A very common design pattern when dealing with network operations is the following one:
네트워크 동작과 관련하여 가장 일반적인 설계 패턴은 다음과 같은 것이다.
socket->connect(host); socket->waitForConnected();
data = getData(); socket->write(data); socket->waitForBytesWritten();
socket->waitForReadyRead(); socket->read(response);
reply = process(response);
socket->write(reply); socket->waitForBytesWritten(); / … 기타 등등 … */
Needless to say, the various waitFor'''() calls block the caller without returning to the event loop, freezing the UI and so on. Notice that the above snippet does not take into account any error handling, otherwise it would have been even more cumbersome. What is very wrong in this design is that we're forgetting that '''networking is asynchronous by design''', and if we build a synchronous processing around we're shooting ourselves in the foot. To solve this problem, many people simple move this code into a different thread.
더 말할 필요도 없이, 다양한 waitFor*()에 대한 호출로 인해 호출측은 이벤트 루프로 부터 리턴되지 않게 되며, UI가 동작을 멈추게 된다. 위와 같은 코드는 오류처리는 고려하지도 않았다. 그것마저 했다면 더 멍청한 상태로 빠졌을 지 모른다. 이 설계에 있어서 가장 잘못된 점은 '''네트워킹은 원래가 비동기적*이라는 사실을 망각한 채 동기처리로 구현하여 제살 깎아먹기를 했다는 점이다. 이 문제를 해결하기 위해서 많은 사람들은 단순히 위의 코드를 다른 쓰레드로 옮겨간다.
Another more abstract example:
또 다른 개념적인 예를 들어보겠다.
result = process_one_thing();
if (result->something())
process_this();
else
process_that();
wait_for_user_input(); input = read_user_input(); process_user_input(input); / … */
Which has more or less the same pitfalls of the networking example.
이 네트워킹 예제에서는 앞서와 유사한 단점이 존재한다.
Let's take a step back and consider from an higher point of view what we're building here: we want to create a '''state machine''' that reacts on inputs of some sort and acts consequently. For instance, with the networking example, we might want to build something like this:
한발 물러서서 구현되고 있는 것을 넓은 시각으로 바라보자. 여기서 우리는 일련의 입력에 따라 반응하고, 그에 따라 행동하는 *상태기계(state machine)'''을 구축하려고 하는 것이 아닐까? 예를들어 이 네트워킹 예제에서는 우리는 다음과 같은 것을 사실은 만들고 있는 것이다.
''' 휴지상태(Idle) → 연결중(Connecting) (connectToHost()가 호출될 때);
* 연결중(Connecting) → 연결완료(Connected) (connected() 시그널이 발생할 때);
* 연결완료(Connected) → 로그인 정보 전송(LoginDataSent) (서버에 로그인 정보를 전송할 때);
* 로그인 정보 전송(LoginDataSent) → 로그인(LoggedIn) (서버로 부터 ACK 응답을 받을 때)
* 로그인 정보 전송(LoginDataSent) → 로그인 오류(LoginError) (서버로 부터 NACK 응답을 받을 때)
기타 등등이다.
Now, there are several ways to build a state machine (and Qt even offers a class for that: "QStateMachine":http://doc.qt.nokia.com/4.7/qstatemachine.html ), the simplest one being an enum (i.e. an integer) used to remember the current state. We can rewrite the above snippets like this:
상태기계를 구축하는 몇가지 방법("QStateMachine":http://doc.qt.nokia.com/4.7/qstatemachine.html 라는 전용의 클래스를 제공하고도 있다)이 있기는 하지만 그 중 가장 간단한 enum (즉 정수형 값)을 사용하여 현재의 상태를 저장하도록 하여 위 예제코드를 다음과 같이 구현해 볼 수 있다.
class Object : public QObject {
Q_OBJECT
enum State {
State1, State2, State3 /* and so on */ };
State state;
public:
Object() : state(State1) { connect(source, SIGNAL (ready()), this, SLOT (doWork())); }
private slots:
void doWork() { switch (state) { case State1: / … */ state = State2; break; case State2: / … */ state = State3; break; / etc. */ } }
};
What the "source" object and its "ready()" signal are? Exactly what we want them to be: for instance, in the networking example, we might want to connect the socket's QAbstractSocket::connected() and the QIODevice::readyRead() signals to our slot. Of course, we can also easily add more slots if that suits better in our case (like a slot to manage error situations, which are notified by the QAbstractSocket::error() signal). This is a true asynchronous, signal-driven design!
"source"객체와 이의 "ready()" 시그널은 무엇이 될 수 있을까? 우리의 예제로 치자면, 소켓의 QAbstractSocket::connected() 와 우리가 구현한 슬롯에 대한 QIODevice::readyRead() 시그널과 같은 것으로 생각해 볼 수 있다. 물론, 경우에 많기만 한다면 더 많은 슬롯을 쉽게 추가할 수 도 있다(QAbstractSocket::error()시그널에 의해 통지 받는, 오류 상황을 관리하기 위한 슬롯). 이것이 진정한 비동기 시그널 주도 설계라 할 수 있다!
== 몇개의 덩어리로 나뉠 수 있는 작업들 ==
Suppose that we have a long computation which can't be easily moved to another thread (or that it can't be moved at all, because for instance it ''must run'' in the GUI thread). If '''we can split the computation in small chunks''', we can return to the event loop, let it dispatch events, and make it invoke the method that processes the next chunk. This can be easily done if we remember how queued connections are implemented: an event is posted in the event loop of the thread the receiver object is living in; when the event is delivered, the corresponding slot is invoked.
어떤 이유에서 단순히 다른 쓰레드로 옮길 수 없는 큰 계산 루틴이 있다고 가정해 보자(또는 GUI쓰레드에서만 실행되어야 하기 때문에 전혀 옮길 수 없다고 가정해 보자). 만일 '''계산을 작은 덩어리들로 나눌 수 있다면''', 이벤트 루프로 그 때 그 때 리턴할 수 있다. 이는 큐 연결이 어떻게 구현되어 있는지를 상기한다면, 쉽게 구성될 수 있는 내용이다. 이벤트 수신 객체가 살아가는 쓰레드의 이벤트 루프에 어떤 이벤트를 날리고, 그 이벤트가 처리될 때 상응하는 슬롯이 호출되도록 하면 된다.
We can use QMetaObject::invokeMethod() to achieve the same result by specifying Qt::QueuedConnection as the type of the invocation; this just requires the method to be invokable, therefore it must be either a slot or marked with the Q_INVOKABLE macro. If we also want to pass parameters to the method, they need to be registered within the Qt metatype system using qRegisterMetaType(). The following snippet shows this pattern:
QMetaObject::invokeMethod() 를 사용해 Qt::QueuedConnection 을 호출 방법으로 지정함으로써 동일한 결과를 얻을 수 도 있다. 이는 해당 메소드가 Q_INVOKABLE 지정되었거나 슬롯이어야만 한다. 만일 메소드에 인자를 전달해야 한다면, 그 인자의 형은 qRegisterMetaTyp() 을 사용해 Qt 의 메타타입 시스템에 등록해야 한다. 아래의 예제코드는 이러한 형태를 나타내어 준다.
class Worker : public QObject {
Q_OBJECT
public slots:
void startProcessing() { processItem(0); }
void processItem(int index)
{ /* process items[index] … */
if (index < numberOfItems) QMetaObject::invokeMethod(this, "processItem", Qt::QueuedConnection, Q_ARG(int, index + 1));
}
};
Since there are no threads involved, it's easy to pause/resume/cancel such a computation and collect the results back.
쓰레드가 사용되어야만 할 이유가 없기 때문에, 중단/재개/취소등과 같은 계산의 동작과 결과의 취합이 용이해 진다.
h1. 몇몇 예제들
h2. MD5 해시
h1. 참고문허
Bradley T. Hughes: "You’re doing it wrong…":http://labs.qt.nokia.com/2010/06/17/youre-doing-it-wrong/, Qt Labs blogs, 2010-06-17
- Bradley T. Hughes: "Threading without the headache":http://labs.qt.nokia.com/2006/12/04/threading-without-the-headache/, Qt Labs blogs, 2006-12-04