C++高并發(fā)三板斧:多進程、多線程、IO多路復用
在日常生活里,我們常常會進行多任務處理。就像你一邊在電腦上用 Word 寫報告,一邊聽著音樂,同時微信還在接收消息。這時候,電腦看似同時在執(zhí)行多個任務,其實就是一種并發(fā)的體現(xiàn)。在編程領域,并發(fā)編程同樣有著至關重要的地位。對于 C++ 編程而言,并發(fā)編程能夠顯著提升程序的性能和響應速度。在服務器開發(fā)中,服務器需要同時處理大量客戶端的請求,如果采用并發(fā)編程技術,就可以讓服務器在同一時間內(nèi)響應多個請求,大大提高了服務器的吞吐量和效率。
在游戲開發(fā)里,并發(fā)編程也發(fā)揮著關鍵作用,游戲中需要同時處理玩家的操作、畫面的渲染、物理效果的模擬等多個任務,并發(fā)編程能夠讓這些任務并行執(zhí)行,從而為玩家?guī)砀恿鲿澈驼鎸嵉挠螒蝮w驗。并發(fā)編程在 C++ 的眾多應用領域中都有著不可或缺的地位,接下來就讓我們深入探索 C++ 并發(fā)編程中的多進程、多線程和 IO 多路復用技術。
Part1.多進程:獨立運行的 “小世界”
1.1多進程是什么?
多進程,簡單來說,就是一個程序同時運行多個獨立的任務,每個任務都由一個進程來負責。在操作系統(tǒng)中,進程是資源分配的最小單位,它擁有獨立的內(nèi)存空間、系統(tǒng)資源(如文件描述符、信號處理等)以及獨立的執(zhí)行環(huán)境 。這就好比在一個小區(qū)里,每個房子都有自己獨立的空間、設施,住戶在自己的房子里生活,互不干擾。進程之間也是如此,它們各自獨立運行,互不影響。
1.2創(chuàng)建進程的魔法:fork () 函數(shù)
在 Linux 系統(tǒng)中,我們可以使用 fork () 函數(shù)來創(chuàng)建新的進程。fork () 函數(shù)的作用是復制當前進程,生成一個子進程。這個子進程幾乎是父進程的一個副本,它擁有與父進程相同的代碼、數(shù)據(jù)和文件描述符等。
fork () 函數(shù)的原理并不復雜。當父進程調(diào)用 fork () 時,操作系統(tǒng)會為子進程分配一個新的進程控制塊(PCB),用于管理子進程的相關信息。子進程會繼承父進程的大部分資源,包括內(nèi)存空間的映射(但有寫時復制機制,后面會詳細介紹)、打開的文件描述符等。
fork () 函數(shù)有一個獨特的返回值特性:在父進程中,它返回子進程的進程 ID(PID);而在子進程中,它返回 0。通過這個返回值,我們可以區(qū)分當前是父進程還是子進程在執(zhí)行,從而讓它們執(zhí)行不同的代碼邏輯。下面是一個簡單的代碼示例:
#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t pid;
    // 調(diào)用fork()創(chuàng)建子進程
    pid = fork();
    if (pid < 0) {
        // fork()失敗
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子進程
        printf("I am the child process, my PID is %d, my parent's PID is %d\n", getpid(), getppid());
    } else {
        // 父進程
        printf("I am the parent process, my PID is %d, my child's PID is %d\n", getpid(), pid);
    }
    return 0;
}運行這段代碼,你會看到父進程和子進程分別輸出不同的信息,證明它們是獨立運行的。
1.3進程間通信(IPC)的橋梁
雖然進程之間相互獨立,但在實際應用中,我們常常需要它們之間進行通信和數(shù)據(jù)共享。這就需要用到進程間通信(IPC,Inter - Process Communication)機制。常見的 IPC 方式有管道(Pipe)、消息隊列(Message Queue)、共享內(nèi)存(Shared Memory)等。
①管道(Pipe):管道是一種半雙工的通信方式,數(shù)據(jù)只能單向流動,而且只能在具有親緣關系(如父子進程)的進程間使用。管道可以看作是一個特殊的文件,它在內(nèi)核中開辟了一塊緩沖區(qū),進程通過讀寫這個緩沖區(qū)來進行通信。例如,在 Linux 系統(tǒng)中,可以使用 pipe () 函數(shù)創(chuàng)建管道。下面是一個簡單的父子進程通過管道通信的示例代碼:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
    int pipe_fd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];
    // 創(chuàng)建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe creation failed");
        return 1;
    }
    // 創(chuàng)建子進程
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子進程
        close(pipe_fd[0]); // 關閉讀端
        const char *message = "Hello from child";
        write(pipe_fd[1], message, strlen(message));
        close(pipe_fd[1]); // 關閉寫端
    } else {
        // 父進程
        close(pipe_fd[1]); // 關閉寫端
        ssize_t bytes_read = read(pipe_fd[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("Received from child: %s\n", buffer);
        }
        close(pipe_fd[0]); // 關閉讀端
    }
    return 0;
}②消息隊列(Message Queue):消息隊列是一種異步通信機制,它允許進程向隊列中發(fā)送消息,也可以從隊列中接收消息。消息隊列中的消息具有特定的格式,每個消息都有一個類型。不同類型的消息可以被不同的進程接收,這樣就實現(xiàn)了多個進程之間的通信。在 Linux 系統(tǒng)中,可以使用 msgget ()、msgsnd () 和 msgrcv () 等函數(shù)來操作消息隊列。
③共享內(nèi)存(Shared Memory):共享內(nèi)存是一種高效的 IPC 方式,它允許多個進程共享同一塊物理內(nèi)存空間。進程可以直接讀寫共享內(nèi)存中的數(shù)據(jù),而不需要進行數(shù)據(jù)拷貝,因此速度非???。但是,由于多個進程共享內(nèi)存,需要注意同步和互斥問題,以避免數(shù)據(jù)沖突。在 Linux 系統(tǒng)中,可以使用 shmget ()、shmat () 和 shmdt () 等函數(shù)來實現(xiàn)共享內(nèi)存。
1.4多進程的優(yōu)缺點剖析
多進程在編程中有著獨特的優(yōu)勢,同時也存在一些不足。
優(yōu)點:
- 進程獨立性:由于每個進程都有獨立的內(nèi)存空間和執(zhí)行環(huán)境,一個進程的崩潰不會影響其他進程的運行。這使得程序更加健壯和穩(wěn)定,特別適合那些對穩(wěn)定性要求較高的應用場景,如服務器程序。
 - 資源分配清晰:進程是資源分配的最小單位,操作系統(tǒng)對進程的資源分配和管理相對簡單。每個進程可以獨立地申請和使用系統(tǒng)資源,不會出現(xiàn)資源競爭導致的死鎖等問題(當然,進程間通信時仍需注意同步)。
 
