이 페이지에서

발신자 차단

QSerialPort 의 동기식 API를 워커 스레드에서 사용하는 방법을 보여줍니다.

'Blocking Sender'는 워커 스레드에서 QSerialPort 의 동기식 API를 사용하여 시리얼 인터페이스용 애플리케이션을 만드는 방법을 보여줍니다.

Qt Serial Port 송신자 차단

QSerialPort 다음과 같은 두 가지 프로그래밍 방식을 지원합니다:

  • 비동기(비차단) 방식. 작업은 제어권이 Qt 이벤트 루프로 돌아올 때 스케줄링되어 수행됩니다. 작업이 완료되면 ` QSerialPort ` 클래스가 신호를 발생시킵니다. 예를 들어, ` write()` 메서드는 즉시 반환됩니다. 데이터가 시리얼 포트로 전송되면 ` QSerialPort ` 클래스가 ` bytesWritten()` 신호를 발생시킵니다.
  • 동기식(블로킹) 방식. 헤드리스 및 멀티스레드 애플리케이션에서는 wait 메서드(이 경우 waitForReadyRead())를 호출하여 작업이 완료될 때까지 호출 스레드를 일시 중지할 수 있습니다.

이 예제에서는 동기식 대안을 보여줍니다. ‘Terminal’ 예제는 비동기식 대안을 설명합니다.

이 예제의 목적은 사용자 인터페이스의 반응성을 잃지 않으면서 시리얼 프로그래밍 코드를 단순화하는 방법을 보여주는 것입니다. 차단형 시리얼 프로그래밍 API는 종종 코드를 더 단순하게 만들지만, 사용자 인터페이스의 반응성을 유지하기 위해서는 비 GUI 스레드에서만 사용해야 합니다.

이 애플리케이션은 송신자 역할을 하며, 수신자 애플리케이션인 ‘Blocking Receiver’ 예제와 연동되는 동작을 보여줍니다.

송신자 애플리케이션은 시리얼 포트를 통해 수신자 애플리케이션에 전송 요청을 시작하고 응답을 기다립니다.

class SenderThread : public QThread
{
    Q_OBJECT

public:
    explicit SenderThread(QObject *parent = nullptr);
    ~SenderThread();

    void transaction(const QString &portName, int waitTimeout, const QString &request);

signals:
    void response(const QString &s);
    void error(const QString &s);
    void timeout(const QString &s);

private:
    void run() override;

    QString m_portName;
    QString m_request;
    int m_waitTimeout = 0;
    QMutex m_mutex;
    QWaitCondition m_cond;
    bool m_quit = false;
};

SenderThread는 수신자에게 요청을 스케줄링하기 위한 API를 제공하는 ` QThread `의 서브클래스입니다. 이 클래스는 응답 및 오류 보고를 위한 신호를 제공합니다. `transaction()` 메서드를 호출하여 원하는 요청으로 새로운 송신 트랜잭션을 시작할 수 있습니다. 결과는 `response()` 신호를 통해 제공됩니다. 문제가 발생하면 `error()` 또는 `timeout()` 신호가 발생합니다.

참고로, transaction() 메서드는 메인 스레드에서 호출되지만, 요청은 SenderThread 스레드에서 처리됩니다. SenderThread의 데이터 멤버는 서로 다른 스레드에서 동시에 읽히거나 쓰이기 때문에, QMutex 클래스를 사용하여 액세스를 동기화합니다.

void SenderThread::transaction(const QString &portName, int waitTimeout, const QString &request)
{
    const QMutexLocker locker(&m_mutex);
    m_portName = portName;
    m_waitTimeout = waitTimeout;
    m_request = request;
    if (!isRunning())
        start();
    else
        m_cond.wakeOne();
}

transaction() 메서드는 시리얼 포트 이름, 타임아웃 및 요청 데이터를 저장합니다. 이 데이터를 보호하기 위해 QMutexLocker 를 사용하여 뮤텍스를 잠글 수 있습니다. 스레드가 이미 실행 중이 아닌 경우, 이때 스레드를 시작할 수 있습니다. wakeOne() 메서드에 대해서는 나중에 설명하겠습니다.

