前言:
本篇博客所涉及到的代碼以同步到本人gitee:進(jìn)程池· 遲來的grown/linux – 碼云 – 開源中國(guó)
一、池化技術(shù)
在之前的學(xué)習(xí)中,多多少少都聽說過池,例如內(nèi)存池,線程池等等。
那這些池到底是干什么的呢?池又指的是什么呢?
這種思想就好比在vector動(dòng)態(tài)數(shù)組擴(kuò)容一樣,是按照2或者1.5倍進(jìn)行擴(kuò)容,這樣減少開辟空間的開銷從而提高效率。
二、進(jìn)程池原理
我們也了解了進(jìn)程間通信——管道,可以實(shí)現(xiàn)進(jìn)程之間的通信技術(shù);那現(xiàn)在我們可不可以利用父進(jìn)程管理所有的子進(jìn)程,并且讓子進(jìn)程去完成某些任務(wù)呢?

那進(jìn)程池又是什么東西呢?
池化技術(shù)是預(yù)先創(chuàng)建資源,通過復(fù)用來提升系統(tǒng)性能和資源利用率。
這里子進(jìn)程要執(zhí)行任務(wù),如果這里父進(jìn)程要傳輸信息給子進(jìn)程時(shí)再去創(chuàng)建進(jìn)程,子進(jìn)程執(zhí)行完任務(wù)之后就退出;這勢(shì)必存儲(chǔ)非常多的系統(tǒng)調(diào)用,而系統(tǒng)調(diào)用也是有成本的。那這里我們就可以預(yù)先創(chuàng)建多個(gè)子進(jìn)程,讓這些子進(jìn)程等待父進(jìn)程傳輸信息;執(zhí)行完任務(wù)后繼續(xù)等待父進(jìn)程傳輸信息。
這樣我們預(yù)先創(chuàng)建進(jìn)程,讓這些進(jìn)程執(zhí)行任務(wù)而不是在要執(zhí)行任務(wù)時(shí)再創(chuàng)建進(jìn)程;并且執(zhí)行完任務(wù)的子進(jìn)程還可以繼續(xù)完成下一個(gè)任務(wù),這樣通過復(fù)用進(jìn)程來通過系統(tǒng)性能和進(jìn)制資源的利用率。
三、進(jìn)程池實(shí)現(xiàn)
了解了進(jìn)程池原理,現(xiàn)在來看它應(yīng)該如何去實(shí)現(xiàn):
首先,我們要預(yù)先創(chuàng)建一個(gè)進(jìn)程池,并把它管理起來。其次,父進(jìn)程要通過傳輸信息來控制子進(jìn)程完成不同的任務(wù);父進(jìn)程就要發(fā)送信息,子進(jìn)程就要接受信息并執(zhí)行任務(wù)最后,進(jìn)程池能夠被創(chuàng)建出來,當(dāng)然也要能夠被釋放(銷毀)。1. 描述進(jìn)程池
我們要預(yù)先創(chuàng)建一個(gè)進(jìn)程池,并且要將它管理起來;那就要像將這個(gè)進(jìn)程池描述出來;如何描述并管理這個(gè)進(jìn)程池呢?
這里我們要實(shí)現(xiàn)的本質(zhì)還是要進(jìn)行父子進(jìn)程間通信,要讓父進(jìn)程發(fā)信息來控制子進(jìn)程;
先描述
那站著父進(jìn)程的角度:
所以就可以設(shè)計(jì)一個(gè)channel類用來描述一個(gè)管道文件;那這個(gè)類具有哪些屬性呢?
wfd:父進(jìn)程中寫端的文件描述符pid:管道文件對(duì)應(yīng)子進(jìn)程的pid,后續(xù)用來回收子進(jìn)程代碼語(yǔ)言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel{public: channel(int wfd, int pid) : _wfd(wfd), _pid(pid) { } ~channel() {}private: int _wfd; int _pid;};
再組織
一個(gè)父進(jìn)程它要?jiǎng)?chuàng)建多個(gè)管道文件也就是多個(gè)子進(jìn)程,就要將這些子進(jìn)程管理起來;
所以,這里可以設(shè)計(jì)一個(gè)channel_manage類來講管道文件channel管理起來。
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel_manage{public: channel_manage() { } ~channel_manage() { }private: std::vector<channel> _channels;};
描述進(jìn)程池
在進(jìn)程池中,一定是存在上面的組織管道文件channel_manage,因?yàn)槲覀円獙?duì)其進(jìn)行管理;
在進(jìn)程池中還可能存在其他信息:進(jìn)程負(fù)載情況,進(jìn)程狀態(tài)等等。
這里就只記錄進(jìn)程的數(shù)量。
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel_pool{public: const int NUM = 5; // 進(jìn)程池中進(jìn)程數(shù)量 channel_pool() : _processnum(NUM) {} ~channel_pool() {}private: channel_manage _cm; int _processnum;};
2. 初始化進(jìn)程池
描述出了進(jìn)程池channel_pool,現(xiàn)在我們能夠根據(jù)這個(gè)channel_pool創(chuàng)建出一個(gè)進(jìn)程池對(duì)象,但是創(chuàng)建出來的這個(gè)進(jìn)程池對(duì)象里面什么是都沒有,一個(gè)進(jìn)程都沒有。
所以,我們就要對(duì)進(jìn)程池進(jìn)行初始化:那如何初始化呢?(這里暫定進(jìn)程池中進(jìn)程個(gè)數(shù)為5個(gè))
簡(jiǎn)單來說初始化進(jìn)程池時(shí)就要將所有的子進(jìn)程創(chuàng)建出來,那創(chuàng)建完子進(jìn)程,子進(jìn)程應(yīng)該做什么呢,父進(jìn)程又該做什么呢?
這里創(chuàng)建子進(jìn)程就是為了讓子進(jìn)程完成某個(gè)任務(wù),所以創(chuàng)建子進(jìn)程之后(記得關(guān)閉不用的文件描述符),就讓子進(jìn)程等待父進(jìn)程發(fā)送信息;在子進(jìn)程接受到信息之后再去完成任務(wù)。而創(chuàng)建完子進(jìn)程之后,父進(jìn)程關(guān)閉不用的文件描述符,然后就要在_cm中新增一個(gè)管道文件channel對(duì)象。
而channel在channel_manage中,所以channel_manage就要通過新增channel對(duì)象的方法。
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
//這里只顯示新增方法和成員變量class channel_manage{public: void _insert(int wfd, int pid) { _channels.emplace_back(wfd,pid); }private: std::vector<channel> _channels;};class channel_pool{public: void work(int rfd){};//任務(wù)方法 void _init() { for(int i = 0;i<_processnum;i++) { int fd[2]; int n = pipe(fd); if(n < 0) { std::cerr<<"pipe failed"<<std::endl; exit(1); } //創(chuàng)建子進(jìn)程 int id = fork(); if(id < 0) { std::cerr<<"fork failed"<<std::endl; exit(1); } else if(id == 0) { //child close(fd[1]);//關(guān)閉寫端 _work(fd[0]);//等到父進(jìn)程發(fā)送信息 close(fd[0]); exit(1); } //parent close(fd[0]); //在_cm中新增channel對(duì)象 _cm._insert(fd[1],id); } }private: channel_manage _cm; int _processnum;};
3. 子進(jìn)程接受信息
完成了上述操作,現(xiàn)在進(jìn)程池被創(chuàng)建出來,也被初始化了。
這里子進(jìn)程是直到管道文件的文件描述符的就是fd[0];所以子進(jìn)程就要在fd[0]文件描述符對(duì)應(yīng)的管道文件中讀取數(shù)據(jù),然后根據(jù)讀取到的數(shù)據(jù)來執(zhí)行不同的任務(wù)。
那父子之間發(fā)送什么樣的信息呢?
這里就采用一個(gè)整數(shù)對(duì)應(yīng)一個(gè)任務(wù),讓父進(jìn)程發(fā)送一個(gè)整數(shù)給子進(jìn)程。
子進(jìn)程讀取數(shù)據(jù):
讀取數(shù)據(jù)失敗就返回-1;寫端退出就返回0;返回值大于0 :表示讀取到的實(shí)際字節(jié)數(shù)。(當(dāng)讀取到的字節(jié)數(shù)不等于4,就表示不是按照int讀取的,就指讀取到一個(gè)位置信息)。代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
void work(int rfd) { while (true) { int massage = 0; int n = read(rfd, &massage, sizeof(massage)); if (n < 0) { std::cerr << "read failed" << std::endl; exit(1); } else if (n == 0) { std::cout << "exit, because write exit" << std::endl; break; } else if (n != 4) { std::cout << "unkonw massage : " << massage << std::endl; } // 讀取成功,執(zhí)行任務(wù) std::cout << "receive massage : " << massage << std::endl; } }
4. 父進(jìn)程發(fā)送信息
實(shí)現(xiàn)子進(jìn)程接受信息,現(xiàn)在來看父進(jìn)程發(fā)送信息;如何發(fā)送呢?
這里父進(jìn)程發(fā)送信息無(wú)非就以下三個(gè)問題:
給哪一個(gè)進(jìn)程發(fā)送信息?發(fā)送什么信息?如何發(fā)送信息?
選擇一個(gè)進(jìn)程發(fā)送信息
發(fā)送什么信息?
如何發(fā)送信息
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel{public: bool _send(int taskcode) { int n = write(_wfd, &taskcode, sizeof(taskcode)); if (n < 0) { // 寫入失敗 std::cerr << "write failed" << std::endl; return false; } // 寫入成功 return true; }private: int _wfd; int _pid;};class channel_manage{public: channel &select() { auto &ret = _channels[_next]; _next++; _next %= _channels.size(); return ret; }private: std::vector<channel> _channels; int _next = 0;};class channel_pool{public: void send(int taskcode) { // 選擇一個(gè)進(jìn)程 auto &c = _cm._select(); // 任務(wù)碼由上層調(diào)用決定 // 發(fā)送信息 c._send(taskcode); }private: channel_manage _cm; int _processnum;};
到這里,進(jìn)程池就可以大致的運(yùn)行起來了,這里簡(jiǎn)單測(cè)試一下
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
//test.cc#include "channelpool.hpp"int main(){ srand((int)time(nullptr)); channel_pool cp; cp._init(); cp.Print(); // 輸出進(jìn)程池中的所有進(jìn)程信息 while (1) { int taskcode = rand() % 5; cp.send(taskcode); std::cout << std::endl; sleep(1); } return 0;}

進(jìn)程池也是能夠正常運(yùn)行,子進(jìn)程也能夠接受到父進(jìn)程發(fā)送的信息。
5. 回收進(jìn)程池資源
做完上述的內(nèi)容,這里的進(jìn)程池就大致可以運(yùn)行起來;
但是現(xiàn)在還缺少一個(gè)步驟,那就是回收進(jìn)程池的資源。
如何回收進(jìn)程池的資源呢?
首先,要關(guān)閉父進(jìn)程中所有的管道文件的文件描述符。其次就是父進(jìn)程等待子進(jìn)程退出,回收子進(jìn)程。
而_wfd文件描述符、_pid子進(jìn)程pid都封裝在channel中;
如何關(guān)閉管道文件,如何等待子進(jìn)程退出,這都要channel來提供。
而我們想要通過進(jìn)程池對(duì)象調(diào)用回收函數(shù),那channel_manage也要提供對(duì)應(yīng)的關(guān)閉文件和等待子進(jìn)程退出的函數(shù)。
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel{public: void _close() { close(_wfd); } void _wait() { wait(nullptr); }private: int _wfd; int _pid;};class channel_manage{public: void _close() { for (auto &channel : _channels) { channel._close(); std::cout << "關(guān)閉管道文件 : " << channel.getname() << std::endl; } } void _wait() { for (auto &channel : _channels) { channel._wait(); std::cout << "等待子進(jìn)程退出 : " << channel.getname() << std::endl; } }private: std::vector<channel> _channels; int _next = 0;};class channel_pool{public: void _quit() { //關(guān)閉所有w端文件 _cm._close(); //回收子進(jìn)程 _cm._wait(); }private: channel_manage _cm; int _processnum;};
這里就發(fā)送一次信息然后退出,測(cè)試一下:
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
//test.cc#include "channelpool.hpp"int main(){ srand((int)time(nullptr)); channel_pool cp; cp._init(); cp.Print(); // 輸出進(jìn)程池中的所有進(jìn)程信息 int cnt = 1; while (cnt--) { int taskcode = rand() % 5; cp.send(taskcode); std::cout << std::endl; } cp._quit(); sleep(10); return 0;}

四、隱藏的問題
在上述的代碼中存在一個(gè)隱藏的問題:
現(xiàn)在來看如果這樣回收進(jìn)程池資源:
代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制
class channel{public: void _close() { close(_wfd); } void _wait() { wait(nullptr); }private: int _wfd; int _pid;};class channel_manage{public: void _quit() { for (auto &channel : _channels) { channel._close(); std::cout << "關(guān)閉管道文件 : " << channel.getname() << std::endl; channel._wait(); std::cout << "等待子進(jìn)程退出 : " << channel.getname() << std::endl; } }private: std::vector<channel> _channels; int _next = 0;};class channel_pool{public: void _quit() { //關(guān)閉所有w端文件 _cm._close(); //回收子進(jìn)程 _cm._wait(); }private: channel_manage _cm; int _processnum;};
這樣關(guān)閉一個(gè)寫端,等待一個(gè)子進(jìn)程退出。

我們會(huì)發(fā)現(xiàn),程序卡到了這里,這是為什么呢?
這樣父進(jìn)程在創(chuàng)建子進(jìn)程時(shí),這個(gè)子進(jìn)程的文件描述符表中是存儲(chǔ)前面創(chuàng)建的管道文件的w端;這樣我們調(diào)用channel類的_close就只關(guān)閉了父進(jìn)程的w端,在其他進(jìn)程中還存在管道文件的w端。這樣子進(jìn)程就會(huì)阻塞到read出,等待w端關(guān)閉,而父進(jìn)程就等待子進(jìn)程退出,這樣程序就卡在了這樣。

而第一個(gè)創(chuàng)建的子進(jìn)程,文件描述符4指向自己的w端,然后被關(guān)閉了。第二個(gè)創(chuàng)建的子進(jìn)程,4指向第一個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。第三個(gè)創(chuàng)建的子進(jìn)程,4指向第一個(gè)子進(jìn)程管道文件的w端,5指向第二個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。…
這樣,在回收進(jìn)程池的資源時(shí),關(guān)閉父進(jìn)程第一個(gè)子進(jìn)程的w端,此時(shí)第二、第三…個(gè)子進(jìn)程的文件描述符中都存在第一個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。
解決方法:
在回收進(jìn)程池資源時(shí),從最后一個(gè)被創(chuàng)建的子進(jìn)程開始回收。 因?yàn)橐粋€(gè)子進(jìn)程中只存在比自己創(chuàng)建的早的子進(jìn)程的w端;所以最后一個(gè)被創(chuàng)創(chuàng)建的子進(jìn)程的w端只有父進(jìn)程存在。 在創(chuàng)建子進(jìn)程時(shí),關(guān)閉比該子進(jìn)程創(chuàng)建早的子進(jìn)程的w端。
對(duì)于第一種方法這里就不演示了,來看第二種方法
對(duì)于第二種方法可能存在疑問:子進(jìn)程和父進(jìn)程是同時(shí)執(zhí)行的,那子進(jìn)程不會(huì)把自己的w段關(guān)閉嗎?那不就同一個(gè)文件描述符關(guān)閉2次了嗎?
實(shí)現(xiàn)第二種方法也很簡(jiǎn)單,只需在創(chuàng)建子進(jìn)程之后,讓子進(jìn)程調(diào)用一次channel_massage的_close函數(shù)即可。
這里就不演示了。
到這里,本篇文章內(nèi)容就結(jié)束了