缺點:
- 進程間通信復雜:雖然有多種 IPC 機制,但每種機制都有其使用場景和限制,實現(xiàn)復雜的通信邏輯時難度較大。例如,共享內(nèi)存需要手動處理同步和互斥問題,否則容易出現(xiàn)數(shù)據(jù)不一致的情況。
 - 系統(tǒng)開銷大:創(chuàng)建和銷毀進程需要操作系統(tǒng)進行大量的工作,包括分配和回收內(nèi)存、創(chuàng)建和銷毀 PCB 等,這會消耗較多的系統(tǒng)資源和時間。而且,每個進程都有自己獨立的內(nèi)存空間,導致內(nèi)存占用較大,在系統(tǒng)資源有限的情況下,可能會影響程序的性能。
 
在實際應用中,我們需要根據(jù)具體的需求和場景來權衡是否使用多進程。如果任務之間需要高度的獨立性和穩(wěn)定性,且對資源開銷不太敏感,多進程是一個不錯的選擇;但如果任務之間需要頻繁通信和協(xié)作,或者系統(tǒng)資源有限,可能需要考慮其他并發(fā)編程方式。
Part2.多線程:輕量級的協(xié)作能手
多線程是指在同一個進程內(nèi),存在多個獨立的執(zhí)行流,它們可以同時(并發(fā))執(zhí)行不同的任務 。與進程不同,線程是進程的一個子集,是操作系統(tǒng)進行運算調(diào)度的最小單位,線程之間共享進程的資源,如內(nèi)存空間、文件描述符等。這就好比在一個房子里,不同的人可以同時進行不同的活動,有人在看電視,有人在做飯,有人在看書,他們共享房子里的空間、水電等資源。線程之間的這種協(xié)作和共享資源的特性,使得多線程編程在很多場景下能夠提高程序的執(zhí)行效率和響應速度。
2.1C++ 中的線程魔法:std::thread
在 C++11 之前,C++ 標準庫并沒有提供對線程的直接支持,開發(fā)者需要依賴操作系統(tǒng)特定的 API(如 Windows 下的 CreateThread 和 Linux 下的 pthread 庫)來進行多線程編程,這使得代碼的可移植性較差。C++11 引入了<thread>頭文件,其中的std::thread類為我們提供了一種跨平臺的線程操作方式,大大簡化了多線程編程。
使用std::thread創(chuàng)建線程非常簡單,只需要將一個可調(diào)用對象(如函數(shù)、lambda 表達式或函數(shù)對象)傳遞給std::thread的構造函數(shù)即可。例如,我們可以創(chuàng)建一個簡單的線程來打印一條消息:
#include <iostream>
#include <thread>
// 線程執(zhí)行的函數(shù)
void print_message() {
    std::cout << "This is a message from the thread." << std::endl;
}
int main() {
    // 創(chuàng)建線程,傳入print_message函數(shù)
    std::thread t(print_message);
    // 等待線程執(zhí)行完畢
    t.join();
    return 0;
}在這個例子中,std::thread t(print_message)創(chuàng)建了一個新的線程t,并將print_message函數(shù)作為線程的執(zhí)行體。t.join()的作用是阻塞當前線程(在這里是主線程),直到線程t執(zhí)行完畢。這樣可以確保在主線程結束之前,子線程已經(jīng)完成了它的任務。
除了傳遞普通函數(shù),我們還可以使用 lambda 表達式來創(chuàng)建線程,這樣可以更方便地捕獲和使用外部變量:
#include <iostream>
#include <thread>
int main() {
    int value = 42;
    // 使用lambda表達式創(chuàng)建線程
    std::thread t([&]() {
        std::cout << "The value is: " << value << std::endl;
    });
    t.join();
    return 0;
}在這個例子中,lambda 表達式[&]() { std::cout << "The value is: " << value << std::endl; }捕獲了外部變量value的引用,并在新線程中使用它。
2.2線程同步:避免沖突的規(guī)則
雖然多線程能夠提高程序的執(zhí)行效率,但由于多個線程共享進程的資源,當它們同時訪問和修改共享數(shù)據(jù)時,就可能會出現(xiàn)數(shù)據(jù)競爭(Data Race)和不一致的問題。例如,假設有兩個線程同時對一個共享的整數(shù)變量進行加 1 操作,如果沒有適當?shù)耐綑C制,最終的結果可能并不是我們期望的。這是因為在多線程環(huán)境下,線程的執(zhí)行順序是不確定的,兩個線程可能會同時讀取變量的值,然后分別進行加 1 操作,最后再寫回結果,這樣就會導致其中一個加 1 操作被覆蓋,最終結果比預期少 1。
為了解決這些問題,我們需要使用線程同步機制。C++ 標準庫提供了多種線程同步工具,其中最常用的是互斥鎖(std::mutex)和條件變量(std::condition_variable)。
①互斥鎖(std::mutex):互斥鎖是一種最基本的同步機制,它就像一把鎖,一次只能被一個線程持有。當一個線程獲取到互斥鎖后,其他線程如果試圖獲取該鎖,就會被阻塞,直到持有鎖的線程釋放它。這樣就保證了在同一時間內(nèi),只有一個線程能夠訪問被保護的共享資源。
下面是一個使用互斥鎖保護共享資源的示例代碼:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;  // 創(chuàng)建一個互斥鎖
int shared_data = 0;  // 共享數(shù)據(jù)
// 線程執(zhí)行的函數(shù)
void increment() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock();  // 加鎖
        ++shared_data;  // 訪問和修改共享數(shù)據(jù)
        mtx.unlock();  // 解鎖
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "The final value of shared_data is: " << shared_data << std::endl;
    return 0;
}在這個例子中,mtx.lock()和mtx.unlock()分別用于加鎖和解鎖。在訪問共享數(shù)據(jù)shared_data之前,線程會先獲取互斥鎖,確保沒有其他線程同時訪問;訪問結束后,再釋放鎖,讓其他線程有機會獲取。這樣就避免了數(shù)據(jù)競爭問題,保證了最終結果的正確性。
②條件變量(std::condition_variable):條件變量通常與互斥鎖配合使用,用于線程之間的通信和同步。它允許線程在某個條件滿足之前等待,當條件滿足時,其他線程可以通知等待的線程繼續(xù)執(zhí)行。例如,在生產(chǎn)者 - 消費者模型中,生產(chǎn)者線程生產(chǎn)數(shù)據(jù)后,通過條件變量通知消費者線程有新的數(shù)據(jù)可用;消費者線程在沒有數(shù)據(jù)時,通過條件變量等待,避免無效的輪詢。
下面是一個簡單的生產(chǎn)者 - 消費者模型的示例代碼,展示了條件變量的使用:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;  // 共享數(shù)據(jù)隊列
// 生產(chǎn)者線程函數(shù)
void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        data_queue.push(i);  // 生產(chǎn)數(shù)據(jù)
        lock.unlock();
        cv.notify_one();  // 通知一個等待的消費者線程
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
// 消費者線程函數(shù)
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return!data_queue.empty(); });  // 等待數(shù)據(jù)到來
        int data = data_queue.front();  // 消費數(shù)據(jù)
        data_queue.pop();
        lock.unlock();
        std::cout << "Consumed: " << data << std::endl;
        if (data == 9) break;  // 消費完所有數(shù)據(jù)后退出
    }
}
int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}在這個例子中,cv.wait(lock, [] { return!data_queue.empty(); });表示消費者線程在data_queue為空時等待,直到data_queue不為空(即有數(shù)據(jù)可用)時才繼續(xù)執(zhí)行。cv.notify_one()則是生產(chǎn)者線程在生產(chǎn)數(shù)據(jù)后,通知一個等待的消費者線程。
2.3多線程的優(yōu)缺點分析
優(yōu)點:
- 資源共享與通信方便:線程共享進程的資源,這使得線程之間的數(shù)據(jù)共享和通信非常方便。它們可以直接訪問進程內(nèi)的共享內(nèi)存,無需像進程間通信那樣使用復雜的 IPC 機制。例如,在一個服務器程序中,多個線程可以共享同一個數(shù)據(jù)庫連接池,方便地進行數(shù)據(jù)庫操作。
 - 上下文切換開銷?。合啾冗M程,線程的上下文切換開銷較小。因為線程共享進程的資源,在進行上下文切換時,只需要保存和恢復線程的寄存器狀態(tài)等少量信息,而不需要像進程切換那樣保存和恢復整個進程的資源狀態(tài),這使得線程能夠更快速地切換執(zhí)行,提高了程序的并發(fā)性能。
 - 提高程序響應性:在圖形界面應用程序中,多線程可以將耗時的操作(如文件讀取、網(wǎng)絡請求等)放在后臺線程執(zhí)行,而主線程可以繼續(xù)處理用戶界面的更新和響應,避免界面卡頓,提高用戶體驗。
 