void SenderThread::run()
{
    bool currentPortNameChanged = false;

    m_mutex.lock();
    QString currentPortName;
    if (currentPortName != m_portName) {
        currentPortName = m_portName;
        currentPortNameChanged = true;
    }

    int currentWaitTimeout = m_waitTimeout;
    QString currentRequest = m_request;
    m_mutex.unlock();

run() 함수에서는 먼저 QMutex 객체를 잠근 다음, 멤버 데이터를 사용하여 시리얼 포트 이름, 타임아웃 및 요청 데이터를 가져옵니다. 이 작업이 완료되면 QMutex 잠금이 해제됩니다.

어떤 경우에도 데이터 가져오기 작업이 진행 중인 스레드와 동시에 transaction() 메서드를 호출해서는 안 됩니다. 참고로, QString 클래스는 재진입 가능하지만 스레드 안전하지는 않습니다. 따라서 요청 스레드에서 시리얼 포트 이름을 읽고, 다른 스레드에서 타임아웃 또는 요청 데이터를 처리하는 것은 권장되지 않습니다. SenderThread 클래스는 한 번에 하나의 요청만 처리할 수 있습니다.

QSerialPort 객체는 루프에 진입하기 전 run() 메서드 내에서 스택에 생성됩니다:

    QSerialPort serial;

    if (currentPortName.isEmpty()) {
        emit error(tr("No port name specified"));
        return;
    }

    while (!m_quit) {

이를 통해 루프가 실행되는 동안 객체를 생성할 수 있습니다. 또한 이는 모든 객체 메서드가 run() 메서드의 범위 내에서 실행됨을 의미합니다.

루프 내부에서는 현재 트랜잭션의 시리얼 포트 이름이 변경되었는지 여부가 확인됩니다. 변경된 경우, 시리얼 포트가 다시 열리고 재구성됩니다.

        if (currentPortNameChanged) {
            serial.close();
            serial.setPortName(currentPortName);

            if (!serial.open(QIODevice::ReadWrite)) {
                emit error(tr("Can't open %1, error code %2")
                           .arg(m_portName).arg(serial.error()));
                return;
            }
        }

루프는 데이터를 계속 요청하고, 시리얼 포트에 데이터를 쓰며, 모든 데이터가 전송될 때까지 대기합니다.

        // write request
        const QByteArray requestData = currentRequest.toUtf8();
        serial.write(requestData);
        if (serial.waitForBytesWritten(m_waitTimeout)) {

경고: 차단 전송의경우 , 각 ` write ` 메서드 호출 후에는 ` waitForBytesWritten()` 메서드를 사용해야 합니다. 이렇게 하면 Qt 이벤트 루프 대신 모든 I/O 루틴이 처리됩니다.

데이터 전송 중 타임아웃 오류가 발생하면 timeout() 신호가 발산됩니다.

        } else {
            emit timeout(tr("Wait write request timeout %1")
                         .arg(QTime::currentTime().toString()));
        }

요청이 성공하면 응답을 기다리는 대기 시간이 있으며, 그 후 다시 읽기가 수행됩니다.

            // read response
            if (serial.waitForReadyRead(currentWaitTimeout)) {
                QByteArray responseData = serial.readAll();
                while (serial.waitForReadyRead(10))
                    responseData += serial.readAll();

                const QString response = QString::fromUtf8(responseData);
                emit this->response(response);

경고: 차단 방식의경우 , 각 read() 호출 전에 waitForReadyRead() 메서드를 사용해야 합니다. 이렇게 하면 Qt 이벤트 루프 대신 모든 I/O 루틴이 처리됩니다.

데이터 수신 시 타임아웃 오류가 발생하면 timeout() 신호가 발산됩니다.

            } else {
                emit timeout(tr("Wait read response timeout %1")
                             .arg(QTime::currentTime().toString()));
            }

트랜잭션이 성공적으로 완료되면, response() 신호에는 수신자 애플리케이션에서 수신한 데이터가 포함됩니다.

                emit this->response(response);

그 후, 스레드는 다음 트랜잭션이 나타날 때까지 대기 상태에 들어갑니다. 스레드는 깨어난 후 멤버 변수를 사용하여 새 데이터를 읽고, 루프를 처음부터 다시 실행합니다.

        m_mutex.lock();
        m_cond.wait(&m_mutex);
        if (currentPortName != m_portName) {
            currentPortName = m_portName;
            currentPortNameChanged = true;
        } else {
            currentPortNameChanged = false;
        }
        currentWaitTimeout = m_waitTimeout;
        currentRequest = m_request;
        m_mutex.unlock();
    }

예제 실행

다음 경로에서 예제를 실행할 수 있습니다:

code.qt.io의 예제 프로젝트

‘직렬 터미널 ’ 및 ‘블로킹 수신기’항목도 참조하십시오 .

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.