Threads Events QObjects/ko: Difference between revisions
AutoSpider (talk | contribs) m (AutoSpider moved page Threads Events QObjects Korean to Threads Events QObjects/ko: Localisation) |
(원문 텍스트 제거 및 문서형식 갈무리.) |
||
(One intermediate revision by one other user not shown) | |||
Line 1: | Line 1: | ||
{{ | {{LangSwitch}} | ||
''STILL WORKING ON'' | ''STILL WORKING ON'' | ||
Line 16: | Line 12: | ||
= 서론 = | = 서론 = | ||
<blockquote>어! 잘못하고 있는데요!. — Bradley T. Hughes | <blockquote>어! 잘못하고 있는데요!. | ||
— Bradley T. Hughes | |||
</blockquote> | </blockquote> | ||
"#qt IRC channel":irc://irc.freenode.net/#qt 에서 가장 많이 논의되는 주제중 하나가 쓰레딩과 관련한 것이다. 많은 사람들이 채널에 들어와 자신들의 문제를 "다른 쓰레드에서 행되는 코드"를 가지고 해결하려하고 이것에 대해 질문한다. | "#qt IRC channel":irc://irc.freenode.net/#qt 에서 가장 많이 논의되는 주제중 하나가 쓰레딩과 관련한 것이다. 많은 사람들이 채널에 들어와 자신들의 문제를 "다른 쓰레드에서 행되는 코드"를 가지고 해결하려하고 이것에 대해 질문한다. | ||
Line 22: | Line 19: | ||
이 사람들의 코드를 들여다 보고 빨리 알아 챌 수 있는 문제들중 열에 아홉은 우선 쓰레드를 쓰고보자는 식으로 접근하고 나서 병렬 프로그래밍의 끝없는 해악에 빠져버린다는 것이다. | 이 사람들의 코드를 들여다 보고 빨리 알아 챌 수 있는 문제들중 열에 아홉은 우선 쓰레드를 쓰고보자는 식으로 접근하고 나서 병렬 프로그래밍의 끝없는 해악에 빠져버린다는 것이다. | ||
쓰레드의 생성과 실행이 | 쓰레드의 생성과 실행이 Qt에서 쉬워진 고로 프로그래밍 스타일(특히 비동기 네트워크 프로그래밍이나 Qt의 시그널 슬롯 아키텍쳐)에 대한 지식 없이 혹은 다른 툴킷이나 언어를 사용할때 생긴 버릇 등과 버무려져 사람들은 종종 잘못된 길로 들어서게 되는 경우가 많다. 더군다나 Qt의 쓰레드관련 지원은 양날의 검과도 같다. 멀티쓰레딩의 구현이 아주 쉽게 되었지만 꼭 알고 사용해야 하는 사항들이 상당수 추가되어 있기 때문이다(특히 QObject 객체들과 상호작용을 하게 되는 경우). | ||
이 문서는 쓰레드를 어떻게 쓰는지, 적합한 locking방법은 무엇인지, 병렬성(parallelism)의 활용은 어떻게 하는 것인지, 확장성 있는 프로그램을 작성하는 방법은 무엇인지를 말하기 위한 것이 '''아니다'''. 이 주제들에 대한 많은 책/문서들이 이미 많다(이를 테면 이 [http://doc.qt.nokia.com/latest/threads.html page] 와 같은 문서들). 대신 | 이 문서는 쓰레드를 어떻게 쓰는지, 적합한 locking방법은 무엇인지, 병렬성(parallelism)의 활용은 어떻게 하는 것인지, 확장성 있는 프로그램을 작성하는 방법은 무엇인지를 말하기 위한 것이 '''아니다'''. 이 주제들에 대한 많은 책/문서들이 이미 많다(이를 테면 이 [http://doc.qt.nokia.com/latest/threads.html page]와 같은 문서들). 대신 이 작은 문서를 통해 사용자들로 하여금 Qt 4에서의 쓰레딩을 소개하여 가장 일반적인 문제점들을 짚고 견고한 동시에 나은 구조를 가지는 코드를 개발할 수 있도록 돕고자 한다. | ||
== 필요사항 == | == 필요사항 == | ||
<blockquote>이렇게 생각해봐요. 쓰레드는 소금과 같지만 | <blockquote>이렇게 생각해봐요. 쓰레드는 소금과 같지만 파스타 같지는 않죠. 소금은 당신도 좋아하고 나도 좋아하고 우리 모두 좋아하지만 사실 파스타를 더 많이 먹죠. | ||
— 래리 맥보이(Larry McVoy) | — 래리 맥보이(Larry McVoy) | ||
</blockquote> | </blockquote> | ||
쓰레드 프로그래밍에 대한 범용 소개문서로 | 쓰레드 프로그래밍에 대한 범용 소개문서로 다음과 같은 사전 지식을 필요로 한다. | ||
* C++ 기초 (사실, C++ 뿐만 아니라 많은 다른 언어들과 상관이 있다); | * C++ 기초 (사실, C++ 뿐만 아니라 많은 다른 언어들과 상관이 있다); | ||
Line 39: | Line 36: | ||
* 뮤텍스(mutex), 세마포어(semaphore) 및 대기조건(wait condition)을 사용하여 쓰레드 안전하고 재진입이 가능한 함수, 자료구조, 클래스를 만드는 법; | * 뮤텍스(mutex), 세마포어(semaphore) 및 대기조건(wait condition)을 사용하여 쓰레드 안전하고 재진입이 가능한 함수, 자료구조, 클래스를 만드는 법; | ||
본 문서에서는 | 본 문서에서는 Qt의 [http://doc.qt.nokia.com/latest/threads-reentrancy.html 명명규칙(naming convention)]인 다음의 사항을 따를것이다. | ||
* '''재진입''' 한번에 최대 하나의 스레드에서 동일 인스턴스에 접근하는 경우를 가정하여 | * '''재진입''' 한번에 최대 하나의 스레드에서 동일 인스턴스에 접근하는 경우를 가정하여 하나 이상의 쓰레드로 부터 어떤 클래스의 인스턴스를 사용하는 것이 안전하다면 그 클래스 재진입이 가능한 클래스이다. 각각의 호출에 고유한 데이터만을 참조한 다면 하나 이상의 쓰레드에서 동시에 호출되어도 안전한 함수 역시 재진입 가능 함수이다. 다시 말해 사용자는 이 클래스/함수에서 모든 인스턴스/공유데이터에 대한 접근을 어떠한 ''외부 락킹 메커니즘(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). 코드의 관점에서 보면 어떤 객체를 위한 모든 이벤트는 [http://doc.qt.nokia.com/latest/qevent.html QEvent]의 파생클래스이며 모든 QObject 파생클래스들은 QObject::event() 가상함수를 오버라이딩하여 자신의 인스턴스들을 대상으로 들어오는 이벤트들을 핸들링할 수 있다. | |||
이벤트는 응용프로그램의 내부 및 | 이벤트는 응용프로그램의 내부 및 외부에서 모두 발생할 수 있다. 예를 들자면, | ||
* | * QKeyEvent와 QMouseEvent 객체들은 키보드 및 마우스 상호작용을 표현하며 윈도우 메니저로 부터 수신된다. | ||
* QTimerEvent 객체들은 는 자신의 타이머가 동작할 때 어떤 | * QTimerEvent 객체들은 는 자신의 타이머가 동작할 때 어떤 QObject로 보내지며 (대개는) 운영체제로 부터 오는 경우가 많다. | ||
* QChildEvent 객체는 자식이 추가/삭제되는 경우 | * QChildEvent 객체는 자식이 추가/삭제되는 경우 QObject로 보내지며 Qt 응용프로그램 내부에서 온다. | ||
이벤트와 관련하여 중요한 점은 발생하는 즉시 전달되지 않는 다는 점이다. 대신 | 이벤트와 관련하여 중요한 점은 발생하는 즉시 전달되지 않는 다는 점이다. 대신 '''이벤트 큐(event queue)'''에 추가되어 나중에 전달된다. 전달자의 역할을 하는 디스패쳐는 스스로 이 이벤트큐에 대해 순회하며 각각의 추가된 이벤트들을 전달 대상 객체로 전송하며 이를 '''이벤트 루프(event loop)'''라 한다. 개념적으로 이벤트 루프는 이렇다(Qt Quarterly 문서에 대한 위 링크를 참조). | ||
<code> | <code> | ||
Line 61: | Line 58: | ||
{ | { | ||
while (!event_queue_is_empty) | while (!event_queue_is_empty) | ||
dispatch_next_event(); | |||
wait_for_more_events(); | wait_for_more_events(); | ||
} | } | ||
</code> | </code> | ||
QCoreApplication::exec()을 실행하면 Qt의 메인 이벤트 루프로 들어간다. 이 호출은 QCoreApplication::exit() 혹은 QCoreApplication::quit()이 호출되면 루프가 종료된다. | |||
"wait_for_more_events()" 함수는 이벤트가 생성되기 전까지는 리턴되지 않는다(이것은 소위 CPU 자원을 잡아먹는 "바쁜 대기"는 아니다). 이를 놓고 생각해 볼 때 특정 시점에 이벤트들을 생성할 수 있는 모든 것은 ''외부''에 있다(모든 내부 이벤트들에 대한 전달(dispatching)은 이제 종료되어 이벤트 큐에는 더이상 전달할 지연 이벤트들이 없기 때문이다). 따라서 이벤트 루프는 다음에 의해 깨어날 수 있다. | |||
* 윈도우 매니저의 동작(키/마우스 누름, 윈도우 상호작용…등); | |||
* 소켓 동작(읽어들여야 할 데이터가 수신되었거나, 대기없이 쓰기동작이 가능해 졌거나, 새로운 접속요청이 들어오거나…등); | |||
* 타이머 (즉, 타이머가 걸리면); | |||
* 다른 쓰레드로 부터 큐에 추가(posted)된 이벤트들. | |||
* 소켓 | UNIX계열의 시스템에서는 윈도우 메니저 동작(즉 X11)은 응용프로그램으로 소켓을 통해 이루어진다(Unix Domain 혹은 TCP/IP). 따라서 클라이언트는 이를 사용해 X 서버와 통신할 수 있다. 만일 쓰레드간 이벤트 포스팅(event posting)을 내부 socketpair(2)를 통해 구현하려 한다면 이벤트 루프를 깨울 수 있는 활동은 다음의 2가지로 압축된다. | ||
* 타이머 | * 소켓; | ||
* 타이머; | |||
이 것들은 '''select(2)''' 시스템 호출이 하는 것과 정확히 일치한다. 일련의 디스크립터들을 지켜보면서 어떤 동작이 발생하거나 타임아웃(설정가능한 타임아웃)이 발생하는지를 한동안 주시하고 있는 것이다. | 이 것들은 '''select(2)''' 시스템 호출이 하는 것과 정확히 일치한다. 일련의 디스크립터들을 지켜보면서 어떤 동작이 발생하거나 타임아웃(설정가능한 타임아웃)이 발생하는지를 한동안 주시하고 있는 것이다. Qt가 필요로 하는것은 select가 반환해주는 내용을 옳바른 QEvent 파생클래스로 바꾸어 이벤트 큐에 넣는 것이다. 이제 이벤트 루프에 어떤 것들이 있는지 알겠는가? :) | ||
== 이벤트 루프 구동에 필요한 것들은? == | == 이벤트 루프 구동에 필요한 것들은? == | ||
정말 많다. 하지만 | 정말 많다. 하지만 전체적인 그림을 그려볼 수 있다면 어떤 클래스들이 있어야 하는지 추정할 수 있을 것이다. | ||
* '''위젯 페인팅(Widgets painting) 및 상호작용(interaction)''' : QWidget::paintEvent() 는 QPaintEvent 객체를 전달될 때 호출되는 함수 이며 | * '''위젯 페인팅(Widgets painting) 및 상호작용(interaction)''': QWidget::paintEvent()는 QPaintEvent 객체를 전달될 때 호출되는 함수 이며 이는 QWidget::update()를 호출함으로 (내부적으로) 생성된다. 이처럼 다른 모든 종류의 상호작용(키보드, 마우스 등)도 이벤트 루프가 있어야 전달될 수 있다. | ||
* '''타이머(Timer)''': 간단히 말해 | * '''타이머(Timer)''': 간단히 말해 select(2) 혹은 이와 유사한 호출에서 타임아웃이 발생하면 이 이벤트가 발생한다. 따라서 Qt로 하여금 이러한 호출을 우리를 대신해 이벤트 루프로 반환하도록만 하면 된다. | ||
* '''네트워킹(Networking)''' : 모든 저수준 Qt 네트워킹 클래스들(QTcpSocket, QUdpSocket, QTcpServer | * '''네트워킹(Networking)''': 모든 저수준 Qt 네트워킹 클래스들(QTcpSocket, QUdpSocket, QTcpServer 등)은 비동기적으로 설계되었다. read()를 호출하면 읽어들일 수 있는 만큼만 읽고 반환된다. write() 역시 나중에 쓰기동작을 예약만 한다. 유의할 점은 동기 메소드(synchronous method)들이 제공된다는 점이다(waitFor* 계열의 메소드들). 하지만 이것들을 사용하게 되면 대기시 이벤트 루프가 먹통이 되기 때문에 곤란하다. QNetworkAcessManager와 같은 고수준 클래스들은 그 어떤 동기 API를 제공하지 않으며 이벤트 루프를 필요로 한다. | ||
== 이벤트 루프의 블록킹 == | == 이벤트 루프의 블록킹 == | ||
왜 '''절대로 이벤트 루프를 블록킹하면 안되는지''' 에 대해 논의하기 전에 | 왜 '''절대로 이벤트 루프를 블록킹하면 안되는지'''에 대해 논의하기 전에 이 "블록킹"의 의미에 대해 알아보기로 하자. 만일 어떤 버튼 위젯이 있는 클릭했을 때 시그널을 발생하도록 되어있다고 치자. 이 시그널에 연결된 다수의 작업을 진행하는 작업 객체의 슬롯이 있다면 버튼을 누르고 나면 스택 트레이스는 다음과 같은 것이다(스택은 아래 방향으로 증가한다고 가정). | ||
# main(int, char | # main(int, char *) | ||
# QApplication::exec() | # QApplication::exec() | ||
# […] | # […] | ||
# QWidget::event(QEvent | # QWidget::event(QEvent *) | ||
# Button::mousePressEvent(QMouseEvent | # Button::mousePressEvent(QMouseEvent *) | ||
# Button::clicked() | # Button::clicked() | ||
# […] | # […] | ||
# Worker::doWork() | # 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 객체는 여전히 큐에 머물게 된다), '''타이머는 발생되지 않으며''', '''네트워크 통신도 느려지고 수신/전송 큐가 차다가 시간이 지나면 멈추게 된다'''. 더구나 대부분의 윈도우 매니저들은 사용자의 응용프로그램이 더이상 이벤트를 처리하지 않는 것을 검출하여 사용자에게 '''응용프로그램이 응답하지 않음'''을 알려준다. 이 때문에 이벤트에 반응하여 가능한 최대한 빨리 이벤트 루프로 반환되도록 하는 것이 중요하다. | |||
이벤트 전송이 멈추게 되면, '''모든 위젯이 화면 갱신을 멈추며''' (QPaintEvent 객체는 여전히 큐에 머물게 된다), '''타이머는 발생되지 않으며''', '''네트워크 통신도 느려지고 | |||
== 이벤트 강제 디스패칭(Forcing event dispatching) == | == 이벤트 강제 디스패칭(Forcing event dispatching) == | ||
그럼 | 그럼 오랜 시간이 걸리는 작업을 하면서도 이벤트 루프가 블록킹되지 않도록 하려면 어떻게 해야 할까? 한가지 가능한 해답은 이 작업을 다른 쓰레드로 옮기는 것이다. 다음 섹션에서 어떻게 하면 되는지 알게 될 것이다. 또 다른 방법은 강제로 이벤트 루프가 실행되도록 하는 것이다. 이는 (반복적으로) QCoreApplication::processEvents()를 블록킹하고 있는 작업 내에서 호출하는 것이다. 이 함수는 이벤트 큐에 있는 모든 이벤트를 처리하고 난 다음에서야 호출자(즉, 작업 객체의 작업 함수)로 반환된다. | ||
또 다른 방법은 [http://doc.qt.nokia.com/latest/qeventloop.html QEventLoop] 클래스를 사용하여 강제로 이벤트 루프로 재진입하는 것이다. QEventLoop::exec() 를 호출하여 | 또 다른 방법은 [http://doc.qt.nokia.com/latest/qeventloop.html QEventLoop] 클래스를 사용하여 강제로 이벤트 루프로 재진입하는 것이다. QEventLoop::exec()를 호출하여 QEventLoop::quit() 슬롯에 시그널을 연결하여 동작을 중간에 멈추게 할 수 있다. | ||
< | <code> | ||
QNetworkAccessManager qnam; | QNetworkAccessManager qnam; | ||
QNetworkReply | QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(…))); | ||
QEventLoop loop; | QEventLoop loop; | ||
QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit())); | QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit())); | ||
loop.exec(); | loop.exec(); | ||
/ | /* reply has finished, use it */ | ||
<code> | </code> | ||
QNetworkReply는 블록킹 API 를 제공하지 않으며 이벤트 루프가 돌고 있어야만 동작을 한다. 임시로 만든 QEventLoop로 들어간 후 응답이 완료되면 이 이벤트 루프가 종료된다. | |||
이벤트 루프로 재진입할 때는 "샛길로 빠지는" 경우를 조심해야 한다. 원치 않는 재귀 호출이 일어나기 때문이다. 만일 | 이벤트 루프로 재진입할 때는 "샛길로 빠지는" 경우를 조심해야 한다. 원치 않는 재귀 호출이 일어나기 때문이다. 만일 QCoreApplication::processEvents()를 doWork() 슬롯 내에서 호출하면 사용자는 버튼을 또 누를 수 있게 될 것이고 그렇게 되면 doWork() 슬롯이 '''또 다시''' 호출되기 때문이다. | ||
# main(int, char | # main(int, char *) | ||
# QApplication::exec() | # QApplication::exec() | ||
# […] | # […] | ||
# QWidget::event(QEvent | # QWidget::event(QEvent *) | ||
# Button::mousePressEvent(QMouseEvent | # Button::mousePressEvent(QMouseEvent *) | ||
# Button::clicked() | # Button::clicked() | ||
# […] | # […] | ||
# Worker::doWork() // '''맨 처음의 내부 호출''' | # Worker::doWork() // '''맨 처음의 내부 호출''' | ||
# QCoreApplication::processEvents() // '''수동으로 이벤트를 전달하게 | # QCoreApplication::processEvents() // '''수동으로 이벤트를 전달하게 되어…''' | ||
# […] | # […] | ||
# QWidget::event(QEvent * ) // '''다른 마우스 클릭이 버튼으로 또 | # QWidget::event(QEvent *) // '''다른 마우스 클릭이 버튼으로 또 전달되어…''' | ||
# Button::mousePressEvent(QMouseEvent *) | # Button::mousePressEvent(QMouseEvent *) | ||
# Button::clicked() // '''clicked() 시그널이 또 | # Button::clicked() // '''clicked() 시그널이 또 발생하고…''' | ||
# […] | # […] | ||
# Worker::doWork() // '''아앗! 슬롯으로 또 들어와 버린다 | # Worker::doWork() // '''아앗! 슬롯으로 또 들어와 버린다.''' | ||
이 문제에 대한 아주 간단한 해결방법은 QEventLoop::ExcludeUserInputEvents 값을 QCoreApplication::processEvents() 로 전달하는 것이다. 이렇게 하면 | 이 문제에 대한 아주 간단한 해결방법은 QEventLoop::ExcludeUserInputEvents 값을 QCoreApplication::processEvents()로 전달하는 것이다. 이렇게 하면 사용자의 입력과 관련한 이벤트들은 전달되지 않는다(그냥 큐에 남아있는다). | ||
다행스럽게도 | 다행스럽게도 이와 같은 문제가 '''삭제(deletion) 이벤트'''(QObject::deleteLater()에 의해 큐에 들어간 이벤트)에 대해서는 적용되지 '''않는다'''(다행스럽게도 삭제가 이루어질 것이다). | ||
< | <code> | ||
QObject *object = new QObject; | QObject *object = new QObject; | ||
object->deleteLater(); | object->deleteLater(); | ||
QDialog dialog; | QDialog dialog; | ||
dialog.exec(); | dialog.exec(); | ||
<code> | </code> | ||
위 코드는 길잃은 포인터(dangling pointer) 문제를 유발하지 '''않는다'''(QDialog::exec()에 의해 들어간 이벤트 루프는 deleteLater 호출 보다 더 안쪽에 있다). 유사한 내용이 QEventLoop로 시작된 임시 이빈트 루프에도 적용될 수 있다. 눈여겨 볼 점은 (Qt 4.7.3버젼 기준) 이벤트 루프가 실행되지 않늘 때 deleteLater를 호출하게 되면, 맨 처음 이벤트 루프로 들어갈 때 이 이벤트가 수집되어 객체를 삭제한다는 것이다. Qt가 그 어떤 최종적으로 삭제를 수행하는 "외부" 루프에 대해 모르기 때문에 객체의 삭제는 곧바로 일어나게 되므로 꽤 논리적이라 할 수 있다. | |||
위 코드는 길잃은 포인터(dangling pointer) 문제를 유발하지 '''않는다'''(QDialog::exec()에 의해 들어간 이벤트 루프는 deleteLater 호출 보다 더 안쪽에 있다). 유사한 내용이 | |||
. | . | ||
Line 166: | Line 160: | ||
— 앤론 콕스(Alan Cox) | — 앤론 콕스(Alan Cox) | ||
</blockquote> | </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 QThread] 는 | [http://doc.qt.nokia.com/latest/qthread.html QThread]는 Qt 쓰레드 지원의 핵심이 되는 저수준 클래스다. QThread 객체는 하나의 쓰레드 실행을 표현한다. Qt가 cross-platform 이기 때문에, QThread는 모든 플랫폼 종속적인 코드들을 숨긴 상태로 서로 다른 운영체제의 사용이 가능하다. | ||
QThread를 사용해 코드를 어떤 쓰레드에서 실행하려면 | QThread를 사용해 코드를 어떤 쓰레드에서 실행하려면 먼저 이 클래스를 상속 받아 QThread::run() 메소드를 오버라이드 해야한다. | ||
< | <code> | ||
class Thread : public QThread { | class Thread : public QThread { | ||
protected: | protected: | ||
void run() { | void run() { | ||
/* 여기 사용자의 실행코드가 들어간다. */ | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
그런 다음 아래와 같이 | 그런 다음 아래와 같이 | ||
<code> | |||
Thread *t = new Thread; | |||
t->start(); /* run()이 아니다! */ | |||
</code> | </code> | ||
하여 새로운 쓰레드를 시작할 수 있다. Qt 4.4 부터는 | 하여 새로운 쓰레드를 시작할 수 있다. Qt 4.4 부터는 QThread가 더이상 가상 클래스가 아님에 유의하자. 가상 메소드 QThread::run()은 내부적으로 QThread::exec()을 호출하여 ''쓰레드의 이벤트 루프''를 시작하도록 되었다. | ||
== | == QRunnable과 QThreadPool == | ||
[http://doc.qt.nokia.com/latest/qrunnable.html QRunnable] 은 경량의 추상 클래스이며 "실행 한 다음 잊어 버리기" 방식으로 임의 작업을 시작하는 데 사용될 수 있다. 이를 위해서 모든 | [http://doc.qt.nokia.com/latest/qrunnable.html QRunnable]은 경량의 추상 클래스이며 "실행 한 다음 잊어 버리기" 방식으로 임의 작업을 시작하는 데 사용될 수 있다. 이를 위해서 모든 QRunnable의 파생클래스는 순수 가상 메소드 run()를 오버라이딩 해야 한다. | ||
< | <code> | ||
class Task : public QRunnable { | class Task : public QRunnable { | ||
public: | public: | ||
void run() { | void run() { | ||
/* 사용자의 실행할 구현이 여기에 온다 */ | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
실제로 QRunnable 객체를 실행하기 위해서는 | 실제로 QRunnable 객체를 실행하기 위해서는 쓰레드의 풀(pool)을 관리하는 [http://doc.qt.nokia.com/latest/qthreadpool.html QThreadPool] 클래스를 사용해야 한다. QThreadPool::start(runnable) 식으로 호출하여 QRunnable 객체를 QThreadPool의 실행큐에 집어 넣는다. 어떤 쓰레드라도 사용가능한 상태가 되자마자 QRunnable 객체는 쓰레드로 전달되어 실행된다. 모든 Qt 응용프로그램은 전역 쓰레드 풀을 가지고 있으며 QThreadPool::globalInstance()을 호출해 이를 얻을 수 있지만 언제라도 QThread 인스턴스를 직접 만들어 명시적으로 사용할 수 있다. | ||
주의 할 | 주의 할 점으로 QRunnable은 QObject 파생 클래스가 아니므로 시그널/슬롯을 사용할 수 없고 다른 컴포넌트와 무언가를 주고 받기 위해서는 저수준 쓰레딩 프리미티브(결과를 수집하기 위한 뮤텍스에 의해 보호되는 큐와 같은)을 사용해 직접 통신코드를 구현해야 한다. | ||
== QtConcurrent == | == QtConcurrent == | ||
[http://doc.qt.nokia.com/latest/threads-qtconcurrent.html QtConcurrent] 은 고수준 API 로서 | [http://doc.qt.nokia.com/latest/threads-qtconcurrent.html QtConcurrent]은 고수준 API 로서 QThreadPool 위에 구현되었으며 대부분의 [http://en.wikipedia.org/wiki/Map_(higher-order_function map]), [http://en.wikipedia.org/wiki/Fold_(higher-order_function reduce]), 및 [http://en.wikipedia.org/wiki/Filter_(higher-order_function filter])와 같은 병렬 컴퓨팅 패턴을 다루는 데 있어 유용하다. 이 역시 QtConcurrent::run() 메소드를 통해서 다른 쓰레드에서 임의 함수를 쉽게 실행 할 수 있다. | ||
QThread나 QRunnable과는 달리 QtConcurrent는 그 어떤 저수준 동기화 객체를 사용할 필요가 없다. 대신 모든 QtConcurrent 메소드들은 [http://doc.qt.nokia.com/latest/qfuture.html QFuture]을 반환하는데 이를 사용하여 작업 상황(진행 상태)를 질의할 수 있으며 진행 중인 작업을 pause/resume/cancel 할 수 있다. 또한 이것은 작업의 ''결과''도 포함하고 있다. | |||
== 기능 비교 == | == 기능 비교 == | ||
Line 220: | Line 216: | ||
!QThread | !QThread | ||
!QRunnable | !QRunnable | ||
!QtConcurrent<ref>QtConcurrent:: | !QtConcurrent<ref>QtConcurrent::run은 예외. QRunnable 을 사용해 구현되었으므로 장/단점이 있다. | ||
</ref> | </ref> | ||
|- | |- | ||
Line 253: | Line 249: | ||
== 쓰레드 별 이벤트 루프 == | == 쓰레드 별 이벤트 루프 == | ||
지금까지 우리는 "''소위'' 이벤트 루프 라는 것" 에 대해 얘기 했었다. Qt 응용프로그램에는 오직 하나의 이벤트 루프만이 있는 것처럼 여겨지는 경우가 많다. 하지만 | 지금까지 우리는 "''소위'' 이벤트 루프 라는 것"에 대해 얘기 했었다. Qt 응용프로그램에는 오직 하나의 이벤트 루프만이 있는 것처럼 여겨지는 경우가 많다. 하지만 실제로는 아니다. QThread 객체는 자기 자신만의 이벤트 루프를 자신의 쓰레드에서 실행한다. 그러므로 '''메인 이벤트 루프'''라고 하는 것만이 main()이 호출되면서 생성된것이고 QCoreApplication::exec()에 의해서 시작된다고 할 수 있다. 이는 다른 말로 '''GUI 쓰레드'''라고도 하는데 이는 GUI와 관련한 작업이 허용되는 유일한 쓰레드이기 때문이다. 쓰레드별 이벤트 루프는 QThread::exec()에 의해 실행된다. | ||
< | <code> | ||
class Thread : public QThread { | class Thread : public QThread { | ||
protected: | protected: | ||
void run() { | void run() { | ||
/* … 초기화 작업 … */ | |||
exec(); | exec(); | ||
} | } | ||
}; | }; | ||
<code> | </code> | ||
앞서 언급한 바 | 앞서 언급한 바 Qt 4.4 부터는 QThread::run()가 더 이상 순수 가상 메소드가 아니라 QThread::exec()을 실행하는 보통 메소드가 되었다. | ||
쓰레드의 이벤트 루프는 이벤트를 그 쓰레드에서 '''운용되는(혹은 살아가는)''' 모든 QObject 객체들에 전달된다. 여기에는 디폴트로 그 쓰레드에서 생성된 모든 객체와 다른 쓰레드에서 생성되었다가 옮겨진 객체가 포함된다(옮겨지는 경우에 대해서는 아래에서 좀 더 상세한 정보를 다룬다). 어떤 QObject 객체의 '''쓰레드 친화도(thread affinity)'''가 특정 쓰레드다..라고 하기도 하는데 이는 이 객체가 그 쓰레드에서 살아가고 있다는 것을 의미한다. 이는 QThread 객체의 생성자에서 생성된 객체들에 적용된다. | |||
쓰레드의 이벤트 루프는 이벤트를 그 쓰레드에서 '''운용되는(혹은 살아가는)''' 모든 QObject 객체들에 전달된다. 여기에는 디폴트로 그 쓰레드에서 생성된 모든 객체와 다른 쓰레드에서 생성되었다가 옮겨진 객체가 포함된다(옮겨지는 경우에 대해서는 아래에서 좀 더 상세한 정보를 다룬다). 어떤 QObject 객체의 '''쓰레드 친화도(thread affinity)''' 가 특정 쓰레드다.. 라고 하기도 하는데 | |||
< | <code> | ||
class MyThread : public QThread | class MyThread : public QThread | ||
{ | { | ||
Line 277: | Line 272: | ||
MyThread() | MyThread() | ||
{ | { | ||
otherObj = new QObject; | |||
} | } | ||
Line 285: | Line 280: | ||
QScopedPointer<QObject> yetAnotherObj; | QScopedPointer<QObject> yetAnotherObj; | ||
}; | }; | ||
<code> | </code> | ||
위에서 MyThread 객체를 생성한 다음 obj, otherObj 및 yetAnotherObj 객체 각각의 쓰레드 친화도라는 것이 어떤 의미일까? 각각을 생성하고 있는 쓰레드에 주목해야 한다. 이는 MyThread 생성자를 실행하는이 된다. 따라서 이 3개의 객체는 모두 MyThread 쓰레드에서 '''살아가지 않는다'''. 대신 MyThread 인스턴스를 생성한 쓰레드에서 '''살아간다'''(그런데 이 MyThread 객체 자체도 자신을 생성한 쓰레드에서 살아간다고 할 수 있다!). | |||
위에서 | |||
어떤 QObject 객체의 쓰레드 친화도는 언제라도 QObject::thread()를 호출하여 질의할 수 있다. QCoreApplication 객체보다 앞서 생성된 그 어떤 QObject 객체들은 '''그 어떤 쓰레드 친화도도 없다'''. 그리고 이런 이유로 이들 객체에는 그 어떤 이벤트도 전달되지 않는다(다시 말해 QCoreApplication은 메인 쓰레드를 담당하는 QThread 객체를 직접 구축한다). | |||
어떤 QObject 객체의 쓰레드 친화도는 언제라도 QObject::thread() 를 호출하여 질의할 수 있다. QCoreApplication 객체보다 앞서 생성된 그 어떤 QObject 객체들은 '''그 어떤 쓰레드 친화도도 없다'''. 그리고 | |||
[[Image:http://doc.qt.nokia.com/4.7/images/threadsandobjects.png|http://doc.qt.nokia.com/4.7/images/threadsandobjects.png]] | [[Image:http://doc.qt.nokia.com/4.7/images/threadsandobjects.png|http://doc.qt.nokia.com/4.7/images/threadsandobjects.png]] | ||
쓰레드 안전한 QCoreApplication::postEvent()를 사용해 이벤트를 임의 객체의 이벤트 규에 추가(posting)할 수 있다. 이렇게 하면 이벤트를 객체가 살아가는 쓰레드의 이벤트 루프에 집어넣게 된다. 하지만 쓰레드가 이벤트 루프를 실행하고 있지 않다면 이벤트는 전달되지 않을 것이다. | |||
쓰레드 안전한 QCoreApplication::postEvent() 를 사용해 이벤트를 임의 객체의 이벤트 규에 추가(posting)할 수 있다. 이렇게 하면 이벤트를 객체가 살아가는 쓰레드의 이벤트 루프에 집어넣게 된다. 하지만 | |||
Object 및 이의 파생클래스들은 '''쓰레드 안전하지 않다'''는 점을 반드시 이해하여야 한다. 따라서 객체의 내부 데이터로의 접근을 직렬화 하지 않는다면(이를 테면, 뮤텍스로 보호하는…) 하나 이상의 쓰레드로 부터 QObject 객체로 접근해서는 안된다. 다른 쓰레드로 부터 객체에 접근하는 동안 객체가 자신이 살아가는 쓰레드의 이벤트 루프에 의해 전달된 이벤트를 처리할 수 도 있음을 반드시 기억해야 한다! 같은 이유로 객체가 살아가는 쓰레드 이외의 다른 쓰레드에서 객체를 할당해제 할 수 없다. 이를 위해서는 QObject::deleteLater()을 호출해서 객체가 살아가는 쓰레드로 할당 해제에 관한 이벤트를 큐에 집어넣고(posting) 최종적으로 자신의 쓰레드에서 할당해제가 이루어지도록 해야만 한다. | |||
Object 및 이의 파생클래스들은 | |||
더구나 QWidget 및 이의 모든 파생클래스는 다른 GUI 관련 클래스들(심지어 QObject 기반이 아닌 QPixmap과 같은 클래스들)과 마찬가지로 '''재진입가능하지 않다'''. 이들은 오직 GUI 쓰레드에서만 배타적으로 사용되어져야만 한다. | |||
더구나 | |||
QObject의 진화도는 QObject::moveToThread()를 호출하여 수정할 수 있다. 이렇게 하면 객체 자신 뿐 아니라 자식들 까지 자신이 살아갈 쓰레드를 바꾸게 된다. 즉, 이 객체들을 자신이 살아가는 쓰레드로 부터 전혀 다른 쓰레드로 '''집어넣는''' 것이지만 그 반대로 다른 쓰레드로 부터 '''끌어오는'''일은 불가능 하다. 더구나 Qt는 QObject의 자식들이 부모가 살아가는 쓰레드와 동일한 쓰레드에서 살아가도록 하는 것을 요구한다. 이는 암묵적으로 다음을 의미한다. | |||
QObject의 진화도는 QObject::moveToThread() 를 호출하여 수정할 수 있다. 이렇게 하면 | |||
* QObject::moveToThread()를 부모를 가지고 있는 객체에 대해 사용할 수 없다; | * QObject::moveToThread()를 부모를 가지고 있는 객체에 대해 사용할 수 없다; | ||
* QThread 객체 자신을 부모로 하는 객체를 QThread 내에서 만들 수 없다. | * QThread 객체 자신을 부모로 하는 객체를 QThread 내에서 만들 수 없다. | ||
< | <code> | ||
class Thread : public QThread { | class Thread : public QThread { | ||
void run() { | void run() { | ||
QObject *obj = new QObject(this); // 잘못됨[[Image:|Image:]]! | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
그 이유는 '''QThread 객체는 다른 쓰레드에서 살아가고 있기 때문'''이다. 즉 자신을 생성한 쓰레드말이다. | |||
그 이유는 '''QThread 객체는 다른 쓰레드에서 살아가고 있기 때문 | |||
Qt에서는 QThread 객체가 파괴된기 전에 이 객체가 나타내는 쓰레드에서 살아가던 모든 객체들이 파괴되는 것을 요구한다. 이는 QThread::run() 메소드의 스택에서 그 쓰레드에서 살아가는 모든 객체들을 생성하기만 하면 쉽게 구현된다. | |||
== 쓰레드간의 시그널과 슬롯 == | == 쓰레드간의 시그널과 슬롯 == | ||
어떻게 하면 다른 쓰레드에서 살아가는 QObject 객체들의 메소드를 호출할 수 있을까? Qt는 깔끔한 해결책을 제공하고 있다. 즉, 처리되기 원하는 쓰레드의 이벤트 큐에 이벤트를 추가하고(posting) 그 쓰레드에서 이벤트가 처리될 때 원하는 메소드가 호출되도록 하는 것이다(이렇게 하려면 해당 쓰레드의 QThread가 이벤트 루프를 실행하고 있어야만 한다). 이는 moc에 의해 자동 생성되는 메소드로 구현되기 때문에 오직 시그날과 슬롯 및 Q_INVOKABLE 매크로로 표기된 메소들만이 다른 쓰레드에서 호출가능하게 된다(역자주: Qt 4.x기준으로 QMetaCallEvent 라는 유형의 이벤트가 사용되고 있다). | |||
어떻게 하면 다른 쓰레드에서 살아가는 QObject 객체들의 메소드를 호출할 수 있을까? | |||
QMetaObject::invokeMethod() 정적 메소드는 이런일을 가능하게 해 준다. | QMetaObject::invokeMethod() 정적 메소드는 이런일을 가능하게 해 준다. | ||
< | <code> | ||
QMetaObject::invokeMethod(object, "methodName", | QMetaObject::invokeMethod(object, "methodName", | ||
Qt::QueuedConnection, | Qt::QueuedConnection, | ||
Q_ARG(type1, arg1), | Q_ARG(type1, arg1), | ||
Q_ARG(type2, arg2)); | Q_ARG(type2, arg2)); | ||
<code> | </code> | ||
이벤트가 처리되는 과정에서 암시적으로 위 함수의 인자들이 복사되어질 필요가 있음에 유의하자. 따라서 이 인자들의 형(type)은 Qt의 형 체계(type system)하에서 qRegisterMetaType()을 사용해 공용 복사생성자, 공용 소멸자 및 공용 복사생성자가 반드시 등록되어져 있어야 한다. | |||
이벤트가 처리되는 과정에서 암시적으로 위 함수의 인자들이 복사되어질 필요가 있음에 유의하자. 따라서 | |||
쓰레드간 시그널 슬롯 역시 이와 유사한 방식으로 동작한다. 어떤 시그널을 슬롯에 연결할 때 QObject::connect 함수의 5번째 인자는 연결유형(connection type)의 지정에 사용된다. | |||
쓰레드간 시그널 슬롯 역시 이와 유사한 방식으로 동작한다. 어떤 시그널을 슬롯에 연결할 때 | |||
* '''직접 연결(direct connection)'''은 시그널이 발생되어진 쓰레드에서 항상 직접 슬롯을 호출하는 것을 의미한다(슬롯의 실행쓰레드는 시그널 발생 쓰레드); | |||
* '''큐 연결(queued connection)'''은 수신측 쓰레드의 이벤트 큐에 이벤트가 추가되고 나중에 이벤트 루프에서 빠질 때 슬롯이 호출되는 것을 의미한다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드); | |||
* '''블록킹 큐 연결(blocking queued connection)'''은 큐 연결 유형과 유사하지만 시그널 발생측이 슬롯의 호출종료시까지 블록킹되는 것이 다르다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드); | |||
* '''자동 연결(automatic connection)'''(''기본'') 은 슬롯이 시그널 발생측과 같은 쓰레드면 직접 연결되고 그 외의 경우에는 큐 연결이 사용된다(슬롯의 실행쓰레드는 시그널 발생시 결정된다). | |||
* '''직접 연결(direct connection)''' 은 시그널이 발생되어진 쓰레드에서 항상 직접 슬롯을 호출하는 것을 의미한다(슬롯의 실행쓰레드는 시그널 발생 쓰레드) | |||
* '''큐 연결(queued connection)''' 은 수신측 쓰레드의 이벤트 큐에 이벤트가 추가되고 나중에 이벤트 루프에서 빠질 때 슬롯이 호출되는 것을 의미한다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드) | |||
* '''블록킹 큐 연결(blocking queued connection)''' 은 | |||
그 어떤 경우에라도 유념해야 할 사항은 ''시그널을 발생시키는 객체가 살고 있는 쓰레드''는 전혀 중요하지 않다는 점이다! 자동 연결의 경우에도 Qt는 시그널을 실행하는 쓰레드를 기준으로 이를 수신측 객채가 살고 있는 쓰레드와 비교하여 어떤 연결 유형을 쓸 것인지를 결정한다. [http://doc.qt.nokia.com/4.7/threads-qobject.html current Qt documentation] (4.7.1) 문서에 나와 있는 내용은 잘못되어있으므로 주의해야 한다. | |||
그 어떤 경우에라도 유념해야 할 사항은 | |||
''자동 연결(auto connection, 기본)유형의 동작은 시그널 발생측과 수신측이 동일 쓰레드이면(즉, 쓰레드 친화도가 동일하다면) 직접 연결(direct connection)의 동작과 동일하지만 다른 쓰레드라면 큐 연결(queued connection)과 같아진다.'' | |||
''자동 연결(auto connection, | |||
위의 문장이 잘못된 이유는 시그널 발생 객체의 쓰레드 친화도는 동작의 차이와는 아무 상관이 없기 때문이다. 예를 들면 | |||
위의 문장이 잘못된 이유는 | |||
< | <code> | ||
class Thread : public QThread | class Thread : public QThread | ||
{ | { | ||
Line 374: | Line 348: | ||
protected: | protected: | ||
void run() { | void run() { | ||
emit aSignal(); | |||
} | } | ||
}; | }; | ||
Line 381: | Line 355: | ||
Thread thread; | Thread thread; | ||
Object obj; | Object obj; | ||
QObject::connect(&thread, SIGNAL (aSignal()), &obj, SLOT (aSlot())); | QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot())); | ||
thread.start(); | thread.start(); | ||
<code> | </code> | ||
위에서 aSignal() 시그널은 Thread 객체로 표현되는 새로운 쓰레드에서 발생하게 된다. 이 쓰레드 자체는 Object형의 obj객체가 살아가는 쓰레드가 아니다(한편 thread 객체의 쓰레드 친화도와는 상관이 없다는 점을 강조하자면 이 쓰레드는 Thread형의 thread 객체가 살아가는 쓰레드이며 이는 친화도를 가지는 쓰레드와는 다르다). 따라서 '''큐 연결(queued connection)'''이 사용된다. | |||
위에서 aSignal() 시그널은 Thread 객체로 표현되는 새로운 쓰레드에서 발생하게 된다. 이 쓰레드 자체는 Object형의 obj객체가 살아가는 쓰레드가 아니다(한편 | |||
또 다른 범하기 쉬운 오류는 다음과 같은 것이다. | 또 다른 범하기 쉬운 오류는 다음과 같은 것이다. | ||
< | <code> | ||
class Thread : public QThread | class Thread : public QThread | ||
{ | { | ||
Line 398: | Line 370: | ||
slots: | slots: | ||
void aSlot() { | void aSlot() { | ||
/* … */ | |||
} | } | ||
protected: | protected: | ||
void run() { | void run() { | ||
/* … */ | |||
} | } | ||
}; | }; | ||
/ | /* … */ | ||
Thread thread; | Thread thread; | ||
Object obj; | Object obj; | ||
QObject::connect(&obj, SIGNAL (aSignal()), &thread, SLOT (aSlot())); | QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot())); | ||
thread.start(); | thread.start(); | ||
obj.emitSignal(); | obj.emitSignal(); | ||
<code> | </code> | ||
"obj"가 aSignal() 시그널을 발생시킬 때 어떤 종류의 연결이 사용되게 될까? 한번 맞추어 보기 바란다. | "obj"가 aSignal() 시그널을 발생시킬 때 어떤 종류의 연결이 사용되게 될까? 한번 맞추어 보기 바란다. | ||
바로 '''직접 연결(direct connection)''' 이다. 그 이유는 Thread 객체는 시그널을 발생시키는 쓰레드에서 살아고 있기 때문이다. 이 경우 | 바로 '''직접 연결(direct connection)'''이다. 그 이유는 Thread 객체는 시그널을 발생시키는 쓰레드에서 살아고 있기 때문이다. 이 경우 run() 메모드에 의해 Thread의 멤버변수에 대한 접근이 이루어지는 동안 aSlot() 슬롯에서도 역시 동일 변수들에 대한 접근이 이루어지게 될 수도 있게 되며 이런 식으로 작업하면 큰 재앙으로 곧잘 이어지게 된다. | ||
또 다른 다음 예는 어쩌면 ''가장 중요한'' 것일지도 모르겠다. | 또 다른 다음 예는 어쩌면 ''가장 중요한'' 것일지도 모르겠다. | ||
< | <code> | ||
class Thread : public QThread | class Thread : public QThread | ||
{ | { | ||
Line 429: | Line 399: | ||
slots: | slots: | ||
void aSlot() { | void aSlot() { | ||
/* … */ | |||
} | } | ||
protected: | protected: | ||
void run() { | void run() { | ||
QObject *obj = new Object; | |||
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot())); | |||
/* … */ | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
이 경우 '''큐 연결(queued connection)'''이 사용되는데 그 이유는 Thread 객체가 살아가는 쓰레드(Thread가 표현하는 쓰레드. 즉, run()의 쓰레드와는 다르다)에 있는 이벤트 루프에 의해서 연결이 처리되기 때문이다. | |||
이 경우 | |||
게시판, 블로그 글 등에서 종종 볼 수 있는 답변들 가운데에는 Thread 의 생성자에 moveToThread(this)를 추가하라는 것이 많이 있다. | |||
< | <code> | ||
class Thread : public QThread { | class Thread : public QThread { | ||
Q_OBJECT | Q_OBJECT | ||
public: | public: | ||
Thread() { | Thread() { | ||
moveToThread(this); // 주의 | |||
} | } | ||
/* … */ | /* … */ | ||
}; | }; | ||
<code> | </code> | ||
위와 같이 해도 사실은 ''동작을 한다''(왜냐하면 Thread 객체의 쓰레드 친화도가 변경되기 때문이다). 하지만 이는 잘못된 설계이다. 이런 방식의 문제는 thread객체(즉, QThread 파생클래스의 객체)의 목적을 잘못 이해하고 있다는 데 있다. ''QThread 객체들은 쓰레드가 아니다''. 이들은 쓰레드를 감싸고 있는 객체들을 제어하기 위한 것이며 다른 쓰레드(대개 그 객체가 살아가는 쓰레드)로 부터 사용되어질 목적으로 존재한다. | |||
위와 같이 해도 사실은 ''동작을 한다'' | |||
'''옳바른 결과를 얻기 위한 바람직한 방법'''은 "제어부"로 부터 "동작부"를 떼어네어서 QObject의 파생클래스를 작성하고 QObject::moveToThread()를 호출하여 쓰레드 친화도를 변경하는 것이다. | |||
'''옳바른 결과를 얻기 위한 바람직한 방법 | |||
< | <code> | ||
class Worker : public QObject | class Worker : public QObject | ||
{ | { | ||
Line 472: | Line 438: | ||
public slots: | public slots: | ||
void doWork() { | void doWork() { | ||
/* … */ | |||
} | } | ||
}; | }; | ||
/ | /* … */ | ||
QThread *thread = new QThread; | QThread *thread = new QThread; | ||
Worker | Worker *worker = new Worker; | ||
connect(obj, SIGNAL (workReady()), worker, SLOT (doWork())); | connect(obj, SIGNAL(workReady()), worker, SLOT(doWork())); | ||
worker->moveToThread(thread); | worker->moveToThread(thread); | ||
thread->start(); | thread->start(); | ||
<code> | </code> | ||
== 해도 좋은 것과 해서는 안 되는 것 == | |||
=== 가능한것… === | |||
* … QThread의 서브클래스에 시그널 추가하기. 이는 완벽히 안전하면서도 "제대로" 일을 한다고 할 수 있다. (위를 보면 시그널 전송부의 쓰레드 친화도는 아무런 문제가 되지 않는 것을 알 수 있다.) | |||
=== | === 하지말것… === | ||
* … | * … moveToThread(this) 사용. | ||
* … | * … 시그널 슬롯의 연결시 연결 유형 강제 적용하기: 이런 구현은 QThread의 제어 인터페이스와 프로그램의 로직을 섞는 것과 같은(로직은 자신이 살고 있는 쓰레드와는 다른 별도의 객체에 존재하는 것이 바람직하다) 오류를 범하는 경우가 많다. | ||
* … QThread 서브클래스에 슬롯 추가하기: 호출되면 안되는 쓰레드로 부터 슬롯 호출이 이루어지게 될 수 있다. 이 허용되지 않는 쓰레드는 QThraed 객체가 관리하는 쓰레드가 아닌 다른 객체가 살아가는 쓰레드이며 QThread가 표현하는 쓰레드에서 호출이 이루어지게 하기 위해 직접 연결(direct connection)을 지정하게 되거나 moveToThread(this)를 사용하게 되고 만다. | |||
* … QThread::terminate 사용. | |||
* QThread 서브클래스에 슬롯 추가하기 : 호출되면 안되는 쓰레드로 부터 슬롯 호출이 이루어지게 될 수 있다. 이 허용되지 않는 쓰레드는 QThraed 객체가 관리하는 쓰레드가 아닌 다른 객체가 살아가는 쓰레드이며 | |||
* QThread::terminate | |||
=== 절대 | === 절대 하지말것… === | ||
* … | * … 쓰레드가 구동하고 있는 중 프로그램을 종료하는 행위: QThread::wait을 사용해 종료를 대기해야 한다. | ||
* … 자신이 관리하고 있는 쓰레드가 아직 구동중일때 이 쓰레드를 관리하던 QThread 객체를 파괴하는 행위: "자동 파괴"와 같은 기능을 원한다면 QThread::finished() 시그널을 QObject::deleteLater() 슬롯과 연결만 하면 된다. | |||
* 자신이 관리하고 있는 쓰레드가 아직 구동중일때 이 쓰레드를 관리하던 QThread 객체를 파괴하는 행위 : "자동 파괴"와 같은 기능을 원한다면 | |||
= 언제 쓰레드를 써야만 | = 언제 쓰레드를 써야만 할까? = | ||
== 쓸 수 있는 API가 블록킹 유형의 것들 밖에 없을 때 == | == 쓸 수 있는 API가 블록킹 유형의 것들 밖에 없을 때 == | ||
사용하고자 하는 라이브러리나 다른 이의 코드가 (시그널/슬롯, 이벤트, 혹은 콜백등의) 논블록킹 API 를 제공하지 않는다면 이벤트 루프의 동작 중단을 방지하기 위한 유일한 방안은 프로세스 혹은 쓰레드를 만드는 것이다. 새로운 작업 프로세스의 생성은 작업 지시와 통보등의 방법에 있어 쓰레드를 생성하는 방법보다 훨씬 어렵고 비용도 비싸므로 가능한 쓰레드를 사용하는 편이 나을 수 있다. | |||
사용하고자 하는 라이브러리나 다른 이의 코드가 (시그널/슬롯, 이벤트, 혹은 콜백등의) 논블록킹 API 를 제공하지 않는다면 | |||
이런 종류의 API 중 가장 좋은 예는 '''주소 확인(address resolution)''' API 이다(3rdparty 라이브러리로 나온 것이 아니라도 모든 C 라이브러리에 포함되어 있다). 이 API 는 호스트 이름을 가져다 주소 값으로 변환해 준다. 그 일련의 과정은 대개 원격지에 있는 시스템(Domain Name System 즉 DNS)에 대한 질의로 이루어진다. 응답이 거의 순간적으로 오기는 하겠지만 원격지 서버가 응답을 못하거나 패킷유실이 이루어지거나, 네트워크 연결이 끊어지는 듯듯의 사고가 있을 수 있다. 어쨌든 질의에 대한 응답이 오기 까지는 몇초정도가 걸린다. | |||
이런 종류의 API 중 가장 좋은 예는 '''주소 확인(address resolution)''' API 이다(3rdparty 라이브러리로 나온 것이 아니라도 | |||
UNIX 시스템에 존재하는 표준 API는 ''블록킹'' 방식이다(오래된 gethostbyname(3) 이 아니더라도 새로운 getservbyname(3)이나 getaddrinfo(3)도 마찬가지다). Qt 클래스인 [http://doc.qt.nokia.com/latest/qhostinfo.html QHostInfo]는 호스트명칭의 검색에 있어 QThreadPool을 사용하여 백그라운드로 이를 처리한다([http://qt.gitorious.com/qt/qt/blobs/master/src/network/kernel/qhostinfo.cpp 여기]를 확인). | |||
UNIX 시스템에 존재하는 표준 API는 ''블록킹'' 방식이다(오래된 gethostbyname(3) 이 아니더라도 | |||
또 다른 간단한 예는 '''이미지 로딩'''이나 '''스케일링(scaling)'''이다. [http://doc.qt.nokia.com/latest/qimagereader.html QImageReader]와 [http://doc.qt.nokia.com/latest/qimage.html QImage]는 디바이스로 부터 이미지를 읽어들이는 블록킹 메소드만 제공한다. 아주 큰 이미지들을 다루거나 하게 되면 이들 기능들은 수십초 이상의 시간이 걸리게 될 것이다. | |||
또 다른 간단한 예는 '''이미지 로딩''' 이나 '''스케일링(scaling)''' 이다. [http://doc.qt.nokia.com/latest/qimagereader.html QImageReader] 와 [http://doc.qt.nokia.com/latest/qimage.html QImage] 는 디바이스로 부터 이미지를 읽어들이는 블록킹 메소드만 제공한다. 아주 큰 이미지들을 다루거나 하게 되면 | |||
== CPU의 갯수에 따라 구현 규모를 조정하고자 할 때 == | == CPU의 갯수에 따라 구현 규모를 조정하고자 할 때 == | ||
쓰레드는 사용자의 프로그램으로 하여금 다중프로세서 시스템의 이점을 활용할 수 있게 해준다. 각각의 쓰레드가 운영체제에 의해 독립적으로 스케쥴링되므로, 응용프로그램이 이러한 다중 프로세서 시스템에서 구동된다면, 스케쥴러는 각 쓰레드를 독립적인 프로세서에서 '''동시에''' 구동될 수 있게된다. | 쓰레드는 사용자의 프로그램으로 하여금 다중프로세서 시스템의 이점을 활용할 수 있게 해준다. 각각의 쓰레드가 운영체제에 의해 독립적으로 스케쥴링되므로, 응용프로그램이 이러한 다중 프로세서 시스템에서 구동된다면, 스케쥴러는 각 쓰레드를 독립적인 프로세서에서 '''동시에''' 구동될 수 있게된다. | ||
예를 들어 일련의 이미지들에 대한 썸네일을 생성하는 응용프로그램이 있다하자. ''n''개의 쓰레드로 구성된 '''쓰레드 팜(thread farm)'''(즉, 고정된 갯수의 쓰레드로 구성된 쓰레드 풀)은 시스템에 존재하는 CPU 하나당 하나씩의 쓰레드를 가지며(QThread::idealThreadCount()를 참조) 이미지를 썸네일로 스케일 다운하는 작업을 모든 쓰레드에 분산시켜 작업할 수 있게 할 수 있다. 이런식으로 프로세서 갯수에 대해 거의 선형적인 승능향상을 얻게 된다(단순화하기 위해 CPU 자체가 병목임을 가정하면). | |||
예를 들어 | |||
== 다른 작업에 방해받지 않기를 원할 때 == | == 다른 작업에 방해받지 않기를 원할 때 == | ||
Line 535: | Line 489: | ||
음. 예를 들어 설명한다. | 음. 예를 들어 설명한다. | ||
이 주제는 조금 난해할 수도 있으므로, 일단 넘어가도 상관은 없다. 이 경우에 대한 가장 좋은 예는 QNetworkAccessManager가 WebKit에서 어떻게 사용되는지와 관련해서 이다. WebKit은 최신의 브라우저 엔지이며 웹페이지를 배치하고 표시하는 일련의 클래스들이다. WebKit을 사용하는 Qt widget은 QWebView 이다. | |||
이 주제는 조금 난해할 수도 있으므로, 일단 넘어가도 상관은 없다. 이 경우에 대한 가장 좋은 예는 QNetworkAccessManager가 | |||
QNetworkAccessmanager는 모든 HTTP 요청과 응답을 처리하는 Qt 클래스이다. 웹 브라우저의 네트워킹 엔진을 생각해 볼 수 있겠다. 현재의 설계는 그 어떤 작업 쓰레드도 사용하고 있지 않고 있다. 모든 네트워크 작업은 QNetworkAccessManager와 이의 QNetworkReply 객체들이 살아가고 있는 쓰레드와 동일한 쓰레드에서 처리된다. | |||
네트워킹과 관련하여 쓰레드를 사용하지 않는 것 자체는 매우 좋은 생각이지만 치명적인 단점이 있다. 소켓으로 부터 데이터를 최대한 빨리 읽지 않게 되면 커널 버퍼가 차게 되어 패킷이 드랍되며 전송속도가 극단적으로 줄어들 수 있다는 점이다. | |||
네트워킹과 관련하여 쓰레드를 사용하지 않는 것 자체는 매우 좋은 생각이지만 | |||
소켓의 동작(소켓으로 부터 읽어들일 데이터의 존재여부)은 Qt 의 이벤트 루프에 의해서 관리된다. 이벤트 루프를 블록킹하게 되면 읽어야할 데이터 거기 있다는 사실을 아무도 통지 받지 못헤게 되므로 전송 성능이 떨어진다. | |||
소켓의 동작(소켓으로 부터 읽어들일 데이터의 존재여부)은 Qt 의 이벤트 루프에 의해서 관리된다. 이벤트 루프를 블록킹하게 되면 | |||
하지만 이벤트 루프를 누가 블록킹하게 되는 것일까? 슬프지만 그 답은 WebKit 바로 자기 자신이다. 일련의 데이터를 수신하게 되면 WebKit은 이를 사용해 웹 페이지를 구성하기 시작한다. 불운하게도 이 구성 과정은 매우 복잡하고 비용도 크기 때문에 드문 드문 이벤트 루프를 블록킹하게 되고 결국 추가적으로 들어오는 전송에 영향을 점점 더 많이 주기 시작한다. | |||
하지만 | |||
요약하자면, 다음과 같은 일들이 벌어진다고 할 수 있다. | 요약하자면, 다음과 같은 일들이 벌어진다고 할 수 있다. | ||
* | * WebKit이 요청을 발생시킨다; | ||
* 응답으로 일련의 데이터가 도착한다; | |||
* WebKit 이 웹 페이지를 들어온 데이터를 사용해 구성하기 시작하면서 이벤트 루프가 블록킹된다; | |||
* 이벤트 루프가 돌지 않게 되면서 OS에 의해 수신되 데이터가 QNetworkAccessManager의 소켓으로 부터 읽혀지지 않게된다; | |||
* 응답으로 일련의 데이터가 도착한다 | |||
* WebKit 이 웹 페이지를 들어온 데이터를 사용해 구성하기 시작하면서 이벤트 루프가 블록킹된다 | |||
* 이벤트 루프가 돌지 않게 되면서 OS에 의해 수신되 데이터가 | |||
* 커널 버퍼가 차게되어 전송이 느려진다. | * 커널 버퍼가 차게되어 전송이 느려진다. | ||
전체적인 페이지 로딩 속도는 결국 자기 자신으로 인해 발생한 전송 속도 저하로 느려지게 된다. | 전체적인 페이지 로딩 속도는 결국 자기 자신으로 인해 발생한 전송 속도 저하로 느려지게 된다. | ||
QNetworkAccessManager와 QNetworkReply 객체들은 모두 QObject 들이다. 이들은 쓰레드 안전(thread safe)하지 않다. 따라서 단순히 이 객체들을 다른 쓰레드로 옮긴 다음 현재 쓰고 있던 쓰레드에서 사용할 수 는 없다. 왜냐하면 후자 쓰레드의 이벤트 루프에 의해 전달되어진 이벤트로 인해 두개의 쓰레드로 부터 동시에 사용될 수도 있기 때문이다. | |||
Qt 4.8 버젼부터 QNetworkAccessManager는 HTTP 요청을 기본적으로 독립적인 쓰레드에서 처리하도록 수정되었다. 따라서 GUI가 무응답하거나 OS 버퍼가 너무 빨리 차 버리는 문제가 해결되었다. | |||
Qt 4.8 버젼부터 QNetworkAccessManager는 HTTP 요청을 | |||
= 쓰레드를 사용하지 않는편이 좋은 경우는? = | = 쓰레드를 사용하지 않는편이 좋은 경우는? = | ||
Line 580: | Line 520: | ||
== 타이머 == | == 타이머 == | ||
쓰레드 남용의 가장 심각한 유형에 대해 알아보자. 만일 어떤 메소드를 반복적으로 수행해야 한다면(예를 들어 매 초마다) 대다수 사람들은 다음과 같은 구현을 떠올 릴 것이다. | |||
쓰레드 남용의 가장 심각한 유형에 대해 알아보자. 만일 어떤 메소드를 반복적으로 수행해야 한다면(예를 들어 | |||
< | <code> | ||
// 아주 잘못 된 것 | // 아주 잘못 된 것 | ||
while (condition) { | while (condition) { | ||
Line 589: | Line 528: | ||
sleep(1); // C라이브러리의 sleep(3)함수 | sleep(1); // C라이브러리의 sleep(3)함수 | ||
} | } | ||
<code> | </code> | ||
그리고 나서, 이 구현은 '''이벤트 루프를 블록킹'''하고 있다는 것을 곧 깨닫고 다음과 같이 쓰레드를 도입하기로 마음먹게 된다. | |||
그리고 나서, 이 구현은 | |||
< | <code> | ||
// 잘못된 것 | // 잘못된 것 | ||
class Thread : public QThread { | class Thread : public QThread { | ||
protected: | protected: | ||
void run() { | void run() { | ||
while (condition) { | |||
// "condition" 은 volatile 이나 mutex 보호가 필요할지도 모른다. | |||
// 다른 쓰레드에서 이를 변경할 수 도 있기 때문이다(!) | |||
doWork(); | |||
sleep(1); // 이것은 QThread::sleep()이다. | |||
} | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
이러한 작업을 하는 데 있어 '''훨씬 더 낫고 간단한 방법'''은 타이머. 즉, [http://doc.qt.nokia.com/latest/qtimer.html QTimer] 객체를 1초의 타임아웃을 주고 doWork() 메소드를 슬롯으로 하여 사용하는 것이다. | |||
이러한 작업을 하는 데 있어 '''훨씬 더 | |||
< | <code> | ||
class Worker : public QObject | class Worker : public QObject | ||
{ | { | ||
Line 619: | Line 556: | ||
public: | public: | ||
Worker() { | Worker() { | ||
connect(&timer, SIGNAL (timeout()), this, SLOT (doWork())); | |||
timer.start(1000); | |||
} | } | ||
private slots: | private slots: | ||
void doWork() { | void doWork() { | ||
/* … */ | |||
} | } | ||
Line 631: | Line 568: | ||
QTimer timer; | QTimer timer; | ||
}; | }; | ||
<code> | </code> | ||
이제 필요한 것은 이벤트 루프를 실행하기만 하면 된다. 그러면 doWork() 메소드는 매 초마다 호출 될 것이다. | 이제 필요한 것은 이벤트 루프를 실행하기만 하면 된다. 그러면 doWork() 메소드는 매 초마다 호출 될 것이다. | ||
== 네트워킹과 상태기계(state machines) == | == 네트워킹과 상태기계(state machines) == | ||
네트워크 동작과 관련하여 가장 일반적인 설계 패턴은 다음과 같은 것이다. | 네트워크 동작과 관련하여 가장 일반적인 설계 패턴은 다음과 같은 것이다. | ||
< | <code> | ||
socket->connect(host); | socket->connect(host); | ||
socket->waitForConnected(); | socket->waitForConnected(); | ||
Line 656: | Line 591: | ||
socket->write(reply); | socket->write(reply); | ||
socket->waitForBytesWritten(); | socket->waitForBytesWritten(); | ||
/ | /* … 기타 등등 … */ | ||
<code> | </code> | ||
더 말할 필요도 없이 다양한 waitFor*()에 대한 호출로 인해 호출측은 이벤트 루프로 부터 리턴되지 않게 되며 UI가 동작을 멈추게 된다. 위와 같은 코드는 오류처리는 고려하지도 않았다. 그것마저 했다면 더 멍청한 상태로 빠졌을 지 모른다. 이 설계에 있어서 가장 잘못된 점은 '''네트워킹은 원래가 비동기적'''이라는 사실을 망각한 채 동기처리로 구현하여 제살 깎아먹기를 했다는 점이다. 이 문제를 해결하기 위해서 많은 사람들은 단순히 위의 코드를 다른 쓰레드로 옮겨간다. | |||
더 말할 필요도 없이 | |||
또 다른 개념적인 예를 들어보겠다. | 또 다른 개념적인 예를 들어보겠다. | ||
< | <code> | ||
result = process_one_thing(); | result = process_one_thing(); | ||
Line 676: | Line 609: | ||
input = read_user_input(); | input = read_user_input(); | ||
process_user_input(input); | process_user_input(input); | ||
/ | /* … */ | ||
<code> | </code> | ||
이 네트워킹 예제에서는 앞서와 유사한 단점이 존재한다. | 이 네트워킹 예제에서는 앞서와 유사한 단점이 존재한다. | ||
한발 물러서서 구현되고 있는 것을 넓은 시각으로 바라보자. 여기서 우리는 일련의 입력에 따라 반응하고 그에 따라 행동하는 '''상태기계(state machine)'''을 구축하려고 하는 것이 아닐까? 예를들어 이 네트워킹 예제에서는 우리는 다음과 같은 것을 사실은 만들고 있는 것이다. | |||
한발 물러서서 구현되고 있는 것을 넓은 시각으로 바라보자. 여기서 우리는 일련의 입력에 따라 반응하고 | |||
* 휴식상태(Idle) → 연결중(Connecting) (connectToHost()가 호출될 때); | |||
* 연결중(Connecting) → 연결완료(Connected) (connected() 시그널이 발생할 때); | * 연결중(Connecting) → 연결완료(Connected) (connected() 시그널이 발생할 때); | ||
* 연결완료(Connected) → 로그인 정보 전송(LoginDataSent) (서버에 로그인 정보를 전송할 때); | * 연결완료(Connected) → 로그인 정보 전송(LoginDataSent) (서버에 로그인 정보를 전송할 때); | ||
* 로그인 정보 전송(LoginDataSent) → 로그인(LoggedIn) (서버로 부터 ACK 응답을 받을 때) | * 로그인 정보 전송(LoginDataSent) → 로그인(LoggedIn) (서버로 부터 ACK 응답을 받을 때); | ||
* 로그인 정보 전송(LoginDataSent) → 로그인 오류(LoginError) (서버로 부터 NACK 응답을 받을 때) | * 로그인 정보 전송(LoginDataSent) → 로그인 오류(LoginError) (서버로 부터 NACK 응답을 받을 때). | ||
기타 등등이다. | 기타 등등이다. | ||
상태기계를 구축하는 몇가지 방법([http://doc.qt.nokia.com/4.7/qstatemachine.html QStateMachine]라는 전용의 클래스를 제공하고도 있다)이 있기는 하지만 그 중 가장 간단한 enum(즉, 정수형 값)을 사용하여 현재의 상태를 저장하도록 하여 위 예제코드를 다음과 같이 구현해 볼 수 있다. | |||
상태기계를 구축하는 몇가지 방법([http://doc.qt.nokia.com/4.7/qstatemachine.html QStateMachine] 라는 전용의 클래스를 제공하고도 있다)이 있기는 하지만 그 중 가장 간단한 enum (즉 정수형 값)을 사용하여 현재의 상태를 저장하도록 하여 위 예제코드를 다음과 같이 구현해 볼 수 있다. | |||
< | <code> | ||
class Object : public QObject | class Object : public QObject | ||
{ | { | ||
Q_OBJECT | Q_OBJECT | ||
enum State { | enum State { | ||
State1, State2, State3 /* and so on */ | |||
}; | }; | ||
Line 710: | Line 640: | ||
Object() : state(State1) | Object() : state(State1) | ||
{ | { | ||
connect(source, SIGNAL(ready()), this, SLOT(doWork())); | |||
} | } | ||
private slots: | private slots: | ||
void doWork() { | void doWork() { | ||
switch (state) { | |||
case State1: | |||
/* … */ | |||
state = State2; | |||
break; | |||
case State2: | |||
/* … */ | |||
state = State3; | |||
break; | |||
/* etc. */ | |||
} | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
"source"객체와 이의 "ready()" 시그널은 무엇이 될 수 있을까? 우리의 예제로 치자면 소켓의 QAbstractSocket::connected()와 우리가 구현한 슬롯에 대한 QIODevice::readyRead() 시그널과 같은 것으로 생각해 볼 수 있다. 물론 경우에 많기만 한다면 더 많은 슬롯을 쉽게 추가할 수 도 있다(QAbstractSocket::error()시그널에 의해 통지 받는 오류 상황을 관리하기 위한 슬롯). 이것이 진정한 비동기 시그널 주도 설계라 할 수 있다! | |||
"source"객체와 이의 "ready()" 시그널은 무엇이 될 수 있을까? 우리의 예제로 치자면 | |||
== 몇개의 덩어리로 나뉠 수 있는 작업들 == | == 몇개의 덩어리로 나뉠 수 있는 작업들 == | ||
어떤 이유에서 단순히 다른 쓰레드로 옮길 수 없는 큰 계산 루틴이 있다고 가정해 보자(또는 GUI쓰레드에서만 실행되어야 하기 때문에 전혀 옮길 수 없다고 가정해 보자). 만일 '''계산을 작은 덩어리들로 나눌 수 있다면''' 이벤트 루프로 그 때 그 때 리턴할 수 있다. 이는 큐 연결이 어떻게 구현되어 있는지를 상기한다면 쉽게 구성될 수 있는 내용이다. 이벤트 수신 객체가 살아가는 쓰레드의 이벤트 루프에 어떤 이벤트를 날리고 그 이벤트가 처리될 때 상응하는 슬롯이 호출되도록 하면 된다. | |||
어떤 이유에서 단순히 다른 쓰레드로 옮길 수 없는 큰 계산 루틴이 있다고 가정해 보자(또는 GUI쓰레드에서만 실행되어야 하기 때문에 전혀 옮길 수 없다고 가정해 보자). 만일 '''계산을 작은 덩어리들로 나눌 수 있다면''' | |||
QMetaObject::invokeMethod()를 사용해 Qt::QueuedConnection을 호출 방법으로 지정함으로써 동일한 결과를 얻을 수 도 있다. 이는 해당 메소드가 Q_INVOKABLE 지정되었거나 슬롯이어야만 한다. 만일 메소드에 인자를 전달해야 한다면 그 인자의 형은 qRegisterMetaTyp()을 사용해 Qt의 메타타입 시스템에 등록해야 한다. 아래의 예제코드는 이러한 형태를 나타내어 준다. | |||
QMetaObject::invokeMethod() 를 사용해 Qt:: | |||
< | <code> | ||
class Worker : public QObject | class Worker : public QObject | ||
{ | { | ||
Line 748: | Line 675: | ||
void startProcessing() | void startProcessing() | ||
{ | { | ||
processItem(0); | |||
} | } | ||
void processItem(int index) | void processItem(int index) | ||
{ | |||
/* process items[index] … */ | |||
if (index < numberOfItems) | |||
QMetaObject::invokeMethod(this, "processItem", | |||
Qt::QueuedConnection, | |||
Q_ARG(int, index + 1)); | |||
} | } | ||
}; | }; | ||
<code> | </code> | ||
쓰레드가 사용되어야만 할 이유가 없기 때문에 중단/재개/취소등과 같은 계산의 동작과 결과의 취합이 용이해 진다. | |||
쓰레드가 사용되어야만 할 이유가 없기 때문에 | |||
= | = 참고 = | ||
* Bradley T. Hughes: [http://labs.qt.nokia.com/2010/06/17/youre-doing-it-wrong/ You’re doing it wrong…], Qt Labs blogs, 2010-06-17 | |||
* Bradley T. Hughes: [http://labs.qt.nokia.com/2006/12/04/threading-without-the-headache/ Threading without the headache], Qt Labs blogs, 2006-12-04 | * Bradley T. Hughes: [http://labs.qt.nokia.com/2006/12/04/threading-without-the-headache/ Threading without the headache], Qt Labs blogs, 2006-12-04 | ||
[[Category:Developing with Qt]] | [[Category:Developing with Qt]] | ||
<references /> | <references /> |
Latest revision as of 04:59, 1 February 2018
STILL WORKING ON 원본 영문버젼의 내용이 업데이트가 되었는지 확인해 이 문서도 함께 갱신되어야 합니다(번역기준으로 원본의 문서리비젼: Last edit: March, 13, 2012)
쓰레드, 이벤트, 그리고 QObject객체(Threads, Events and QObjects)
경고 : 베타버젼입니다
이 문서는 거의 다 작업되었느나, 아직까지는 좀 더 다듬어야 합니다. 이 문서에 대한 논의는 여기 에서 진행되고 있습니다.
서론
어! 잘못하고 있는데요!.
— Bradley T. Hughes
"#qt IRC channel":irc://irc.freenode.net/#qt 에서 가장 많이 논의되는 주제중 하나가 쓰레딩과 관련한 것이다. 많은 사람들이 채널에 들어와 자신들의 문제를 "다른 쓰레드에서 행되는 코드"를 가지고 해결하려하고 이것에 대해 질문한다.
이 사람들의 코드를 들여다 보고 빨리 알아 챌 수 있는 문제들중 열에 아홉은 우선 쓰레드를 쓰고보자는 식으로 접근하고 나서 병렬 프로그래밍의 끝없는 해악에 빠져버린다는 것이다.
쓰레드의 생성과 실행이 Qt에서 쉬워진 고로 프로그래밍 스타일(특히 비동기 네트워크 프로그래밍이나 Qt의 시그널 슬롯 아키텍쳐)에 대한 지식 없이 혹은 다른 툴킷이나 언어를 사용할때 생긴 버릇 등과 버무려져 사람들은 종종 잘못된 길로 들어서게 되는 경우가 많다. 더군다나 Qt의 쓰레드관련 지원은 양날의 검과도 같다. 멀티쓰레딩의 구현이 아주 쉽게 되었지만 꼭 알고 사용해야 하는 사항들이 상당수 추가되어 있기 때문이다(특히 QObject 객체들과 상호작용을 하게 되는 경우).
이 문서는 쓰레드를 어떻게 쓰는지, 적합한 locking방법은 무엇인지, 병렬성(parallelism)의 활용은 어떻게 하는 것인지, 확장성 있는 프로그램을 작성하는 방법은 무엇인지를 말하기 위한 것이 아니다. 이 주제들에 대한 많은 책/문서들이 이미 많다(이를 테면 이 page와 같은 문서들). 대신 이 작은 문서를 통해 사용자들로 하여금 Qt 4에서의 쓰레딩을 소개하여 가장 일반적인 문제점들을 짚고 견고한 동시에 나은 구조를 가지는 코드를 개발할 수 있도록 돕고자 한다.
필요사항
이렇게 생각해봐요. 쓰레드는 소금과 같지만 파스타 같지는 않죠. 소금은 당신도 좋아하고 나도 좋아하고 우리 모두 좋아하지만 사실 파스타를 더 많이 먹죠.
— 래리 맥보이(Larry McVoy)
쓰레드 프로그래밍에 대한 범용 소개문서로 다음과 같은 사전 지식을 필요로 한다.
- C++ 기초 (사실, C++ 뿐만 아니라 많은 다른 언어들과 상관이 있다);
- Qt 기초: QObjects, signals 및 slots, 이벤트 핸들링);
- 쓰레드가 무엇이며, 쓰레드,프로세스 및 운영체제간의 상호관계;
- 적어도 하나 이상의 주요한 운영체제에서, 쓰레드를 어떻게 시작하고 멈추는지 그리고 쓰레드가 끝날 때 까지 기다리는 방법;
- 뮤텍스(mutex), 세마포어(semaphore) 및 대기조건(wait condition)을 사용하여 쓰레드 안전하고 재진입이 가능한 함수, 자료구조, 클래스를 만드는 법;
본 문서에서는 Qt의 명명규칙(naming convention)인 다음의 사항을 따를것이다.
- 재진입 한번에 최대 하나의 스레드에서 동일 인스턴스에 접근하는 경우를 가정하여 하나 이상의 쓰레드로 부터 어떤 클래스의 인스턴스를 사용하는 것이 안전하다면 그 클래스 재진입이 가능한 클래스이다. 각각의 호출에 고유한 데이터만을 참조한 다면 하나 이상의 쓰레드에서 동시에 호출되어도 안전한 함수 역시 재진입 가능 함수이다. 다시 말해 사용자는 이 클래스/함수에서 모든 인스턴스/공유데이터에 대한 접근을 어떠한 외부 락킹 메커니즘(external locking mechanism)을 사용해 직렬화(serialize) 해야만 한다는 것을 의미한다.
- 쓰레드안전 만일 하나 이상의 쓰레드에서 동시에 인스턴스들을 사용하는 것이 안전하다면 그 인스턴스의 클래스는 쓰레드 안전하다. 어떤 함수가 호출을 통해 공유데이터를 참조하더라도 한번에 하나 이상의 쓰레드에서 실행해도 안전하다면 그 함수는 쓰레드 안전하다.
이벤트와 이벤트 루프
이벤트 구동방식의 툴킷으로 이벤트들과 이벤트의 전송은 Qt 아키텍쳐에 있어 가장 중요한 역할을 담당하고 있다. 이 문서에서는 이 주제와 관련하여 자세한 언급은 하지 않도록 한다(여기 그리고 여기를 참조하여 Qt 이벤트 시스템에 대한 좀 더 자세한 정보를 참조).
Qt에서 이벤트는 현재 발생하는 사건에 대한 어떤 내용을 담고 있다. 이벤트와 시그널의 주요한 차이는 이벤트는 응용프로그램내의 특정 객체를 "대상"으로 하고 있는 것과 달리 시그널은 "주변으로" 퍼져나간다(emitted). 코드의 관점에서 보면 어떤 객체를 위한 모든 이벤트는 QEvent의 파생클래스이며 모든 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()을 실행하면 Qt의 메인 이벤트 루프로 들어간다. 이 호출은 QCoreApplication::exit() 혹은 QCoreApplication::quit()이 호출되면 루프가 종료된다.
"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 객체는 여전히 큐에 머물게 된다), 타이머는 발생되지 않으며, 네트워크 통신도 느려지고 수신/전송 큐가 차다가 시간이 지나면 멈추게 된다. 더구나 대부분의 윈도우 매니저들은 사용자의 응용프로그램이 더이상 이벤트를 처리하지 않는 것을 검출하여 사용자에게 응용프로그램이 응답하지 않음을 알려준다. 이 때문에 이벤트에 반응하여 가능한 최대한 빨리 이벤트 루프로 반환되도록 하는 것이 중요하다.
이벤트 강제 디스패칭(Forcing event dispatching)
그럼 오랜 시간이 걸리는 작업을 하면서도 이벤트 루프가 블록킹되지 않도록 하려면 어떻게 해야 할까? 한가지 가능한 해답은 이 작업을 다른 쓰레드로 옮기는 것이다. 다음 섹션에서 어떻게 하면 되는지 알게 될 것이다. 또 다른 방법은 강제로 이벤트 루프가 실행되도록 하는 것이다. 이는 (반복적으로) QCoreApplication::processEvents()를 블록킹하고 있는 작업 내에서 호출하는 것이다. 이 함수는 이벤트 큐에 있는 모든 이벤트를 처리하고 난 다음에서야 호출자(즉, 작업 객체의 작업 함수)로 반환된다.
또 다른 방법은 QEventLoop 클래스를 사용하여 강제로 이벤트 루프로 재진입하는 것이다. 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()로 전달하는 것이다. 이렇게 하면 사용자의 입력과 관련한 이벤트들은 전달되지 않는다(그냥 큐에 남아있는다).
다행스럽게도 이와 같은 문제가 삭제(deletion) 이벤트(QObject::deleteLater()에 의해 큐에 들어간 이벤트)에 대해서는 적용되지 않는다(다행스럽게도 삭제가 이루어질 것이다).
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
위 코드는 길잃은 포인터(dangling pointer) 문제를 유발하지 않는다(QDialog::exec()에 의해 들어간 이벤트 루프는 deleteLater 호출 보다 더 안쪽에 있다). 유사한 내용이 QEventLoop로 시작된 임시 이빈트 루프에도 적용될 수 있다. 눈여겨 볼 점은 (Qt 4.7.3버젼 기준) 이벤트 루프가 실행되지 않늘 때 deleteLater를 호출하게 되면, 맨 처음 이벤트 루프로 들어갈 때 이 이벤트가 수집되어 객체를 삭제한다는 것이다. Qt가 그 어떤 최종적으로 삭제를 수행하는 "외부" 루프에 대해 모르기 때문에 객체의 삭제는 곧바로 일어나게 되므로 꽤 논리적이라 할 수 있다.
.
Qt 쓰레드 클래스들
컴퓨터는 상태기계이다. 쓰레드는 상태기계를 프로그래밍 할 줄 모르는 사람들을 위한 것이다.
— 앤론 콕스(Alan Cox)
Qt는 많은 세월동안 쓰레드를 지원해 왔다(2000년 10월 22일 릴리즈된 Qt 2.2 부터 QThread 클래스가 등장하였다). 4.0이 릴리즈되면서는 모든 플랫폼에서 쓰레드 지원이 이루어졌다(하지만 이 기능은 끌 수가 있다. 여기를 참조). Qt는 이제 쓰레드와 관련한 몇가지 클래스를 더 지원하고 있다. 개괄 부터 시작해 보자.
QThread
QThread는 Qt 쓰레드 지원의 핵심이 되는 저수준 클래스다. QThread 객체는 하나의 쓰레드 실행을 표현한다. Qt가 cross-platform 이기 때문에, QThread는 모든 플랫폼 종속적인 코드들을 숨긴 상태로 서로 다른 운영체제의 사용이 가능하다.
QThread를 사용해 코드를 어떤 쓰레드에서 실행하려면 먼저 이 클래스를 상속 받아 QThread::run() 메소드를 오버라이드 해야한다.
class Thread : public QThread {
protected:
void run() {
/* 여기 사용자의 실행코드가 들어간다. */
}
};
그런 다음 아래와 같이
Thread *t = new Thread;
t->start(); /* run()이 아니다! */
하여 새로운 쓰레드를 시작할 수 있다. Qt 4.4 부터는 QThread가 더이상 가상 클래스가 아님에 유의하자. 가상 메소드 QThread::run()은 내부적으로 QThread::exec()을 호출하여 쓰레드의 이벤트 루프를 시작하도록 되었다.
QRunnable과 QThreadPool
QRunnable은 경량의 추상 클래스이며 "실행 한 다음 잊어 버리기" 방식으로 임의 작업을 시작하는 데 사용될 수 있다. 이를 위해서 모든 QRunnable의 파생클래스는 순수 가상 메소드 run()를 오버라이딩 해야 한다.
class Task : public QRunnable {
public:
void run() {
/* 사용자의 실행할 구현이 여기에 온다 */
}
};
실제로 QRunnable 객체를 실행하기 위해서는 쓰레드의 풀(pool)을 관리하는 QThreadPool 클래스를 사용해야 한다. QThreadPool::start(runnable) 식으로 호출하여 QRunnable 객체를 QThreadPool의 실행큐에 집어 넣는다. 어떤 쓰레드라도 사용가능한 상태가 되자마자 QRunnable 객체는 쓰레드로 전달되어 실행된다. 모든 Qt 응용프로그램은 전역 쓰레드 풀을 가지고 있으며 QThreadPool::globalInstance()을 호출해 이를 얻을 수 있지만 언제라도 QThread 인스턴스를 직접 만들어 명시적으로 사용할 수 있다.
주의 할 점으로 QRunnable은 QObject 파생 클래스가 아니므로 시그널/슬롯을 사용할 수 없고 다른 컴포넌트와 무언가를 주고 받기 위해서는 저수준 쓰레딩 프리미티브(결과를 수집하기 위한 뮤텍스에 의해 보호되는 큐와 같은)을 사용해 직접 통신코드를 구현해야 한다.
QtConcurrent
QtConcurrent은 고수준 API 로서 QThreadPool 위에 구현되었으며 대부분의 map), reduce), 및 filter)와 같은 병렬 컴퓨팅 패턴을 다루는 데 있어 유용하다. 이 역시 QtConcurrent::run() 메소드를 통해서 다른 쓰레드에서 임의 함수를 쉽게 실행 할 수 있다.
QThread나 QRunnable과는 달리 QtConcurrent는 그 어떤 저수준 동기화 객체를 사용할 필요가 없다. 대신 모든 QtConcurrent 메소드들은 QFuture을 반환하는데 이를 사용하여 작업 상황(진행 상태)를 질의할 수 있으며 진행 중인 작업을 pause/resume/cancel 할 수 있다. 또한 이것은 작업의 결과도 포함하고 있다.
기능 비교
QThread | QRunnable | QtConcurrent[1] | |
---|---|---|---|
고수준 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()을 실행하는 보통 메소드가 되었다.
쓰레드의 이벤트 루프는 이벤트를 그 쓰레드에서 운용되는(혹은 살아가는) 모든 QObject 객체들에 전달된다. 여기에는 디폴트로 그 쓰레드에서 생성된 모든 객체와 다른 쓰레드에서 생성되었다가 옮겨진 객체가 포함된다(옮겨지는 경우에 대해서는 아래에서 좀 더 상세한 정보를 다룬다). 어떤 QObject 객체의 쓰레드 친화도(thread affinity)가 특정 쓰레드다..라고 하기도 하는데 이는 이 객체가 그 쓰레드에서 살아가고 있다는 것을 의미한다. 이는 QThread 객체의 생성자에서 생성된 객체들에 적용된다.
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
QObject obj;
QObject *otherObj;
QScopedPointer<QObject> yetAnotherObj;
};
위에서 MyThread 객체를 생성한 다음 obj, otherObj 및 yetAnotherObj 객체 각각의 쓰레드 친화도라는 것이 어떤 의미일까? 각각을 생성하고 있는 쓰레드에 주목해야 한다. 이는 MyThread 생성자를 실행하는이 된다. 따라서 이 3개의 객체는 모두 MyThread 쓰레드에서 살아가지 않는다. 대신 MyThread 인스턴스를 생성한 쓰레드에서 살아간다(그런데 이 MyThread 객체 자체도 자신을 생성한 쓰레드에서 살아간다고 할 수 있다!).
어떤 QObject 객체의 쓰레드 친화도는 언제라도 QObject::thread()를 호출하여 질의할 수 있다. QCoreApplication 객체보다 앞서 생성된 그 어떤 QObject 객체들은 그 어떤 쓰레드 친화도도 없다. 그리고 이런 이유로 이들 객체에는 그 어떤 이벤트도 전달되지 않는다(다시 말해 QCoreApplication은 메인 쓰레드를 담당하는 QThread 객체를 직접 구축한다).
http://doc.qt.nokia.com/4.7/images/threadsandobjects.png
쓰레드 안전한 QCoreApplication::postEvent()를 사용해 이벤트를 임의 객체의 이벤트 규에 추가(posting)할 수 있다. 이렇게 하면 이벤트를 객체가 살아가는 쓰레드의 이벤트 루프에 집어넣게 된다. 하지만 쓰레드가 이벤트 루프를 실행하고 있지 않다면 이벤트는 전달되지 않을 것이다.
Object 및 이의 파생클래스들은 쓰레드 안전하지 않다는 점을 반드시 이해하여야 한다. 따라서 객체의 내부 데이터로의 접근을 직렬화 하지 않는다면(이를 테면, 뮤텍스로 보호하는…) 하나 이상의 쓰레드로 부터 QObject 객체로 접근해서는 안된다. 다른 쓰레드로 부터 객체에 접근하는 동안 객체가 자신이 살아가는 쓰레드의 이벤트 루프에 의해 전달된 이벤트를 처리할 수 도 있음을 반드시 기억해야 한다! 같은 이유로 객체가 살아가는 쓰레드 이외의 다른 쓰레드에서 객체를 할당해제 할 수 없다. 이를 위해서는 QObject::deleteLater()을 호출해서 객체가 살아가는 쓰레드로 할당 해제에 관한 이벤트를 큐에 집어넣고(posting) 최종적으로 자신의 쓰레드에서 할당해제가 이루어지도록 해야만 한다.
더구나 QWidget 및 이의 모든 파생클래스는 다른 GUI 관련 클래스들(심지어 QObject 기반이 아닌 QPixmap과 같은 클래스들)과 마찬가지로 재진입가능하지 않다. 이들은 오직 GUI 쓰레드에서만 배타적으로 사용되어져야만 한다.
QObject의 진화도는 QObject::moveToThread()를 호출하여 수정할 수 있다. 이렇게 하면 객체 자신 뿐 아니라 자식들 까지 자신이 살아갈 쓰레드를 바꾸게 된다. 즉, 이 객체들을 자신이 살아가는 쓰레드로 부터 전혀 다른 쓰레드로 집어넣는 것이지만 그 반대로 다른 쓰레드로 부터 끌어오는일은 불가능 하다. 더구나 Qt는 QObject의 자식들이 부모가 살아가는 쓰레드와 동일한 쓰레드에서 살아가도록 하는 것을 요구한다. 이는 암묵적으로 다음을 의미한다.
- QObject::moveToThread()를 부모를 가지고 있는 객체에 대해 사용할 수 없다;
- QThread 객체 자신을 부모로 하는 객체를 QThread 내에서 만들 수 없다.
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 잘못됨[[Image:|Image:]]!
}
};
그 이유는 QThread 객체는 다른 쓰레드에서 살아가고 있기 때문이다. 즉 자신을 생성한 쓰레드말이다.
Qt에서는 QThread 객체가 파괴된기 전에 이 객체가 나타내는 쓰레드에서 살아가던 모든 객체들이 파괴되는 것을 요구한다. 이는 QThread::run() 메소드의 스택에서 그 쓰레드에서 살아가는 모든 객체들을 생성하기만 하면 쉽게 구현된다.
쓰레드간의 시그널과 슬롯
어떻게 하면 다른 쓰레드에서 살아가는 QObject 객체들의 메소드를 호출할 수 있을까? Qt는 깔끔한 해결책을 제공하고 있다. 즉, 처리되기 원하는 쓰레드의 이벤트 큐에 이벤트를 추가하고(posting) 그 쓰레드에서 이벤트가 처리될 때 원하는 메소드가 호출되도록 하는 것이다(이렇게 하려면 해당 쓰레드의 QThread가 이벤트 루프를 실행하고 있어야만 한다). 이는 moc에 의해 자동 생성되는 메소드로 구현되기 때문에 오직 시그날과 슬롯 및 Q_INVOKABLE 매크로로 표기된 메소들만이 다른 쓰레드에서 호출가능하게 된다(역자주: Qt 4.x기준으로 QMetaCallEvent 라는 유형의 이벤트가 사용되고 있다).
QMetaObject::invokeMethod() 정적 메소드는 이런일을 가능하게 해 준다.
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
이벤트가 처리되는 과정에서 암시적으로 위 함수의 인자들이 복사되어질 필요가 있음에 유의하자. 따라서 이 인자들의 형(type)은 Qt의 형 체계(type system)하에서 qRegisterMetaType()을 사용해 공용 복사생성자, 공용 소멸자 및 공용 복사생성자가 반드시 등록되어져 있어야 한다.
쓰레드간 시그널 슬롯 역시 이와 유사한 방식으로 동작한다. 어떤 시그널을 슬롯에 연결할 때 QObject::connect 함수의 5번째 인자는 연결유형(connection type)의 지정에 사용된다.
- 직접 연결(direct connection)은 시그널이 발생되어진 쓰레드에서 항상 직접 슬롯을 호출하는 것을 의미한다(슬롯의 실행쓰레드는 시그널 발생 쓰레드);
- 큐 연결(queued connection)은 수신측 쓰레드의 이벤트 큐에 이벤트가 추가되고 나중에 이벤트 루프에서 빠질 때 슬롯이 호출되는 것을 의미한다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드);
- 블록킹 큐 연결(blocking queued connection)은 큐 연결 유형과 유사하지만 시그널 발생측이 슬롯의 호출종료시까지 블록킹되는 것이 다르다(슬롯의 실행쓰레드는 슬롯 객체의 이벤트 루프가 실행되는 쓰레드);
- 자동 연결(automatic connection)(기본) 은 슬롯이 시그널 발생측과 같은 쓰레드면 직접 연결되고 그 외의 경우에는 큐 연결이 사용된다(슬롯의 실행쓰레드는 시그널 발생시 결정된다).
그 어떤 경우에라도 유념해야 할 사항은 시그널을 발생시키는 객체가 살고 있는 쓰레드는 전혀 중요하지 않다는 점이다! 자동 연결의 경우에도 Qt는 시그널을 실행하는 쓰레드를 기준으로 이를 수신측 객채가 살고 있는 쓰레드와 비교하여 어떤 연결 유형을 쓸 것인지를 결정한다. current Qt documentation (4.7.1) 문서에 나와 있는 내용은 잘못되어있으므로 주의해야 한다.
자동 연결(auto connection, 기본)유형의 동작은 시그널 발생측과 수신측이 동일 쓰레드이면(즉, 쓰레드 친화도가 동일하다면) 직접 연결(direct connection)의 동작과 동일하지만 다른 쓰레드라면 큐 연결(queued connection)과 같아진다.
위의 문장이 잘못된 이유는 시그널 발생 객체의 쓰레드 친화도는 동작의 차이와는 아무 상관이 없기 때문이다. 예를 들면
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();
위에서 aSignal() 시그널은 Thread 객체로 표현되는 새로운 쓰레드에서 발생하게 된다. 이 쓰레드 자체는 Object형의 obj객체가 살아가는 쓰레드가 아니다(한편 thread 객체의 쓰레드 친화도와는 상관이 없다는 점을 강조하자면 이 쓰레드는 Thread형의 thread 객체가 살아가는 쓰레드이며 이는 친화도를 가지는 쓰레드와는 다르다). 따라서 큐 연결(queued connection)이 사용된다.
또 다른 범하기 쉬운 오류는 다음과 같은 것이다.
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();
"obj"가 aSignal() 시그널을 발생시킬 때 어떤 종류의 연결이 사용되게 될까? 한번 맞추어 보기 바란다. 바로 직접 연결(direct connection)이다. 그 이유는 Thread 객체는 시그널을 발생시키는 쓰레드에서 살아고 있기 때문이다. 이 경우 run() 메모드에 의해 Thread의 멤버변수에 대한 접근이 이루어지는 동안 aSlot() 슬롯에서도 역시 동일 변수들에 대한 접근이 이루어지게 될 수도 있게 되며 이런 식으로 작업하면 큰 재앙으로 곧잘 이어지게 된다.
또 다른 다음 예는 어쩌면 가장 중요한 것일지도 모르겠다.
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* … */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
/* … */
}
};
이 경우 큐 연결(queued connection)이 사용되는데 그 이유는 Thread 객체가 살아가는 쓰레드(Thread가 표현하는 쓰레드. 즉, run()의 쓰레드와는 다르다)에 있는 이벤트 루프에 의해서 연결이 처리되기 때문이다.
게시판, 블로그 글 등에서 종종 볼 수 있는 답변들 가운데에는 Thread 의 생성자에 moveToThread(this)를 추가하라는 것이 많이 있다.
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 주의
}
/* … */
};
위와 같이 해도 사실은 동작을 한다(왜냐하면 Thread 객체의 쓰레드 친화도가 변경되기 때문이다). 하지만 이는 잘못된 설계이다. 이런 방식의 문제는 thread객체(즉, QThread 파생클래스의 객체)의 목적을 잘못 이해하고 있다는 데 있다. QThread 객체들은 쓰레드가 아니다. 이들은 쓰레드를 감싸고 있는 객체들을 제어하기 위한 것이며 다른 쓰레드(대개 그 객체가 살아가는 쓰레드)로 부터 사용되어질 목적으로 존재한다.
옳바른 결과를 얻기 위한 바람직한 방법은 "제어부"로 부터 "동작부"를 떼어네어서 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();
해도 좋은 것과 해서는 안 되는 것
가능한것…
- … QThread의 서브클래스에 시그널 추가하기. 이는 완벽히 안전하면서도 "제대로" 일을 한다고 할 수 있다. (위를 보면 시그널 전송부의 쓰레드 친화도는 아무런 문제가 되지 않는 것을 알 수 있다.)
하지말것…
- … moveToThread(this) 사용.
- … 시그널 슬롯의 연결시 연결 유형 강제 적용하기: 이런 구현은 QThread의 제어 인터페이스와 프로그램의 로직을 섞는 것과 같은(로직은 자신이 살고 있는 쓰레드와는 다른 별도의 객체에 존재하는 것이 바람직하다) 오류를 범하는 경우가 많다.
- … QThread 서브클래스에 슬롯 추가하기: 호출되면 안되는 쓰레드로 부터 슬롯 호출이 이루어지게 될 수 있다. 이 허용되지 않는 쓰레드는 QThraed 객체가 관리하는 쓰레드가 아닌 다른 객체가 살아가는 쓰레드이며 QThread가 표현하는 쓰레드에서 호출이 이루어지게 하기 위해 직접 연결(direct connection)을 지정하게 되거나 moveToThread(this)를 사용하게 되고 만다.
- … QThread::terminate 사용.
절대 하지말것…
- … 쓰레드가 구동하고 있는 중 프로그램을 종료하는 행위: QThread::wait을 사용해 종료를 대기해야 한다.
- … 자신이 관리하고 있는 쓰레드가 아직 구동중일때 이 쓰레드를 관리하던 QThread 객체를 파괴하는 행위: "자동 파괴"와 같은 기능을 원한다면 QThread::finished() 시그널을 QObject::deleteLater() 슬롯과 연결만 하면 된다.
언제 쓰레드를 써야만 할까?
쓸 수 있는 API가 블록킹 유형의 것들 밖에 없을 때
사용하고자 하는 라이브러리나 다른 이의 코드가 (시그널/슬롯, 이벤트, 혹은 콜백등의) 논블록킹 API 를 제공하지 않는다면 이벤트 루프의 동작 중단을 방지하기 위한 유일한 방안은 프로세스 혹은 쓰레드를 만드는 것이다. 새로운 작업 프로세스의 생성은 작업 지시와 통보등의 방법에 있어 쓰레드를 생성하는 방법보다 훨씬 어렵고 비용도 비싸므로 가능한 쓰레드를 사용하는 편이 나을 수 있다.
이런 종류의 API 중 가장 좋은 예는 주소 확인(address resolution) API 이다(3rdparty 라이브러리로 나온 것이 아니라도 모든 C 라이브러리에 포함되어 있다). 이 API 는 호스트 이름을 가져다 주소 값으로 변환해 준다. 그 일련의 과정은 대개 원격지에 있는 시스템(Domain Name System 즉 DNS)에 대한 질의로 이루어진다. 응답이 거의 순간적으로 오기는 하겠지만 원격지 서버가 응답을 못하거나 패킷유실이 이루어지거나, 네트워크 연결이 끊어지는 듯듯의 사고가 있을 수 있다. 어쨌든 질의에 대한 응답이 오기 까지는 몇초정도가 걸린다.
UNIX 시스템에 존재하는 표준 API는 블록킹 방식이다(오래된 gethostbyname(3) 이 아니더라도 새로운 getservbyname(3)이나 getaddrinfo(3)도 마찬가지다). Qt 클래스인 QHostInfo는 호스트명칭의 검색에 있어 QThreadPool을 사용하여 백그라운드로 이를 처리한다(여기를 확인).
또 다른 간단한 예는 이미지 로딩이나 스케일링(scaling)이다. QImageReader와 QImage는 디바이스로 부터 이미지를 읽어들이는 블록킹 메소드만 제공한다. 아주 큰 이미지들을 다루거나 하게 되면 이들 기능들은 수십초 이상의 시간이 걸리게 될 것이다.
CPU의 갯수에 따라 구현 규모를 조정하고자 할 때
쓰레드는 사용자의 프로그램으로 하여금 다중프로세서 시스템의 이점을 활용할 수 있게 해준다. 각각의 쓰레드가 운영체제에 의해 독립적으로 스케쥴링되므로, 응용프로그램이 이러한 다중 프로세서 시스템에서 구동된다면, 스케쥴러는 각 쓰레드를 독립적인 프로세서에서 동시에 구동될 수 있게된다.
예를 들어 일련의 이미지들에 대한 썸네일을 생성하는 응용프로그램이 있다하자. n개의 쓰레드로 구성된 쓰레드 팜(thread farm)(즉, 고정된 갯수의 쓰레드로 구성된 쓰레드 풀)은 시스템에 존재하는 CPU 하나당 하나씩의 쓰레드를 가지며(QThread::idealThreadCount()를 참조) 이미지를 썸네일로 스케일 다운하는 작업을 모든 쓰레드에 분산시켜 작업할 수 있게 할 수 있다. 이런식으로 프로세서 갯수에 대해 거의 선형적인 승능향상을 얻게 된다(단순화하기 위해 CPU 자체가 병목임을 가정하면).
다른 작업에 방해받지 않기를 원할 때
음. 예를 들어 설명한다.
이 주제는 조금 난해할 수도 있으므로, 일단 넘어가도 상관은 없다. 이 경우에 대한 가장 좋은 예는 QNetworkAccessManager가 WebKit에서 어떻게 사용되는지와 관련해서 이다. WebKit은 최신의 브라우저 엔지이며 웹페이지를 배치하고 표시하는 일련의 클래스들이다. WebKit을 사용하는 Qt widget은 QWebView 이다.
QNetworkAccessmanager는 모든 HTTP 요청과 응답을 처리하는 Qt 클래스이다. 웹 브라우저의 네트워킹 엔진을 생각해 볼 수 있겠다. 현재의 설계는 그 어떤 작업 쓰레드도 사용하고 있지 않고 있다. 모든 네트워크 작업은 QNetworkAccessManager와 이의 QNetworkReply 객체들이 살아가고 있는 쓰레드와 동일한 쓰레드에서 처리된다.
네트워킹과 관련하여 쓰레드를 사용하지 않는 것 자체는 매우 좋은 생각이지만 치명적인 단점이 있다. 소켓으로 부터 데이터를 최대한 빨리 읽지 않게 되면 커널 버퍼가 차게 되어 패킷이 드랍되며 전송속도가 극단적으로 줄어들 수 있다는 점이다.
소켓의 동작(소켓으로 부터 읽어들일 데이터의 존재여부)은 Qt 의 이벤트 루프에 의해서 관리된다. 이벤트 루프를 블록킹하게 되면 읽어야할 데이터 거기 있다는 사실을 아무도 통지 받지 못헤게 되므로 전송 성능이 떨어진다.
하지만 이벤트 루프를 누가 블록킹하게 되는 것일까? 슬프지만 그 답은 WebKit 바로 자기 자신이다. 일련의 데이터를 수신하게 되면 WebKit은 이를 사용해 웹 페이지를 구성하기 시작한다. 불운하게도 이 구성 과정은 매우 복잡하고 비용도 크기 때문에 드문 드문 이벤트 루프를 블록킹하게 되고 결국 추가적으로 들어오는 전송에 영향을 점점 더 많이 주기 시작한다.
요약하자면, 다음과 같은 일들이 벌어진다고 할 수 있다.
- WebKit이 요청을 발생시킨다;
- 응답으로 일련의 데이터가 도착한다;
- WebKit 이 웹 페이지를 들어온 데이터를 사용해 구성하기 시작하면서 이벤트 루프가 블록킹된다;
- 이벤트 루프가 돌지 않게 되면서 OS에 의해 수신되 데이터가 QNetworkAccessManager의 소켓으로 부터 읽혀지지 않게된다;
- 커널 버퍼가 차게되어 전송이 느려진다.
전체적인 페이지 로딩 속도는 결국 자기 자신으로 인해 발생한 전송 속도 저하로 느려지게 된다.
QNetworkAccessManager와 QNetworkReply 객체들은 모두 QObject 들이다. 이들은 쓰레드 안전(thread safe)하지 않다. 따라서 단순히 이 객체들을 다른 쓰레드로 옮긴 다음 현재 쓰고 있던 쓰레드에서 사용할 수 는 없다. 왜냐하면 후자 쓰레드의 이벤트 루프에 의해 전달되어진 이벤트로 인해 두개의 쓰레드로 부터 동시에 사용될 수도 있기 때문이다.
Qt 4.8 버젼부터 QNetworkAccessManager는 HTTP 요청을 기본적으로 독립적인 쓰레드에서 처리하도록 수정되었다. 따라서 GUI가 무응답하거나 OS 버퍼가 너무 빨리 차 버리는 문제가 해결되었다.
쓰레드를 사용하지 않는편이 좋은 경우는?
쓰레드를 사용하려고 생각하고 있다면, 당신의 작업이 너무 방만하기 때문일것이오.
— 롭 파이크(Rob Pike)
타이머
쓰레드 남용의 가장 심각한 유형에 대해 알아보자. 만일 어떤 메소드를 반복적으로 수행해야 한다면(예를 들어 매 초마다) 대다수 사람들은 다음과 같은 구현을 떠올 릴 것이다.
// 아주 잘못 된 것
while (condition) {
doWork();
sleep(1); // C라이브러리의 sleep(3)함수
}
그리고 나서, 이 구현은 이벤트 루프를 블록킹하고 있다는 것을 곧 깨닫고 다음과 같이 쓰레드를 도입하기로 마음먹게 된다.
// 잘못된 것
class Thread : public QThread {
protected:
void run() {
while (condition) {
// "condition" 은 volatile 이나 mutex 보호가 필요할지도 모른다.
// 다른 쓰레드에서 이를 변경할 수 도 있기 때문이다(!)
doWork();
sleep(1); // 이것은 QThread::sleep()이다.
}
}
};
이러한 작업을 하는 데 있어 훨씬 더 낫고 간단한 방법은 타이머. 즉, QTimer 객체를 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;
};
이제 필요한 것은 이벤트 루프를 실행하기만 하면 된다. 그러면 doWork() 메소드는 매 초마다 호출 될 것이다.
네트워킹과 상태기계(state machines)
네트워크 동작과 관련하여 가장 일반적인 설계 패턴은 다음과 같은 것이다.
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();
/* … 기타 등등 … */
더 말할 필요도 없이 다양한 waitFor*()에 대한 호출로 인해 호출측은 이벤트 루프로 부터 리턴되지 않게 되며 UI가 동작을 멈추게 된다. 위와 같은 코드는 오류처리는 고려하지도 않았다. 그것마저 했다면 더 멍청한 상태로 빠졌을 지 모른다. 이 설계에 있어서 가장 잘못된 점은 네트워킹은 원래가 비동기적이라는 사실을 망각한 채 동기처리로 구현하여 제살 깎아먹기를 했다는 점이다. 이 문제를 해결하기 위해서 많은 사람들은 단순히 위의 코드를 다른 쓰레드로 옮겨간다.
또 다른 개념적인 예를 들어보겠다.
result = process_one_thing();
if (result->something())
process_this();
else
process_that();
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* … */
이 네트워킹 예제에서는 앞서와 유사한 단점이 존재한다.
한발 물러서서 구현되고 있는 것을 넓은 시각으로 바라보자. 여기서 우리는 일련의 입력에 따라 반응하고 그에 따라 행동하는 상태기계(state machine)을 구축하려고 하는 것이 아닐까? 예를들어 이 네트워킹 예제에서는 우리는 다음과 같은 것을 사실은 만들고 있는 것이다.
- 휴식상태(Idle) → 연결중(Connecting) (connectToHost()가 호출될 때);
- 연결중(Connecting) → 연결완료(Connected) (connected() 시그널이 발생할 때);
- 연결완료(Connected) → 로그인 정보 전송(LoginDataSent) (서버에 로그인 정보를 전송할 때);
- 로그인 정보 전송(LoginDataSent) → 로그인(LoggedIn) (서버로 부터 ACK 응답을 받을 때);
- 로그인 정보 전송(LoginDataSent) → 로그인 오류(LoginError) (서버로 부터 NACK 응답을 받을 때).
기타 등등이다.
상태기계를 구축하는 몇가지 방법(QStateMachine라는 전용의 클래스를 제공하고도 있다)이 있기는 하지만 그 중 가장 간단한 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. */
}
}
};
"source"객체와 이의 "ready()" 시그널은 무엇이 될 수 있을까? 우리의 예제로 치자면 소켓의 QAbstractSocket::connected()와 우리가 구현한 슬롯에 대한 QIODevice::readyRead() 시그널과 같은 것으로 생각해 볼 수 있다. 물론 경우에 많기만 한다면 더 많은 슬롯을 쉽게 추가할 수 도 있다(QAbstractSocket::error()시그널에 의해 통지 받는 오류 상황을 관리하기 위한 슬롯). 이것이 진정한 비동기 시그널 주도 설계라 할 수 있다!
몇개의 덩어리로 나뉠 수 있는 작업들
어떤 이유에서 단순히 다른 쓰레드로 옮길 수 없는 큰 계산 루틴이 있다고 가정해 보자(또는 GUI쓰레드에서만 실행되어야 하기 때문에 전혀 옮길 수 없다고 가정해 보자). 만일 계산을 작은 덩어리들로 나눌 수 있다면 이벤트 루프로 그 때 그 때 리턴할 수 있다. 이는 큐 연결이 어떻게 구현되어 있는지를 상기한다면 쉽게 구성될 수 있는 내용이다. 이벤트 수신 객체가 살아가는 쓰레드의 이벤트 루프에 어떤 이벤트를 날리고 그 이벤트가 처리될 때 상응하는 슬롯이 호출되도록 하면 된다.
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));
}
};
쓰레드가 사용되어야만 할 이유가 없기 때문에 중단/재개/취소등과 같은 계산의 동작과 결과의 취합이 용이해 진다.
참고
- Bradley T. Hughes: You’re doing it wrong…, Qt Labs blogs, 2010-06-17
- Bradley T. Hughes: Threading without the headache, Qt Labs blogs, 2006-12-04
- ↑ QtConcurrent::run은 예외. QRunnable 을 사용해 구현되었으므로 장/단점이 있다.