缺點:
- 線程同步問題:如前面所述,多線程共享資源容易導致數(shù)據(jù)競爭和不一致的問題,需要使用同步機制來解決。然而,不正確地使用同步機制(如死鎖、鎖粒度不當?shù)龋е鲁绦虺霈F(xiàn)難以調(diào)試的錯誤,增加開發(fā)和維護的難度。
 - 編程復雜度增加:多線程編程需要考慮線程的生命周期管理、同步問題、資源競爭等,使得程序的邏輯變得更加復雜。調(diào)試多線程程序也比單線程程序困難得多,因為線程的執(zhí)行順序不確定,問題可能難以重現(xiàn)和定位。
 - 性能問題:雖然多線程在理論上可以提高程序的執(zhí)行效率,但在實際應用中,如果線程數(shù)量過多,會導致上下文切換頻繁,消耗大量的 CPU 時間,反而降低程序的性能。而且,線程之間的同步操作(如加鎖和解鎖)也會帶來一定的開銷,如果不合理使用,也會影響程序的性能。
 
Part3.IO多路復用:高效的I/O管理術
3.1什么是 IO 多路復用
在編程世界里,I/O 操作(如文件讀寫、網(wǎng)絡通信等)是非常常見的任務。傳統(tǒng)的 I/O 模型中,一個線程通常只能處理一個 I/O 操作,如果要處理多個 I/O 操作,就需要創(chuàng)建多個線程或者進程,這會帶來資源浪費和復雜度增加的問題。
IO 多路復用(I/O Multiplexing)技術的出現(xiàn),很好地解決了這個問題。它允許一個進程同時監(jiān)聽多個文件描述符(File Descriptor,簡稱 fd,在 Linux 系統(tǒng)中,一切皆文件,文件描述符是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引)的 I/O 事件,當某個文件描述符就緒(有數(shù)據(jù)可讀、可寫或有異常發(fā)生)時,進程能夠及時得到通知并進行相應的處理 。這就好比一個餐廳服務員,他可以同時照顧多桌客人,當某一桌客人有需求(比如需要加水、上菜等)時,服務員能夠及時響應,而不是一個服務員只服務一桌客人,造成資源浪費。
3.2常見的 IO 多路復用方式
在 Linux 系統(tǒng)中,常見的 IO 多路復用方式有 select、poll 和 epoll,它們各自有著不同的特點和適用場景。
①select:select 是最早出現(xiàn)的 IO 多路復用方式,它通過一個select()系統(tǒng)調(diào)用來監(jiān)視多個文件描述符的數(shù)組。select()函數(shù)的原型如下:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);參數(shù)說明
- nfds:需要監(jiān)聽的文件描述符的最大值加 1。
 - readfds:需要監(jiān)聽讀事件的文件描述符集合。
 - writefds:需要監(jiān)聽寫事件的文件描述符集合。
 - exceptfds:需要監(jiān)聽異常事件的文件描述符集合。
 - timeout:設置select函數(shù)的超時時間,如果為NULL,則表示一直阻塞等待。
 
