66.QT-執行緒併發、QTcpServer併發、QThreadPool執行緒池

諾謙發表於2021-07-06

1.執行緒併發
一個程式內部能擁有多個執行緒並行執行。一個執行緒的執行可以被認為是一個CPU在執行該程式。
當一個程式執行在多執行緒下,就好像有多個CPU在同時執行該程式。
總之,多執行緒即可以這麼理解:多執行緒是處理高併發的一種程式設計方法,即併發需要用多執行緒實現。

2.如何分配執行緒數量
利用 CPU 核心數,應用併發程式設計來提高效率.執行緒IO時間所佔比例越高,需要越多執行緒;執行緒CPU時間所佔比例越高,需要越少執行緒。
理論上:

執行緒數量 = CPU 核數(邏輯)+ 1 

為什麼+1,《Java併發程式設計實戰》這麼說:

  • 計算(CPU)密集型的執行緒恰好在某時因為發生一個頁錯誤或者因其他原因而暫停,剛好有一個“額外”的執行緒,可以確保在這種情況下CPU週期不會中斷工作。

IO時間和CPU時間

  • IO操作實際就是不需要CPU介入,比如DMA請求,比如把內容從硬碟上讀到記憶體的過程,或者是從網路上接收資訊到本機記憶體的過程(sleep也可以算IO操作)
  • CPU操作實際就是進行大量的計算,消耗CPU資源,比如計算圓周率、對視訊進行高清解碼等等,全靠CPU的運算能力。

所以對於單核CPU而言:

最佳執行緒數 = 1 + (IO操作耗時/CPU操作耗時)

比如: IO操作耗時為500ms、CPU操作耗時為1500ms

最佳執行緒數 = 1 + (IO操作耗時/CPU操作耗時) = 1 + (500/1500) = 4

對於多核CPU而言:

最佳執行緒數 = CPU核心數 * (1 + (IO操作耗時/CPU操作耗時))

 

3.QTcpServer併發
QTcpServer要實現併發,首先需要子類化QTcpServer,然後重寫incomingConnection()函式.該函式定義如下所示:

[virtual protected] void QTcpServer::incomingConnection(qintptr socketDescriptor)
// 當有新連線時,首先會呼叫該函式,通過socketDescriptor引數(連線本機的套接字)建立一個QTcpSocket,設定套接字描述符,然後將QTcpSocket儲存在一個內部掛起連線列表中。最後觸發newConnection()。

我們重寫該函式,通過一個QThread將socketDescriptor引數傳到一個執行緒中,然後呼叫socketDescriptor()函式初始化一個QTcpSocket.從而達到QThread中生成一個新的QTcpSocket.

MyServer重寫如下所示:

void MyServer::incomingConnection(qintptr socketDescriptor)
{
  MyThread *thread = new MyThread(socketDescriptor, this);
  connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
  thread->start();
}

MyThread重寫run如下所示:

void MyThread::run()
{
  QTcpSocket tcpSocket;
  // 初始化一個QTcpSocket
  if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
    emit error(tcpSocket.error());
    return;
  }
  // 傳送字串
  tcpSocket.write("123456".toLocal8Bit());
  tcpSocket.disconnectFromHost();
  tcpSocket.waitForDisconnected();
}

然後在widget中:

server.listen(QHostAddress::AnyIPv4,8080);

每當一個client連線該server時,就會接收到"123456",然後被斷開.

 

4.執行緒池概念
假如伺服器突然來了500個任務,但是我們最佳執行緒數是20個,不可能立馬建立500個執行緒,因為執行緒過多會帶來排程開銷,進而影響快取區域性性和整體效能。
所以我們需要執行緒池,執行緒池不僅能夠保證核心的充分利用,還能防止過分排程。
執行緒池就相當於排隊去銀行辦理業務.排隊的人就是要處理業務的任務執行緒,客服就是執行緒池中容納辦理業務的最大數量.每當一個辦理業務的執行緒結束後,執行緒池就會從等待佇列中取出一個執行緒進行業務辦理.

 

5.QThreadPool併發執行緒池
在Qt中,執行緒池可以使用QThreadPool類,用來管理多個QThread的集合.
QThreadPool管理和回收單獨的QThread物件,以幫助減少使用執行緒的程式中建立執行緒的成本。
每個Qt應用程式都有一個全域性QThreadPool物件,可以通過呼叫globalInstance()來訪問(也可以自己定義個QThreadPool)
要使用一個QThreadPool執行緒,需要子類化QRunnable.並實現run()虛擬函式。
然後建立一個子類化QRunnable類的一個物件,並將其傳遞給QThreadPool::start(),來啟動一個執行緒.start()函式如下所示:

void QThreadPool::start(QRunnable *runnable, int priority = 0)
// 啟動一個runnable,如果當前執行緒池數量超過了maxThreadCount(),那麼將runnable新增到等待佇列中.
// priority引數可用於控制runnable在等待佇列中的被執行的順序。
// 預設runnable->autoDelete()返回true,執行緒池將獲得可執行物件的所有權,並且在runnable->run()返回後,可執行物件將被執行緒池自動刪除。
// 可以通過QRunnable::setAutoDelete()來更改自動刪除標誌 

QThreadPool支援通過在QRunnable::run()中呼叫tryStart(this)來多次執行同一個QRunnable。
如果autoDelete被啟用,QRunnable將在最後一個執行緒退出run函式時被刪除。
當autoDelete啟用時,使用相同的QRunnable多次呼叫start()會建立一個競爭條件,不建議這樣做。

在一定時間內未使用的執行緒將過期。預設超時時間為30000毫秒(30秒)。這可以使用setExpiryTimeout(int)來更改。設定負數將禁用過期機制。

呼叫maxThreadCount()查詢要使用的最大執行緒數。也可以使用setMaxThreadCount()來更改這個限制。預設值是QThread::idealThreadCount(). 該函式定義如下所示:

[static] int QThread::idealThreadCount()
//返回系統上可以執行的理想執行緒數。這是通過查詢系統中真實的和邏輯的處理器核的數量來完成的。
//如果無法檢測到處理器核數,則該函式返回1。

示例如下所示:

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QRunnable>
#include <QThreadPool>

class ComputeTask : public QRunnable
{
  int index;
  void run() override
  {
      const int work = 1000 * 1000 * 40; // 每個任務計數40000000次
      volatile int v = 0;
      for (int j = 0; j < work; ++j)
          ++v;
      qDebug() << index << " thread: " << QThread::currentThreadId();
  }

public:
  ComputeTask(int i) {
        index = i;
  }

};

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

    const int cnt = 200;   // 200個任務

    QThreadPool pool;
    qDebug() << "maxThreadCount: " << pool.maxThreadCount();
    for (int i = 0; i < cnt; ++i) {
        ComputeTask *compute = new ComputeTask(i);
        pool.start(compute);
    }

    return a.exec();
}

 列印如下所示:

 

 

 

 

 

相關文章