들어가며
우리 회사에서는 Qt Framework를 이용해 Windows와 Linux에서 동작하는 애플리케이션을 개발한다. Qt에서 사용하는 Signal/Slot 방식과 Multi Thread에서 어떻게 동작하는지, 처음 Qt에서 QMetaObject::invokeMethod 함수를 만나며 애먹던 BlockingQueuedConnection과 DirectConnection에 대한 내용을 정리하고. 마지막으로 "함수 설계는 어떻게 해야 하는지"까지 생각해 보자.
Qt의 Signal-Slot 이벤트 전달 메커니즘
Qt를 접하게 되면 제일 중요하고 쉬운 부분이 이 부분이다. Qt는 내부적으로 이벤트루프가 동작하면서 순차적으로 실행한다.
이때 최우선으로 잡아야 하는 개념은 QObject는 자신이 속한 Thread 별로 이벤트 루프를 가진다는 점이다.
예를 들어 한 개의 스레드에 2개의 QTimer가 돌고 있다고 상상해 보자, 두 개의 타이머는 각각 timeout이 발생해 한 개의 이벤트 루프로 들어간다. 즉, 타이머에서 같은 메모리를 공유한다 해도 Race Condition이 발생하지 않을 것이라는 걸 알 수 있다.
QObject::connect(timerA, &QTimer::timeout, []() {
sharedValue += 1;
});
QObject::connect(timerB, &QTimer::timeout, []() {
sharedValue += 1;
});
timerA->start();
timerB->start();
한편, 2개의 스레드에서 각각 QTimer가 동작한다고 하면 Race Condition이 발생해 sharedValue값이 이상하게 동작할 것이다.
QThread *threadA = new QThread(&a);
QThread *threadB = new QThread(&a);
Worker *workerA = new Worker("ThreadA_Timer", 100);
Worker *workerB = new Worker("ThreadB_Timer", 150);
workerA->moveToThread(threadA);
workerB->moveToThread(threadB);
QObject::connect(threadA, &QThread::started, workerA, &Worker::start);
QObject::connect(threadB, &QThread::started, workerB, &Worker::start);
threadA->start();
threadB->start();
Worker 없이 QTimer를 다른 Thread로 옮기기만 하면 될 거 같은 느낌이지만, 정상적인 Qt동작을 위해 Worker가 필요하다.
이 내용은 QThread에서의 QObject생성에 관한 내용으로 너무 길어서 생략한다.
다른 Thread의 시그널을 호출하는 invokeMethod()
QMetaObject::invokeMethod()는 다른 스레드에 있는 QObject의 슬롯을 호출할 수 있도록 설계된 메서드이다.
메서드를 통해 같은 스레드의 슬롯 호출은 당연히 가능하고, 다른 스레드의 슬롯을 호출할 수도 있다.
이때 고려할 점은 메서드를 Default로 인자로 호출할 때(=AutoConnection), 스레드에 따라 동작 방식이 달라진 다는 것이다.
- 같은 스레드라면 (=같은 이벤트 루프) 라면 즉시 실행.
- 다른 스레드라면 (=다른 이벤트 루프) 라면 EventQueue에 넣고 비동기 실행
먼저 설명하기 쉽게 getValue로 int형 값을 가져오는 슬롯을 설계했다고 가정하자,
public slots:
const int getValue();
1번의 경우 아래와 같이 호출할 수 있고, invoke 함수 호출을 하면 동기적으로 getValue() 함수가 끝날 때까지 대기할 것이다.
이는 invokeMethod가 DirectConnection옵션으로 동작할 때와 동일한 동작이다.
int result;
bool ok = QMetaObject::invokeMethod(
&worker,
"getValue",
//Qt::DirectConnection, *생략된 옵션
Q_RETURN_ARG(int, result) // ★ 리턴값 받기
);
qDebug() << "getValue Return = " << result;
문제는 2번의 경우(다른 스레드에서 동작하는 경우)에 발생한다. 이 부분이 내가 잘 몰라 애먹었던 부분이다.
다른 이벤트루프의 슬롯을 호출한 경우 코드는 아래와 같이 동작한다.
int result;
bool ok = QMetaObject::invokeMethod(
&worker,
"getValue",
//Qt::QueuedConnection, *생략된 옵션
Q_RETURN_ARG(int, result) // ★ 리턴값 받기
);
qDebug() << "getValue Return = " << result;
위 코드 동작은 이렇다. invoke 함수가 호출된 뒤 다른 스레드의 이벤트 큐에 getValue를 넣는다. 넣기만 하고 ok값은 invokeMethod는 할 일을 다했으니 true를 반환하고 qDebug로 result값인 쓰레기값 또는 초기값이 출력될 것이다.
(또는 다른 이벤트루프가 먼저 동작했다면 getValue값이 들어있을 수도 있다.)
이처럼 비동기적으로 동작하면서 개발자의 의도와 틀어질 수 있음을 고려해야 한다.
동기방식 or 비동기방식을 고려한 함수 설계
문제의 원인을 알았다면 동기 또는 비동기 방식으로 고려해 문제를 해결할 수 있다.
동기방식을 원한다면 invoke 함수를 호출할 때 BlockingQueuedConnection 옵션을 함께 호출하면 된다.
invoke 함수호출시 해당 이벤트큐가 Blocking 되며, 다른 스레드의 이벤트큐에 함수가 끝나길 기다린다.
비동기방식으로 호출할 때는 리턴값을 고려해야 한다. invoke함수를 QueuedConnection 옵션으로 호출한다면, 이벤트큐에 등록하자마자 리턴한다. 따라서 invoke로 호출하려던 함수가 실행되지 않은 상태에서 반환될 수 있다. 리턴값 또한 default값으로 반환될 가능성이 크다.
비동기방식으로 호출하는 함수의 경우 아래와 같이 혼동을 야기할 수 있는 코드를 피해야 한다.
int calculate() {
QThread::sleep(1); //시간이 걸리는 작업
return 10;
}
bool ok = QMetaObject::invokeMethod(
&worker,
"calculate",
Qt::ueuedConnection, // 비동기 호출
Q_RETURN_ARG(int, result) // 반환이 더 빨라 실제로는 Default값 or 쓰레기값이 반환됨.
);
따라서 비동기 방식의 함수를 설계한다면 함수가 끝났다는 emit Signal 방식을 통해 전달하는 방식이 제일 깔끔하다.
void calculate() {
QThread::sleep(1); //시간이 걸리는 작업
emit finished(10);
}
bool ok = QMetaObject::invokeMethod(
&worker,
"calculate",
Qt::ueuedConnection, // 비동기 호출
);
만약 dll로 구현한다는 등 QObject를 사용하지 못하는 경우라면, std::function을 이용해 콜백함수를 이용해 기능을 구현할 수 있다.
void calculateAsync(std::function<void(int)> callback) {
// sleep(1)처럼 시간이 걸리는 작업
callback(10);
}
worker.calculateAsync([](int result) {
qDebug() << "결과:" << result;
});
회고.
invoke함수가 AutoConnection으로 동작하고 있었으며 디버깅에 상당히 애를 먹었다. 이번 개발을 통해 Qt에서 invokeMethod의 AutoConnection이 어떻게 동작하는지 정확하게 알 수 있었고, 매개변수 명시화를 통해 코드 가독성을 향상할 수 있었다.
이번 디버깅을 통해 파악한 문제의 본질은 "비동기 호출에서 리턴값을 사용할 수 없다"는 사실이었다.
추후 처음부터 개발한다면 Qt의 Thread와 EventQueue 개념에 대해 정확히 알고 있어, 함수설계 단계에서부터 호출방식(동기/비동기)을 구분하고 더 명확한 API를 작성해야겠다.
'Qt' 카테고리의 다른 글
| Qt IPC 성능 비교 실험기 ㅡ QSharedMemory, QLocalSocket, QTcpSocket, QRemoteObject (1) | 2026.01.19 |
|---|---|
| Qt6 DLL 생성부터 gtest 유닛 테스트까지 ㅡ 프로젝트 구성 따라하기 (0) | 2026.01.14 |
| Qt 4 · Qt 5 · Qt 6 차이점 정리 — 레거시 컴파일 실무 경험 (0) | 2026.01.07 |
| Qt에서 시간 다루기: Unix Time, TimeZone, Embedded Device의 시간 전송 (0) | 2025.12.19 |
| QLibrary DLL 로드 실패 원인 분석기 — ‘지정된 모듈을 찾을 수 없습니다’ 해결기 (0) | 2025.11.11 |