返回值說明
- 成功時返回就緒文件描述符個數(shù)。
 - 超時時返回 0。
 - 出錯時返回負值。
 
使用select時,需要先初始化文件描述符集合,將需要監(jiān)聽的文件描述符添加到對應的集合中,然后調(diào)用select函數(shù)。當select返回后,通過檢查返回值和文件描述符集合,判斷哪些文件描述符就緒,進而進行相應的讀寫操作。例如:
#include <iostream>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket creation failed");
        return 1;
    }
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_socket);
        return 1;
    }
    if (listen(server_socket, 3) == -1) {
        perror("listen failed");
        close(server_socket);
        return 1;
    }
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(server_socket, &read_fds);
    int max_fd = server_socket;
    while (true) {
        fd_set temp_fds = read_fds;
        int activity = select(max_fd + 1, &temp_fds, NULL, NULL, NULL);
        if (activity == -1) {
            perror("select error");
            break;
        } else if (activity > 0) {
            if (FD_ISSET(server_socket, &temp_fds)) {
                int client_socket = accept(server_socket, NULL, NULL);
                if (client_socket != -1) {
                    FD_SET(client_socket, &read_fds);
                    if (client_socket > max_fd) {
                        max_fd = client_socket;
                    }
                }
            }
            for (int i = 0; i <= max_fd; ++i) {
                if (FD_ISSET(i, &temp_fds) && i != server_socket) {
                    char buffer[1024] = {0};
                    int valread = read(i, buffer, sizeof(buffer));
                    if (valread == -1) {
                        perror("read failed");
                        close(i);
                        FD_CLR(i, &read_fds);
                    } else if (valread == 0) {
                        close(i);
                        FD_CLR(i, &read_fds);
                    } else {
                        std::cout << "Received: " << buffer << std::endl;
                    }
                }
            }
        }
    }
    close(server_socket);
    return 0;
}這段代碼創(chuàng)建了一個簡單的 TCP 服務器,使用select監(jiān)聽新的客戶端連接和客戶端發(fā)送的數(shù)據(jù)。
select 的優(yōu)點是幾乎在所有平臺上都支持,具有良好的跨平臺性;缺點是單個進程能夠監(jiān)視的文件描述符數(shù)量有限,在 Linux 上一般為 1024,并且每次調(diào)用select都需要將文件描述符集合從用戶態(tài)拷貝到內(nèi)核態(tài),隨著文件描述符數(shù)量的增大,其復制和遍歷的開銷也會線性增長。
②poll:poll 出現(xiàn)的時間比 select 稍晚,它和 select 在本質(zhì)上沒有太大差別,也是通過輪詢的方式來檢查文件描述符是否就緒。poll函數(shù)的原型如下:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);參數(shù)說明—fds:一個指向struct pollfd結構體數(shù)組的指針,struct pollfd結構體定義如下:
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 等待的事件
    short revents;  // 實際發(fā)生的事件
};- nfds:指定fds數(shù)組中結構體的個數(shù)。
 - timeout:設置超時時間,單位是毫秒。
 
