­

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

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();
}

 列印如下所示: