前言

在 Qt 網絡開發中,TCP 粘包 / 拆包問題與多線程管理是兩個繞不開的核心痛點。傳統方案中,開發者常通過繼承 QThread 為每個 TCP 連接創建獨立線程,這種方式不僅會導致線程創建銷燬的巨大開銷,還容易因鎖管理不當引發競態條件。本文將深入剖析 TCP 粘包 / 拆包的底層原理,詳解如何用 QRunnable + 線程池替代 QThread 繼承,並結合 QMutexLocker 實現自動化鎖管理,最終構建一個高效、安全的高併發網絡通信架構。

一、TCP 粘包 / 拆包:流式協議的 “隱形陷阱”

在開始討論多線程架構前,我們必須先理解 TCP 粘包 / 拆包的本質 —— 這是所有基於 TCP 的網絡通信都無法迴避的基礎問題,也是設計線程模型的前提。

1.1 為什麼會出現粘包 / 拆包?

TCP 協議是一種面向連接的流式協議,其核心特性是 “字節流無邊界”。這意味着:

  • 發送端多次調用write()發送的數據包,接收端可能一次性接收(粘包);
  • 發送端一次發送的大數據包,接收端可能分多次接收(拆包);
  • 接收端無法通過 TCP 協議本身判斷一個完整的 “業務數據包” 從哪裏開始、到哪裏結束。

這種特性源於 TCP 的底層優化機制:

  • Nagle 算法:會將小數據包合併發送以減少網絡擁塞;
  • MSS(最大分段大小):超過 MSS 的數據包會被 IP 層拆分;
  • 接收緩衝區:接收端會將多個數據包暫存於緩衝區,應用層讀取時可能一次性取走多個。

舉個實際場景:客户端將"HelloWorld"字符串拆開,連續發送兩個數據包"Hello"(5 字節)和"World"(5 字節),服務器可能收到:

  • 粘包:一次性收到"HelloWorld"(10 字節);
  • 拆包:先收到"Hel"(3 字節),再收到"loWorld"(7 字節);
  • 混合:先收到"HelloW"(6 字節),再收到"orld"(4 字節)。

如果服務器直接按 “讀取一次 = 一個數據包” 處理,必然導致解析錯誤。

1.2 粘包 / 拆包的解決方案

解決粘包 / 拆包的核心是為字節流添加 “業務邊界”,讓接收端能準確識別一個完整數據包。在 Qt 中,常用的可靠方案有三種:

方案 1:定長包頭 + 變長包體(推薦)

這是工業級應用中最常用的方案,協議格式定義為:

[包頭(固定N字節)][包體(變長)]
  • 包頭:通常包含包體長度(如 4 字節 uint32_t),可附加版本號、校驗位等元數據;
  • 包體:實際業務數據(如 JSON、Protobuf 序列化數據)。

接收流程:

  • 先讀取固定長度的包頭(如 4 字節),解析出包體長度len;
  • 再讀取len字節的包體,組合為完整數據包。
方案 2:特殊分隔符

在數據包末尾添加特殊分隔符(如\r\n),接收端通過分隔符判斷包結束。但存在缺陷:

  • 若包體中包含分隔符(如 JSON 字符串中的\n),需額外轉義;
  • 分隔符匹配可能消耗 CPU(尤其長數據包)。
方案 3:固定長度數據包

所有數據包長度固定,不足補 0。缺點明顯:

  • 靈活性差,無法適應變長業務數據;
  • 空間浪費(小數據也要佔滿固定長度)。

本文將以 “定長包頭(4 字節)+ 變長包體” 方案為例,展開後續實現。

二、傳統 QThread 方案的 “性能陷阱”

在處理多 TCP 連接時,傳統方案常採用 “一個連接一個線程” 的模型:通過繼承 QThread,為每個新連接創建獨立線程,在線程中處理該連接的讀寫與粘包解析。這種方案看似直觀,卻隱藏着嚴重的性能問題。

2.1 QThread 繼承模式的實現

先看一個典型的 QThread 繼承實現(簡化後的):