返回值說明:
- 成功時返回就緒文件描述符個數(shù)。
 - 超時時返回 0。
 - 出錯時返回負值。
 
與 select 相比,poll 沒有最大文件描述符數(shù)量的限制,并且它將輸入輸出參數(shù)進行了分離,不需要每次都重新設定。但是,poll 同樣存在包含大量文件描述符的數(shù)組被整體復制于用戶態(tài)和內(nèi)核的地址空間之間的問題,其開銷隨著文件描述符數(shù)量的增加而線性增大。
③epoll:epoll 是在 Linux 2.6 內(nèi)核中引入的,它被公認為是 Linux 下性能最好的多路 I/O 就緒通知方法。
epoll 有三個主要函數(shù):
⑴epoll_create:用于創(chuàng)建一個 epoll 實例,返回一個 epoll 專用的文件描述符。
#include <sys/epoll.h>
int epoll_create(int size);這里的size參數(shù)在 Linux 2.6.8 版本之后被忽略,但仍需傳入一個大于 0 的值。
⑵epoll_ctl:用于控制某個 epoll 實例監(jiān)聽的文件描述符,比如添加、刪除或修改監(jiān)聽事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);參數(shù)說明:
- epfd:epoll 實例的文件描述符。
 - op:操作類型,有EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(刪除)。
 - fd:要操作的文件描述符。
 
