1. 程式人生 > >qt 利用執行緒池和鎖搭建客戶端框架(一)

qt 利用執行緒池和鎖搭建客戶端框架(一)

    做客戶端開發很久了,一直在嘗試搭建一個更好的客戶端架構;在看了qt的QQucikAsyicImageProvider這個類的官方示例後發現,qt的QRunable類在run函式中也可以通過傳送訊號的方式與主執行緒通訊,之前看過的說明都說QRunable不好與主執行緒互動,這次算看到例項了。訪方法是通過多繼承的方式繼承QObject和QRunable,這個程式碼編寫的時候要注意,多繼承時QObject要寫在前面,這個我就不知道了。

    通訊多繼承之後就可以在run函式中與主執行緒通訊,但是主執行緒怎麼與run中的子執行緒通訊呢;從原始的角度看就只有啟動執行緒是那一個,run函式執行結束就完了,因為它是沒有訊息迴圈的。也就是在沒有死迴圈的情況下,它只能執行完了就退出執行緒了。由於我是要保證主執行緒隨時能與它通訊,所以第一時間就想到了死迴圈;而後主執行緒與子執行緒的通訊,我選擇了鎖和條件表示式,這樣就能阻塞等待了。程式碼如下

#ifndef WORKERAPI_H
#define WORKERAPI_H

#include <QRunnable>
class QVariant;
namespace BLL {
enum WorkerType{
    RunForever,
    RunOnce
};
class Worker : public QRunnable
{
public:
    Worker() = default;
    virtual void quit() = 0;
    virtual WorkerType workerType() const = 0;

protected:
    virtual void pushBackTask(QVariant &data) = 0;
    virtual QVariant getTask() = 0;
};
}
#endif // WORKERAPI_H

先聲明瞭介面,下面是一種實現

#ifndef BASEWORKER_H
#define BASEWORKER_H

#include "workerapi.h"
#include <mutex>
#include <condition_variable>
#include <queue>
#include <QVariant>
QT_FORWARD_DECLARE_CLASS(QVariant)
namespace BLL{
class BaseWorker : public QObject , public Worker
{
    Q_OBJECT
public:
    explicit BaseWorker(QObject *parent = nullptr);
    ~BaseWorker();
    Q_INVOKABLE void quit() override;

protected:
    virtual void pushBackTask(QVariant &data) override;
    virtual QVariant getTask() override;

private:
    std::mutex _mtx;
    std::condition_variable _cv;
    std::queue<QVariant> _argVec;
};
}#endif//
BASEWORKER_H
#include "baseworker.h"
#include <QThread>
#include <QDebug>

BLL::BaseWorker::BaseWorker(QObject *parent):
    QObject(parent)
{
}

BLL::BaseWorker::~BaseWorker()
{
    qDebug() << "delete Worker";
}

void BLL::BaseWorker::quit()
{
    QVariant invalid;
    pushBackTask(invalid);
}

void BLL::BaseWorker::pushBackTask(QVariant &data)
{
    std::lock_guard<std::mutex> lck(_mtx);
    _argVec.push(data);
    _cv.notify_one();
}

QVariant BLL::BaseWorker::getTask()
{
    std::unique_lock<std::mutex> lck(_mtx);
    _cv.wait(lck,[this]{return !_argVec.empty();});
    QVariant argData = _argVec.front();
    _argVec.pop();
    lck.unlock();
    return argData;
}

可以看到我在其中重要的兩個介面中使用了鎖和條件表示式,另外一個quit()介面顯示易見就是用來退出的了,往佇列裡面放一個空的資料,當子執行緒拿到資料是無效的時候就直接返回run()函式這樣子執行緒就退出了。

下面是一個死迴圈的最終與業務相關的類了

#ifndef RUNFOREVERWORKER_H
#define RUNFOREVERWORKER_H

#include "core/baseworker.h"
#include "DLL/testhttp.h"
#include "DLL/testtcp.h"
#include <QVariant>
namespace BLL {
class RunForeverWorker : public BaseWorker
{
    Q_OBJECT
public:
    RunForeverWorker(QObject *parent = nullptr):BaseWorker(parent){}
    WorkerType workerType() const override;
    Q_INVOKABLE void requestData();

signals:
    void sigResponse(QVariant result);

protected:
    void run() override;
};
}

#endif // RUNFOREVERWORKER_H
#include "runforeverworker.h"
#include <QThread>
#include <QDebug>

BLL::WorkerType BLL::RunForeverWorker::workerType() const
{
    return RunForever;
}

void BLL::RunForeverWorker::requestData()
{
    QVariant argData("-------------forever-----------------");
    pushBackTask(argData);
}

void BLL::RunForeverWorker::run()
{
    std::shared_ptr<DLL::TestHttp> _http(new DLL::TestHttp);
    std::shared_ptr<DLL::TestSocket> _tcp(new DLL::TestSocket);
    while (true) {
        QVariant arguments = getTask();
        if(!arguments.isValid())return;
        qDebug() << "get a task " << QThread::currentThreadId() << arguments << _tcp.get() << _http.get();
        _tcp->search();
        CURLcode resCode = CURLcode(_http->searchHistoty());
        if(resCode != CURLE_OK){
            emit sigResponse(QVariant::fromValue(QVariant::fromValue(QString(curl_easy_strerror(resCode)))));
        }else {
            emit sigResponse(QVariant::fromValue(QVariant::fromValue(QString::fromStdString(_http->records()))));
        }
    }
}
可以看到

在run函式中有一個死迴圈,每次當佇列不為空時都會取得一個數據來執行業務操作,執行完後繼續等待下一個資料,如果佇列為空就阻塞在那裡,當資料處理好後就傳送訊號到主執行緒。主執行緒要執行任務時只需要呼叫介面pushBack'Task(QVariant&data)就可以將資料放入佇列,這時會啟用一個子執行緒來完成該任務。

值得提醒的是該類最好不要指定父物件,因為我的設計中並沒有呼叫setAutoDelete(false),也就是退出run()函式會自動釋放,在主執行緒中大部分是與GUI相關的,這樣做會造成異常;另外一點只有在run()函式中的程式碼才是在子執行緒中執行的,如果將變數定義為類的成員變數,那麼如果使用QThreadPool::start()多次同一個物件的Worker就會有多個run()子執行緒執行,但是物件只有一個也就是變數只有一個,這樣就會造成多個子執行緒同時共用方執行緒的變數,造成異常。所以最好將子執行緒需要定義的東西直接在run()函式中新建,保證每個執行緒都有一份自已的變數,各自獨立。之前有一種設計就是把與鎖相關的函式封裝成另一外類用傳參的方法,傳入到業務相關的Worker中呼叫;但在釋放時,由於另一個類在主執行緒中釋放只需呼叫quit()函式非常快就釋放了,但子執行緒如果正在處理某個業務,當完成時去取下一個無效的資料時就可以退出,但由於需要用另一個類的指標來訪問,就會造成異常。

    終上,這是最安全的一種設計了。另外加入了Worker的管理類,用來啟動和管理Worker,下次介紹。