// TcpClientThread.h
class TcpClientThread : public QThread {
    Q_OBJECT
public:
    explicit TcpClientThread(qintptr socketDescriptor, QObject *parent = nullptr)
        : QThread(parent), m_socketDescriptor(socketDescriptor) {}

protected:
    void run() override {
        // 在子線程中創建QTcpSocket
        QTcpSocket socket;
        if (!socket.setSocketDescriptor(m_socketDescriptor)) {
            emit error(socket.error());
            return;
        }

        // 處理粘包/拆包的緩衝區
        QByteArray recvBuffer;

        // 等待數據到達
        while (socket.waitForReadyRead(-1)) {
            recvBuffer += socket.readAll();
            // 解析緩衝區中的完整數據包
            parsePackets(recvBuffer);
        }
    }

private:
    void parsePackets(QByteArray &buffer) {
        // 定長包頭(4字節)解析邏輯
        while (buffer.size() >= 4) {
            uint32_t bodyLen = *reinterpret_cast<uint32_t*>(buffer.data());
            // 注意:網絡字節序與主機字節序轉換(Qt提供qToBigEndian/qFromBigEndian)
            bodyLen = qFromBigEndian(bodyLen);

            if (buffer.size() < 4 + bodyLen) {
                break; // 包體不完整,等待後續數據
            }

            // 提取完整包體
            QByteArray body = buffer.mid(4, bodyLen);
            // 處理業務邏輯
            processBody(body);
            // 移除已處理數據
            buffer = buffer.mid(4 + bodyLen);
        }
    }

    void processBody(const QByteArray &body) {
        // 實際業務處理(如解析JSON、執行命令)
        qDebug() << "Received data:" << body;
    }

    qintptr m_socketDescriptor;
};

// TcpServer.h
class TcpServer : public QTcpServer {
    Q_OBJECT
public:
    explicit TcpServer(QObject *parent = nullptr) : QTcpServer(parent) {}

protected:
    void incomingConnection(qintptr socketDescriptor) override {
        // 為每個新連接創建線程
        TcpClientThread *thread = new TcpClientThread(socketDescriptor);
        connect(thread, &TcpClientThread::finished, thread, &TcpClientThread::deleteLater);
        thread->start();
    }
};

這段代碼實現了基本的多連接處理,但在高併發場景下會暴露致命問題。

2.2 線程創建的 “隱性開銷”

QThread 繼承方案的核心問題是線程資源的低效利用:
線程創建 / 銷燬 成本高; 線程是操作系統級資源,創建線程需要分配棧空間(默認 8MB)、內核對象,銷燬時需要回收資源。在 Linux 系統中,創建一個線程的耗時約為 10-100 微秒,而頻繁創建銷燬 1000 個線程會導致顯著的 CPU 與內存開銷。
上下文切換代價大操作系統調度線程時需要保存 / 恢復寄存器、切換頁表等,當線程數量超過 CPU 核心數時,上下文切換會急劇增加。例如,1000 個線程爭奪 8 核 CPU,光切換成本可能都能佔總 CPU 時間的一小半了。
資源限制操作系統對線程數量有上限(如 Linux 默認線程數上限為 32768),高併發場景下(如 10 萬級連接),“一個連接一個線程” 完全不可行。
QThread 的設計陷阱:QThread 本身是 “線程控制器”,而非線程本身。繼承 QThread 並覆寫run()的模式,容易讓開發者誤解 “線程對象即線程”,導致線程與對象生命週期管理混亂(如在主線程調用子線程對象的方法)。

2.3 競態條件的 “隱形殺手”

當多個線程操作共享資源(如連接列表、全局計數器)時,若鎖管理不當,極易引發競態條件。傳統方案中,大家常手動調用QMutex::lock()/unlock(),但存在風險:

// 錯誤示例:手動鎖管理可能導致死鎖
QMutex mutex;
QList<qintptr> clientList;

void addClient(qintptr socketDesc) {
    mutex.lock();
    clientList.append(socketDesc);
    // 若此處拋出異常,unlock()將不會執行,導致死鎖
    mutex.unlock();
}

即使沒有異常,複雜邏輯中也可能因疏忽忘記解鎖,或因鎖的順序錯誤引發死鎖。

三、QRunnable + 線程池:輕量級多任務處理

Qt 提供了QRunnable與QThreadPool組合,專為短期任務複用線程設計,能有效解決 QThread 繼承方案的性能問題。

3.1 QRunnable:無狀態的任務載體

