送給大家一句話:
沒有一顆星,會因為追求夢想而受傷,當你真心渴望某樣東西時,整個宇宙都會來幫忙。 – 保羅?戈埃羅 《牧羊少年奇幻之旅》
🏕?🏕?🏕?🏕?🏕?🏕?
🗻🗻🗻🗻🗻🗻
進程通信實戰 —— 進程池項目
- 1 ??知識回顧
- 2 ??項目介紹
- 3 ??項目實現
- 3.1 ?創建信道和子進程
- 3.2 ?建立任務
- 3.3 ?控制子進程
- 3.4 ?回收信道和子進程
- 4 ??總結
- Thanks?(・ω・)ノ謝謝閱讀!!!
- 下一篇文章見!!!
1 ??知識回顧
在之前的講解中,我們深入探討了以下幾個方面:
- 父子進程的創建與管理:我們詳細講解了父子進程是如何建立的,以及子進程如何繼承父進程的代碼和數據。子進程通常用于完成特定的任務。
- 文件操作:我們學習了如何使用 read 和 write 操作文件,并了解了文件描述符(fd)的概念,從而能夠在文件中進行信息的讀取和寫入。
- 進程間通信:我們介紹了匿名管道,這是一種父子進程間進行通信的方式。通過共享資源,父子進程可以實現數據的傳遞和同步。
在接下來的內容中,讓我們把所學知識來進行運用,我們將探討進程池的概念和實現細節。
2 ??項目介紹
進程池是一種用于管理和復用進程的技術,它可以有效地管理系統資源并提高程序的性能和效率。通過維護一組預先創建的進程與管道,進程池可以避免頻繁地創建和銷毀進程,從而減少了系統開銷和資源浪費。
主要使用的是池化技術的思想:
池化技術是一種廣泛應用于系統開發中的優化策略,旨在通過復用資源來提高性能和效率。池化技術的核心思想是預先分配一組資源,并在需要時進行復用,而不是每次都重新創建和銷毀資源。
池化技術(Pooling)涉及創建和管理一組預先分配的資源,這些資源可以是進程、線程、數據庫連接或對象實例。在池化系統中,當請求到達時,它會從池中獲取一個空閑資源,使用完畢后將其歸還池中。這種方法避免了頻繁的創建和銷毀操作,從而顯著減少了系統開銷。
進程池就是通過預先創建若干個進程與管道,在需要進行任務時,選擇一個進程,通過管道發送信息,讓其完成工作。
進程池在實際項目中有廣泛的應用,尤其是在處理大量并發任務時,例如:網絡服務器中的請求處理、數據處理以及計算密集型任務。通過合理配置進程池的大小和參數,可以有效控制系統負載,提高整體響應速度。
3 ??項目實現
3.1 ?創建信道和子進程
首先我們需要建立一個信道類,來儲存管道及其對應的子進程信息。
//信道類
class Channel
{
public:Channel(pid_t id , int wfd , std::string name):_id(id) , _wfd(wfd) , _name(name) {}~Channel(){}void Close(){close(_wfd);}//關閉管道時需要等待對應子進程結束void WaitSub(){pid_t rid = waitpid(_id, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}pid_t GetId(){ return _id;}int GetWfd(){ return _wfd;}std::string GetName(){return _name ;}
private:pid_t _id ;//對應 子進程 idint _wfd ;//寫入端std::string _name ; //管道名稱
};
然后我們就建立若干個信道與子進程,創建子進程與信道的時候,把信息插入到信道容器中,完成儲存。子進程需要阻塞在讀取文件,等待父進程寫入信息:
void CreateChannel(int num , std::vector<Channel>* channel)
{//初始化任務InitTask();for(int i = 0 ; i < num ; i++){//創建管道int pipefd[2] = {0};int n = pipe(pipefd);if(n != 0){std::cout << "create pipe failed!" << std::endl;}//創建子進程pid_t id = fork();if(id == 0){//子進程 --- 只讀不寫close(pipefd[1]);work(pipefd[0]);close(pipefd[0]);exit(0);}//父進程close(pipefd[0]);std::string name = "Channel - " + std::to_string(i);//儲存信道信息channel->push_back( Channel(id , pipefd[1] , name) );}
}
這里提一下傳參的規范:
const &
:表示輸出型參數,即該參數是輸入型,不會被修改。常用于傳遞不需要修改的對象或數據。&
:表示輸入輸出型參數,即該參數既是輸入參數,又是輸出參數,函數可能修改其內容。*
:表示輸出型參數,通常用于傳遞指針,函數通過指針參數返回結果給調用者。
進行一下測試,看看是否可以這正常建立信道與子進程;
int main(int argc , char* argv[])
{//1. 通過main函數的參數 int argc char* argv[] (./ProcessPool 5) //判斷要創建多少個進程if(argc != 2){std::cout << "請輸入需要創建的信道數量 :" << std::endl;}std::vector<Channel> channel;int num = std::stoi(argv[1]);//2. 創建信道和子進程CreateChannel(num , &channel);//測試:for(auto t : channel){std::cout<< "==============="<<std::endl;std::cout<< "信道對應 name :" << t.GetName() <<std::endl;std::cout<< "信道對應子進程 pid :" << t.GetId() <<std::endl;std::cout<< "信道對應寫端 wfd :" << t.GetWfd() <<std::endl;}return 0;
}
完美,可以正常創建!!!
3.2 ?建立任務
完成了信道與子進程的創建,接下來我們就來設置一些任務。我們在.hpp文件
里直接把聲明定義寫在一起,確保代碼的模塊化和可維護性。
void Print()
{std::cout << "this is Print()"<< std::endl;
}void Fflush()
{std::cout << "this is Fflush()"<< std::endl;
}void Scanf()
{std::cout << "this is Scanf()"<< std::endl;
}
然后通過函數指針數來儲存這些函數,因為子進程會繼承父進程的數據,這樣通過一個數字下標即可確定調用的函數。只需要傳入 4 個字節的int類型,最大程度的減少了通信的成本!!!
#pragma once#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>#define TaskNum 3
//這個文件里是任務函數
typedef void(*task_t)();task_t tasks[TaskNum];
//...
//...三個函數
//...
void InitTask()
{srand(time(nullptr) ^ getpid() ^ 17777);tasks[0] = Print;tasks[1] = Fflush;tasks[2] = Scanf;
}
//執行任務!!!
void ExecuteTask(int num)
{if(num < 0 || num > 2) return;tasks[num]();
}
//隨機挑選一個任務
int SelectTask()
{return rand() % TaskNum;
}
3.3 ?控制子進程
首先通過 SelectTask() 選擇一個任務,然后選擇一個信道和子進程。需要注意的是,這里要依次調用每一組子進程,采用輪詢(Round-Robin)方案,以盡可能實現負載均衡。 然后發送任務(向信道寫入4字節的數組下標)
int SelectChannel(int n)
{//靜態變量做到輪詢方案static int next = 0;int channel = next;next++;next %= n;return channel;
}
void SendTaskCommond(Channel& channel , int TaskCommand )
{//寫入對應信息write(channel.GetWfd() , &TaskCommand , sizeof(TaskCommand));
}void CtrlProcessOnce(std::vector<Channel>& channel)
{//選擇一個任務int TaskCommand = SelectTask();//選擇一個進程與信道int ChannelNum = SelectChannel(channel.size());//發送信號//測試std::cout << "taskcommand: " << TaskCommand << " channel: "<< channel[ChannelNum].GetName() << " sub process: " << channel[ChannelNum].GetId() << std::endl; SendTaskCommond(channel[ChannelNum] ,TaskCommand);}
我們寫入之后,子進程就可以讀取任務并執行,注意子進程讀取只讀4個字節!!!如果讀取的個數不正確,那么就出現了錯誤,需要報錯!!!
//子進程運行函數
void work(int rfd)
{while(true){int Commond = 0;//等待相應int n = read(rfd , &Commond , sizeof(Commond));if(n == sizeof(int)){std::cout << "pid is : " << getpid() << " handler task" << std::endl;//執行命令std::cout << "commond :" << Commond << std::endl;ExecuteTask(Commond);}//寫端關閉else if(n == 0){std::cout << "sub Process:" << getpid() << std::endl;break;}}
}
//...//創建子進程pid_t id = fork();if(id == 0){//子進程 --- 只讀不寫close(pipefd[1]);work(pipefd[0]);close(pipefd[0]);exit(0);}
//...
進行一下測試:
成功執行任務!!!
3.4 ?回收信道和子進程
首先關閉信道寫端,這樣子進程會自己退出,然后父進程等待子進程退出(wait等待子進程 )不要出現僵尸進程 !!!
注意由于子進程會繼承父進程的數據,所以一個信道實際上會有多個寫端。為了不必要的錯誤,分開集中操作:先關閉所有寫端,再等待所以子進程。
void CleanUpChannel(std::vector<Channel>& channel)
{for(auto t : channel){t.Close();}for(auto t : channel){t.WaitSub();}
}
測試一下:
5 個子進程成功退出釋放!!!
4 ??總結
這樣,我們的進程池項目就完成了。不過,實際上我們還可以進一步優化,比如優化 work 函數,將其設置為回調函數,以實現完全解耦。
盡管如此,目前的實現已經能夠滿足我們的項目需求。一個面向過程的進程池項目就此完成!!!