event:指向struct epoll_event結構體的指針,用于設置監(jiān)聽的事件和關聯(lián)的數(shù)據(jù),struct epoll_event結構體定義如下:
struct epoll_event {
    uint32_t events;      // Epoll事件
    epoll_data_t data;    // 用戶數(shù)據(jù)
};其中,events可以是EPOLLIN(可讀事件)、EPOLLOUT(可寫事件)等事件的組合;data可以是一個void*指針,用于關聯(lián)用戶自定義的數(shù)據(jù)。
⑶epoll_wait:用于等待 epoll 實例上的事件發(fā)生,返回就緒的事件列表。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);參數(shù)說明
- epfd:epoll 實例的文件描述符。
 - events:用于存儲就緒事件的數(shù)組。
 - maxevents:指定events數(shù)組的大小。
 - timeout:設置超時時間,單位是毫秒,若為 - 1 則表示一直阻塞。
 
返回值說明
- 成功時返回就緒事件的個數(shù)。
 - 超時時返回 0。
 - 出錯時返回負值。
 
epoll 使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的拷貝只需一次。而且,epoll 采用基于事件的就緒通知方式,當某個文件描述符就緒時,內(nèi)核會采用類似 callback 的回調(diào)機制,迅速激活這個文件描述符,當進程調(diào)用epoll_wait時便得到通知,大大提高了效率。
綜上所述,select、poll 和 epoll 各有優(yōu)劣,在實際應用中,我們需要根據(jù)具體的需求和場景來選擇合適的 IO 多路復用方式。如果需要跨平臺支持,且文件描述符數(shù)量較少,select 是一個不錯的選擇;如果需要處理大量的文件描述符,且對性能要求較高,epoll 則是更好的選擇;而 poll 則處于兩者之間,在一些特定場景下也有其用武之地。
Part4.三者之間的區(qū)別
在 C++ 并發(fā)編程的世界里,多進程、多線程和 IO 多路復用各有千秋,它們就像是三把不同的鑰匙,適用于不同的 “鎖”。
多進程適用于需要高度獨立性和穩(wěn)定性的場景。在服務器開發(fā)中,若服務器的各個模塊需要獨立運行,互不干擾,即使某個模塊崩潰也不能影響其他模塊和整個服務器的運行,此時多進程就是一個很好的選擇。像數(shù)據(jù)庫服務器,它的不同功能模塊(如查詢處理、事務管理、存儲管理等)可以分別由不同的進程來負責,這樣可以保證每個模塊的獨立性和穩(wěn)定性。但由于多進程的開銷較大,創(chuàng)建和銷毀進程需要消耗較多的資源和時間,因此在對資源和性能要求較高的場景下,使用多進程時需要謹慎考慮。
多線程則更適合那些對資源共享和通信要求較高,且任務之間協(xié)作緊密的場景。在圖形界面應用程序中,為了保證界面的流暢響應,同時進行一些耗時的操作(如數(shù)據(jù)加載、網(wǎng)絡請求等),就可以利用多線程。將耗時操作放在后臺線程執(zhí)行,主線程則專注于處理用戶界面的更新和響應,這樣可以大大提高用戶體驗。然而,多線程編程需要注意線程同步和資源競爭的問題,以避免出現(xiàn)數(shù)據(jù)不一致和死鎖等錯誤。
IO 多路復用主要用于處理大量并發(fā)的 I/O 操作。在高并發(fā)的網(wǎng)絡服務器中,服務器需要同時處理大量客戶端的連接和請求,如果為每個客戶端連接都創(chuàng)建一個線程或進程,會消耗大量的系統(tǒng)資源,導致性能下降。而使用 IO 多路復用技術,服務器可以通過一個線程同時監(jiān)聽多個客戶端連接的 I/O 事件,當某個連接有數(shù)據(jù)可讀或可寫時,再進行相應的處理,這樣可以大大提高服務器的并發(fā)處理能力和資源利用率。例如,像 Nginx 這樣的高性能 Web 服務器,就廣泛使用了 epoll 這種 IO 多路復用技術來實現(xiàn)高效的并發(fā)處理。
在實際的 C++ 并發(fā)編程中,我們需要根據(jù)具體的需求和場景,綜合考慮多進程、多線程和 IO 多路復用的特點,選擇最合適的并發(fā)編程方式,以實現(xiàn)高效、穩(wěn)定的程序。















 
 
 








 
 
 
 