QRunnable是一個輕量級的任務接口,核心特點:

  • 僅包含一個純虛函數run(),用於定義任務邏輯;
  • 無事件循環(區別於 QThread),適合執行一次性任務;
  • 可通過setAutoDelete(bool)設置是否自動銷燬(默認 true),避免內存泄漏。

與 QThread 相比,QRunnable的優勢在於:

  • 無需繼承複雜的 QObject,實現更簡潔;
  • 任務完成後自動銷燬(或由線程池管理),生命週期更可控;
  • 本身不綁定線程,由線程池調度到空閒線程執行,實現線程複用。

3.2 QThreadPool:線程複用的 “調度中心”

QThreadPool是 Qt 的線程池管理器,核心功能:

  • 維護一個線程隊列,自動複用空閒線程執行QRunnable任務;
  • 可通過setMaxThreadCount(int)限制最大線程數(默認值為 CPU 核心數 ×2);
  • 提供globalInstance()獲取全局線程池,避免重複創建。

線程池的工作流程:

  • 當提交QRunnable任務時,若有空閒線程,直接分配執行;
  • 若無空閒線程且未達最大線程數,創建新線程執行;
  • 若已達最大線程數,任務進入等待隊列,直到有線程空閒。

這種機制避免了線程頻繁創建銷燬的開銷,尤其適合處理大量短期任務(如 TCP 連接的單次數據處理)。

3.3 線程池 vs 傳統線程:性能對比

下面這是別人通過一個簡單的壓力測試(10000 個任務,每個任務執行 1ms),對比兩種方案的性能的結果,由此可見一斑:

TCP粘包產生的原因、解決方法及Qt項目代碼實現_線程池


數據顯示,線程池方案在耗時、內存、CPU 效率上均有顯著優勢,尤其在任務數量龐大時差距更明顯。

四、重構 TCP 服務器:QRunnable 處理粘包 / 拆包

基於上述分析,我們用QRunnable+ 線程池重構 TCP 服務器,實現高效的粘包 / 拆包處理。

4.1 架構設計

整體架構分為三層:

  • 監聽層(TcpServer):主線程中監聽端口,接收新連接,生成SocketDescriptor;
  • 任務層(TcpClientTask):繼承QRunnable,封裝單個連接的讀寫與粘包解析邏輯;
  • 線程池(QThreadPool):全局線程池調度TcpClientTask,複用線程執行任務。

核心流程:

  • 客户端連接到達時,TcpServer獲取SocketDescriptor;
  • 創建TcpClientTask任務,傳入SocketDescriptor;
  • 將任務提交給線程池,由空閒線程執行;
  • 任務中完成 TCP 連接的建立、數據讀寫、粘包解析與業務處理。

4.2 核心代碼實現

  • 步驟 1:定義 TcpClientTask 任務類
// TcpClientTask.h
#include <QRunnable>
#include <QTcpSocket>
#include <QMutex>
#include <QByteArray>
#include <QLoggingCategory>

// 日誌分類,方便調試
Q_DECLARE_LOGGING_CATEGORY(logTcpTask)

class TcpClientTask : public QRunnable {
public:
    explicit TcpClientTask(qintptr socketDescriptor, QObject *parent = nullptr);
    ~TcpClientTask() override;

    void run() override; // 任務執行入口

private:
    // 粘包/拆包解析
    void parsePackets(QByteArray &buffer);
    // 業務處理
    void processBusiness(const QByteArray &data);
    // 發送響應
    void sendResponse(const QByteArray &response);

private:
    qintptr m_socketDescriptor; //  socket描述符
    QTcpSocket *m_socket;       // TCP socket
    QByteArray m_recvBuffer;    // 接收緩衝區(用於粘包處理)
    bool m_isRunning;           // 任務運行狀態
};

// TcpClientTask.cpp
Q_LOGGING_CATEGORY(logTcpTask, "tcp.task")

TcpClientTask::TcpClientTask(qintptr socketDescriptor, QObject *parent)
    : QRunnable(), m_socketDescriptor(socketDescriptor), m_socket(nullptr), m_isRunning(true) {
    setAutoDelete(true); // 任務完成後自動銷燬
}

TcpClientTask::~TcpClientTask() {
    if (m_socket) {
        m_socket->close();
        delete m_socket;
    }
    qCDebug(logTcpTask) << "Task destroyed, socketDescriptor:" << m_socketDescriptor;
}

