1. 程式人生 > >網路接收資料快取機制的分析和改進

網路接收資料快取機制的分析和改進

在非同步網路接收資料等場合,剝離資料接收和資料快取的對於提高處理能力具有重要影響。相比之下,接收即處理的做法不能很好的適應高速資料吞吐。因為後者的處理如果稍有延遲,就會導致嚴重的資料堵塞或資料丟失。

下面以libuv接收UDP資料包加入快取、應用程式從快取提取資料的應用為例(來於公司專案開發),總結如何實現一個有效的快取機制。

1.    分析失敗的快取案例

最初的快取資料提取是從快取佇列每次提取隊首的單元后將其刪除,libuv在另一個執行緒向快取佇列隨機的向佇列增加新的單元,兩個執行緒使用互斥鎖保護快取佇列這個共享資源。從原理上看,這沒有問題。在linux執行效果很流暢,但在Windows出現了嚴重的資料積壓,快取佇列快速增長,很短的時間就擴充套件到數十萬個單元。如果長期運行於Windows,程序必然會因為耗盡記憶體而崩潰。

直觀的結論是std::queue在Windows系統的出隊速度趕不上入隊速度。但深入分析會發現原因比這個複雜得多。在執行下面的用例之後足夠說明問題:

#include <thread>

#include <mutex>

using namespace std;

mutex m_buff_mutex;

bool bSend = true;

int n = 0;

void mysend()

{

   printlog("start %s.", __FUNCTION__);

   char *pch;

   while (bSend)

    {

       lock_guard<std::mutex> locker(m_buff_mutex);

       //pch = new char[1024];

        ////qData.push(pch);

       //qData.push_back(pch);

       //this_thread::sleep_for(chrono::microseconds(1000000));

       n++;

       printlog("snd n=%d.", n);

    }

   printlog("quit %s.", __FUNCTION__);

}

void myrecv()

{

   printlog("start %s.", __FUNCTION__);

   char szRcv[1024];

   int len=0,size;

   while (true)

    {

       lock_guard<std::mutex> locker(m_buff_mutex);

       //len = getdata(szRcv,size);

       //printlog("len=%dB, remain %d packages ,n=%d.",len, size,n);

       n--;

       printlog("rcv n=%d.", n);

    }

}

int main(int argc, char **argv)

{   

   std::thread  thrdrcv(myrecv);

   std::thread  thrdsnd(mysend);

    getchar();

   bSend = false;

   thrdsnd.join();

   getchar();

   return 0;

}

註釋的痕跡說明最初這個用例也是用於分析佇列的進出速度為什麼不匹配。後來開始懷疑原因是不是thrdrcv和thrdsnd執行機會不均所致,就進一步將用例的兩個執行緒簡化為向全域性變數n分別進行增減操作。可以肯定,此時兩個執行緒的複雜程度已經非常接近了,如果排程機會均等,那麼在大的時間範圍內,n的數值應該在0附近。

結果則是:在Windows系統,基本上每次執行都能觀察到n值在逐步增大;接下來在Linux系統測試,得出了相同的結論。結合原有機制在Linux沒有積壓資料的事實,再次證明了Linux的事實效能的確要遠高於Windows

其實,無論n是越來越大還是越來越小,都能說明同樣的問題,兩個執行函式幾乎相同的std::thread實際的排程機會非常不均,呈一邊倒的趨勢,這個現象在不同的作業系統都存在。網上查閱std的資料得知,其thread是系統級的,使用者很難干預優先順序——而現在的這個用例也並不適合干預兩個執行緒的優先順序。後來調整了main函式前兩行的次序,結果不變。

可見,導致出入隊速率不等的原因是出隊執行緒的執行機會小於libuv入隊執行緒(如果二者反過來麻煩會大一點,但也能克服:入隊執行緒每次更新佇列長度,這個變數不需要互斥保護,其他執行緒只能讀不能寫;出隊執行緒先檢查佇列長度,如果非零或大於某個值才鎖定快取佇列提取資料。本質上是減少出隊執行緒和入隊執行緒的競爭,提高後者的排程機會)。

2.    改進之後的解決方法

查明問題的原因之後,可以做出如下改進:

既然從快取佇列提取資料的機會相對較少,不如集中起來一次把佇列中現有的資料都提取出來。在Windows測試,發現這時快取佇列不再無限增長,只要處理速度跟得上,基本上控制在10個左右,即便偶有增長,也會逐漸下降。