void TcpClientTask::run() {
    m_socket = new QTcpSocket();
    if (!m_socket->setSocketDescriptor(m_socketDescriptor)) {
        qCWarning(logTcpTask) << "Set socket descriptor failed:" << m_socket->errorString();
        return;
    }

    qCDebug(logTcpTask) << "New client connected, socketDescriptor:" << m_socketDescriptor
                        << "Thread ID:" << QThread::currentThreadId();

    // 循環讀取數據
    while (m_isRunning && m_socket->state() == QTcpSocket::ConnectedState) {
        // 等待數據到達(超時100ms,避免永久阻塞)
        if (m_socket->waitForReadyRead(100)) {
            m_recvBuffer += m_socket->readAll();
            parsePackets(m_recvBuffer); // 解析緩衝區
        } else {
            // 處理錯誤(如連接斷開)
            if (m_socket->error() != QTcpSocket::TimeoutError) {
                qCWarning(logTcpTask) << "Socket error:" << m_socket->errorString();
                m_isRunning = false;
            }
        }
    }

    qCDebug(logTcpTask) << "Client disconnected, socketDescriptor:" << m_socketDescriptor;
}

void TcpClientTask::parsePackets(QByteArray &buffer) {
    // 定長包頭(4字節,存儲包體長度,網絡字節序)
    while (buffer.size() >= 4) {
        // 解析包頭(注意字節序轉換)
        uint32_t bodyLen = *reinterpret_cast<const uint32_t*>(buffer.constData());
        bodyLen = qFromBigEndian(bodyLen); // 網絡字節序轉主機字節序

        // 檢查包體是否完整
        if (buffer.size() < 4 + bodyLen) {
            break; // 包體不完整,等待後續數據
        }

        // 提取包體
        QByteArray body = buffer.mid(4, bodyLen);
        // 移除已處理的包頭+包體
        buffer = buffer.mid(4 + bodyLen);

        // 處理業務邏輯
        processBusiness(body);
    }
}

void TcpClientTask::processBusiness(const QByteArray &data) {
    qCDebug(logTcpTask) << "Processing data:" << data << "Length:" << data.size();

    // 示例:簡單回顯業務(實際場景可替換為JSON解析、數據庫操作等)
    QByteArray response = "Server received: " + data;
    sendResponse(response);
}

void TcpClientTask::sendResponse(const QByteArray &response) {
    if (m_socket->state() != QTcpSocket::ConnectedState) {
        return;
    }

    // 構建響應包(包頭+包體)
    uint32_t bodyLen = qToBigEndian(static_cast<uint32_t>(response.size())); // 主機字節序轉網絡字節序
    QByteArray packet;
    packet.append(reinterpret_cast<const char*>(&bodyLen), 4); // 包頭
    packet.append(response); // 包體

    // 發送數據
    m_socket->write(packet);
    m_socket->flush();
}
  • 步驟 2:實現 TcpServer 監聽類
// TcpServer.h
#include <QTcpServer>
#include <QThreadPool>
#include "TcpClientTask.h"

class TcpServer : public QTcpServer {
    Q_OBJECT
public:
    explicit TcpServer(QObject *parent = nullptr) : QTcpServer(parent) {
        // 配置線程池(根據CPU核心數調整)
        QThreadPool::globalInstance()->setMaxThreadCount(QThread::idealThreadCount() * 2);
        qCDebug(logTcpTask) << "ThreadPool max threads:" << QThreadPool::globalInstance()->maxThreadCount();
    }

protected:
    void incomingConnection(qintptr socketDescriptor) override {
        // 創建任務並提交到線程池
        TcpClientTask *task = new TcpClientTask(socketDescriptor);
        QThreadPool::globalInstance()->start(task);
    }
};
  • 步驟 3:啓動服務器
// main.cpp
#include <QCoreApplication>
#include "TcpServer.h"

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

    TcpServer server;
    if (!server.listen(QHostAddress::Any, 8888)) {
        qCritical() << "Server listen failed:" << server.errorString();
        return -1;
    }

    qInfo() << "Server started on port 8888";
    return a.exec();
}

4.3 粘包 / 拆包處理的關鍵細節

上述代碼中,parsePackets函數是處理粘包 / 拆包的核心,需注意以下細節:

  • 字節序轉換不同設備的字節序(大端 / 小端)可能不同,你得使用 Qt 的qToBigEndian(主機→網絡)和qFromBigEndian(網絡→主機)進行轉換,確保跨平台兼容性。
  • 緩衝區管理用QByteArray作為接收緩衝區,每次讀取數據後追加到緩衝區,解析完成後移除已處理部分,未處理的剩餘數據保留在緩衝區中,等待下次解析。
  • 半包處理當緩衝區數據長度不足 “包頭 + 包體” 時,退出循環等待後續數據,避免解析不完整的包。
  • 異常處理通過waitForReadyRead的超時機制(100ms)避免線程永久阻塞,同時處理 socket 錯誤(如連接斷開)。

五、QMutexLocker:自動化鎖管理的 “安全衞士”

在多線程環境中,共享資源(如連接數統計、全局配置)的訪問必須加鎖保護。QMutexLocker基於 RAII(資源獲取即初始化)模式,能自動管理鎖的獲取與釋放,徹底避免手動鎖管理的風險。

5.1 RAII 模式的核心思想

RAII 的核心是:將資源的生命週期與對象的生命週期綁定。在QMutexLocker中:

  • 構造函數獲取鎖(調用QMutex::lock());
  • 析構函數釋放鎖(調用QMutex::unlock());
  • 無論代碼通過正常路徑(return)還是異常路徑(throw)退出作用域,析構函數都會執行,確保鎖被釋放。

5.2 用 QMutexLocker 保護共享資源

假設我們需要統計當前在線客户端數量,這是一個典型的共享資源,需用鎖保護:

// 共享資源管理類
class ClientManager {
public:
    static ClientManager &instance() {
        static ClientManager inst;
        return inst;
    }

    // 增加在線客户端
    void addClient(qintptr socketDesc) {
        QMutexLocker locker(&m_mutex); // 自動加鎖
        m_onlineClients.insert(socketDesc);
        // 離開作用域時,locker析構,自動解鎖
    }

    // 移除在線客户端
    void removeClient(qintptr socketDesc) {
        QMutexLocker locker(&m_mutex); // 自動加鎖
        m_onlineClients.remove(socketDesc);
    }

    // 獲取在線客户端數量
    int onlineCount() {
        QMutexLocker locker(&m_mutex); // 自動加鎖
        return m_onlineClients.size();
    }

private:
    ClientManager() = default;
    QMutex m_mutex; // 保護共享資源的互斥鎖
    QSet<qintptr> m_onlineClients; // 在線客户端集合
};

在TcpClientTask中使用該管理器:

// TcpClientTask.cpp 中補充
void TcpClientTask::run() {
    m_socket = new QTcpSocket();
    if (!m_socket->setSocketDescriptor(m_socketDescriptor)) {
        // ... 錯誤處理
        return;
    }

    // 客户端上線,添加到管理器
    ClientManager::instance().addClient(m_socketDescriptor);
    qCDebug(logTcpTask) << "Online clients:" << ClientManager::instance().onlineCount();

    // ... 數據處理循環 ...

    // 客户端下線,從管理器移除
    ClientManager::instance().removeClient(m_socketDescriptor);
    qCDebug(logTcpTask) << "Online clients:" << ClientManager::instance().onlineCount();
}

5.3 QMutexLocker 的高級用法

臨時解鎖若在鎖作用域內需要臨時釋放鎖(如等待某個條件),可使用unlock()和relock():

void doSomething() {
    QMutexLocker locker(&m_mutex);
    // 處理需要加鎖的邏輯
    
    locker.unlock(); // 臨時解鎖
    // 執行不需要鎖的耗時操作(如IO)
    
    locker.relock(); // 重新加鎖
    // 繼續處理需要加鎖的邏輯
}

轉移鎖所有權通過moveToThread將鎖的所有權轉移給其他QMutexLocker對象(C++11 及以上):

QMutexLocker locker1(&m_mutex);
// ... 
QMutexLocker locker2(std::move(locker1)); // 鎖所有權轉移給locker2
// 此時locker1不再持有鎖,解鎖由locker2負責

5.4 避免死鎖的最佳方案

即使使用QMutexLocker,仍需注意鎖的使用順序,避免死鎖。例如:

// 錯誤示例:兩個線程獲取鎖的順序相反
// 線程1
QMutexLocker locker1(&mutexA);
QMutexLocker locker2(&mutexB); // 可能死鎖

// 線程2
QMutexLocker locker1(&mutexB);
QMutexLocker locker2(&mutexA); // 可能死鎖

解決方案:全局統一鎖的獲取順序(如按地址大小排序):

// 正確示例:按鎖地址排序獲取
QMutex *m1 = &mutexA;
QMutex *m2 = &mutexB;
if (m1 > m2) std::swap(m1, m2); // 確保先獲取地址小的鎖

QMutexLocker locker1(m1);
QMutexLocker locker2(m2);

六、性能優化與實戰經驗

基於上述架構,我們還需在實戰中進行針對性優化,以應對高併發場景。

6.1 線程池參數調優

QThreadPool的setMaxThreadCount參數並非越大越好,需根據業務特點調整:

  • CPU 密集型任務(如數據計算):線程數≈CPU 核心數(避免過多上下文切換);
  • IO 密集型任務(如 TCP 讀寫):線程數可設置為 CPU 核心數 ×2~4(利用 IO 等待時間處理其他任務)。
  • 可通過QThread::idealThreadCount()獲取 CPU 核心數,動態調整:
int cpuCount = QThread::idealThreadCount();
QThreadPool::globalInstance()->setMaxThreadCount(cpuCount * 3); // IO密集型場景

6.2 緩衝區優化

QByteArray的mid()方法會複製數據,在高頻解析場景下可優化為指針操作:

// 優化前:mid()會複製數據
QByteArray body = buffer.mid(4, bodyLen);
buffer = buffer.mid(4 + bodyLen);

// 優化後:使用指針和偏移量,避免複製
const char *bodyData = buffer.constData() + 4;
processBusiness(QByteArray::fromRawData(bodyData, bodyLen)); // 不復制數據
buffer.remove(0, 4 + bodyLen); // 高效移除頭部數據

6.3 非阻塞 IO 與信號槽

QTcpSocket在多線程中可使用信號槽(需注意線程親和性),替代waitForReadyRead的阻塞方式:

// 在TcpClientTask::run()中使用信號槽
void TcpClientTask::run() {
    m_socket = new QTcpSocket();
    // ... 綁定socketDescriptor ...

    // 將socket移動到當前線程(任務執行的線程)
    m_socket->moveToThread(QThread::currentThread());

    // 連接信號槽
    connect(m_socket, &QTcpSocket::readyRead, this, &TcpClientTask::onReadyRead, Qt::DirectConnection);
    connect(m_socket, &QTcpSocket::disconnected, this, &TcpClientTask::onDisconnected, Qt::DirectConnection);

    // 啓動事件循環(QRunnable本身無事件循環,需手動創建)
    QEventLoop loop;
    connect(this, &TcpClientTask::finished, &loop, &QEventLoop::quit);
    loop.exec();
}

void TcpClientTask::onReadyRead() {
    m_recvBuffer += m_socket->readAll();
    parsePackets(m_recvBuffer);
}

void TcpClientTask::onDisconnected() {
    m_isRunning = false;
    emit finished(); // 退出事件循環
}

注意:信號槽連接類型需用Qt::DirectConnection(同線程直接調用),避免跨線程調度開銷。

6.4 連接過載保護

當併發連接數超過服務器承載能力時,需進行過載保護:

// TcpServer中限制最大連接數
void TcpServer::incomingConnection(qintptr socketDescriptor) {
    if (ClientManager::instance().onlineCount() >= 10000) { // 最大連接數限制
        QTcpSocket socket;
        socket.setSocketDescriptor(socketDescriptor);
        socket.write("Server is busy");
        socket.disconnectFromHost();
        return;
    }
    // 正常創建任務
    TcpClientTask *task = new TcpClientTask(socketDescriptor);
    QThreadPool::globalInstance()->start(task);
}

總結
本文深入探討了 Qt 網絡開發中兩個核心問題的解決方案:

  • TCP 粘包 / 拆包:通過 “定長包頭 + 變長包體” 協議,結合緩衝區管理,實現可靠的數據包解析;
  • 多線程效率:用QRunnable+QThreadPool替代 QThread 繼承,通過線程複用降低資源開銷;
  • 線程安全:基於QMutexLocker的 RAII 模式,自動化管理鎖的生命週期,避免競態條件與死鎖。