偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Linux線程棧內(nèi)存管理:從底層原理到性能優(yōu)化實(shí)戰(zhàn)

系統(tǒng) Linux
從底層講透 Linux 線程棧的分配規(guī)則,教你避開(kāi)棧溢出、內(nèi)存泄漏的陷阱,還會(huì)給你pstack查棧布局、ulimit調(diào)棧限制的實(shí)戰(zhàn)工具,以及線程池控?cái)?shù)量、遞歸轉(zhuǎn)迭代的優(yōu)化方案。不管你是后端開(kāi)發(fā)還是運(yùn)維,看完就能把線程棧管理的主動(dòng)權(quán)攥在手里,再也不用為 “線程棧出問(wèn)題” 熬夜排查。

你是不是也遇到過(guò)這種糟心情況?線上服務(wù)突然崩了,日志滿(mǎn)屏Segmentation fault,gdb調(diào)試半天,發(fā)現(xiàn)是線程創(chuàng)建后沒(méi)多久就 “爆?!?;明明線程退出了,內(nèi)存卻沒(méi)降,排查后才知道是棧資源沒(méi)回收;甚至試過(guò)開(kāi)幾百個(gè)線程,虛擬地址空間就不夠用了 —— 這些坑,其實(shí)都藏在 Linux 線程棧的管理邏輯里。很多開(kāi)發(fā)者對(duì)線程棧的認(rèn)知,停留在 “每個(gè)線程有塊棧內(nèi)存”,卻不知道 Linux 下線程棧分用戶(hù)棧(可通過(guò)pthread_attr設(shè)置,默認(rèn) 8MB)和內(nèi)核棧(32 位 8KB、64 位 16KB 固定);不清楚底層是mmap在虛擬地址空間劃的獨(dú)立區(qū)域,更沒(méi)算過(guò) “遞歸深度 × 棧幀大小” 要怎么匹配棧空間,才不會(huì)溢出。

從底層講透 Linux 線程棧的分配規(guī)則,教你避開(kāi)棧溢出、內(nèi)存泄漏的陷阱,還會(huì)給你pstack查棧布局、ulimit調(diào)棧限制的實(shí)戰(zhàn)工具,以及線程池控?cái)?shù)量、遞歸轉(zhuǎn)迭代的優(yōu)化方案。不管你是后端開(kāi)發(fā)還是運(yùn)維,看完就能把線程棧管理的主動(dòng)權(quán)攥在手里,再也不用為 “線程棧出問(wèn)題” 熬夜排查。

一、Linux 線程棧的底層原理

1.1 用戶(hù)棧 vs 內(nèi)核棧

在 Linux 系統(tǒng)中,線程棧有兩個(gè)重要的組成部分:用戶(hù)棧和內(nèi)核棧 ,它們就像是線程的 “雙重身份”,各自承擔(dān)著獨(dú)特而關(guān)鍵的職責(zé)。

用戶(hù)棧,是每個(gè)線程私有的專(zhuān)屬空間,它如同線程的 “私人儲(chǔ)物間”,存放著線程運(yùn)行時(shí)的局部變量、函數(shù)調(diào)用鏈等關(guān)鍵信息。當(dāng)我們使用pthread_create創(chuàng)建線程時(shí),可以通過(guò)pthread_attr_t結(jié)構(gòu)體來(lái)指定用戶(hù)棧的大小。在大多數(shù)情況下,Linux 線程的用戶(hù)棧默認(rèn)大小在 1MB 到 8MB 之間,這個(gè)數(shù)值可不是隨意設(shè)定的,它是綜合考慮了系統(tǒng)性能和資源利用的結(jié)果。你可以通過(guò)ulimit -s命令查看當(dāng)前系統(tǒng)中棧大小的限制。

圖片

用戶(hù)棧還有一個(gè)很特別的 “生長(zhǎng)習(xí)慣”—— 它是向下生長(zhǎng)的。這意味著棧頂指針(在 x86 架構(gòu)中通常是%rsp寄存器)會(huì)朝著低地址方向移動(dòng)。當(dāng)線程調(diào)用一個(gè)新函數(shù)時(shí),新的棧幀就會(huì)被壓入棧中,棧頂指針也隨之向下移動(dòng);函數(shù)返回時(shí),棧幀彈出,棧頂指針又向上移動(dòng)。這種 “后進(jìn)先出” 的特性,就像我們往一個(gè)桶里放東西,最后放進(jìn)去的總是最先拿出來(lái)。從內(nèi)存分配的角度來(lái)看,用戶(hù)棧是通過(guò)mmap系統(tǒng)調(diào)用在進(jìn)程的虛擬地址空間中分配的。我們可以通過(guò)cat /proc/[pid]/maps命令查看進(jìn)程的虛擬內(nèi)存映射情況,其中標(biāo)記為[stack]的區(qū)域就是用戶(hù)棧所在的位置。

內(nèi)核棧,則是線程在內(nèi)核態(tài)運(yùn)行時(shí)使用的棧。與用戶(hù)棧不同,內(nèi)核棧的大小是固定的,在 32 位系統(tǒng)中通常為 8KB,64 位系統(tǒng)中為 16KB 。這個(gè)固定的大小是經(jīng)過(guò)精心設(shè)計(jì)的,因?yàn)閮?nèi)核態(tài)的操作相對(duì)穩(wěn)定,不需要像用戶(hù)態(tài)那樣頻繁地調(diào)整棧的大小。內(nèi)核棧主要用于存儲(chǔ)內(nèi)核函數(shù)的調(diào)用信息,包括函數(shù)參數(shù)、返回地址以及內(nèi)核態(tài)下的局部變量等。當(dāng)線程通過(guò)系統(tǒng)調(diào)用、中斷或異常進(jìn)入內(nèi)核態(tài)時(shí),就會(huì)切換到內(nèi)核棧上執(zhí)行,內(nèi)核棧就像是內(nèi)核態(tài)的 “工作區(qū)”,為內(nèi)核函數(shù)的執(zhí)行提供了必要的支持。而且,內(nèi)核棧的管理是由內(nèi)核自動(dòng)完成的,對(duì)用戶(hù)態(tài)的程序來(lái)說(shuō)是透明的,就像一個(gè)隱藏在幕后的 “管家”,默默地處理著內(nèi)核態(tài)下的各種事務(wù)。

圖片

這里有一個(gè)小技巧,直接將 esp 的地址與上 ~(THREAD_SIZE - 1) 后即可直接獲得 thread_info 的地址。由于 thread_union 結(jié)構(gòu)體是從 thread_info_cache 的 Slab 緩存池中申請(qǐng)出來(lái)的,而 thread_info_cache 在 kmem_cache_create 創(chuàng)建的時(shí)候,保證了地址是 THREAD_SIZE 對(duì)齊的。因此只需要對(duì)棧指針進(jìn)行 THREAD_SIZE 對(duì)齊,即可獲得 thread_union 的地址,也就獲得了 thread_union 的地址。成功獲取到 thread_info 后,直接取出它的 task 成員就成功得到了 task_struct。其實(shí)上面這段描述,也就是 current 宏的實(shí)現(xiàn)方法:

register unsigned long current_stack_pointer asm ("sp");
static inline struct thread_info *current_thread_info(void)  
{                                                           
        return (struct thread_info *)                        
                (current_stack_pointer & ~(THREAD_SIZE - 1));
}                                                            
#define get_current() (current_thread_info()->task)
#define current get_current()

1.2 Linux 如何實(shí)現(xiàn)線程棧獨(dú)立?

在早期的 Linux 系統(tǒng)中,線程的實(shí)現(xiàn)是通過(guò)LinuxThreads庫(kù)來(lái)模擬的,這種方式就像是在沒(méi)有真正的 “線程” 基礎(chǔ)設(shè)施的情況下,用一些巧妙的技巧搭建起了一個(gè)類(lèi)似線程的環(huán)境。但這種模擬線程的方式存在很多問(wèn)題,就像用臨時(shí)搭建的腳手架來(lái)支撐一座高樓,雖然勉強(qiáng)能用,但總是不夠穩(wěn)固和高效。例如,在信號(hào)處理、調(diào)度和進(jìn)程間同步原語(yǔ)等方面,LinuxThreads都表現(xiàn)得不盡如人意,而且它也沒(méi)有完全符合 POSIX 標(biāo)準(zhǔn),這就像是一個(gè)沒(méi)有遵守規(guī)則的選手,在比賽中總是會(huì)遇到各種麻煩。

為了解決這些問(wèn)題,NPTL(Native POSIX Thread Library)應(yīng)運(yùn)而生,它就像是一位擁有魔法的工匠,為 Linux 系統(tǒng)帶來(lái)了真正的 POSIX 線程支持。NPTL 通過(guò)clone系統(tǒng)調(diào)用創(chuàng)建輕量級(jí)進(jìn)程(LWP),這些輕量級(jí)進(jìn)程共享進(jìn)程的地址空間,但每個(gè)都擁有獨(dú)立的??臻g,就像是在一個(gè)大房子里,每個(gè)房間都有自己獨(dú)立的小倉(cāng)庫(kù),既節(jié)省了空間,又保證了獨(dú)立性。

當(dāng)我們使用pthread_create創(chuàng)建線程時(shí),NPTL 在底層做了很多精細(xì)的工作。它首先會(huì)通過(guò)mmap系統(tǒng)調(diào)用為線程分配用戶(hù)棧空間,這個(gè)過(guò)程就像是為線程找到了一塊專(zhuān)屬的 “土地” 來(lái)建造它的 “儲(chǔ)物間”。然后,在struct pthread結(jié)構(gòu)體中,stackblock指針會(huì)記錄下用戶(hù)棧的基址,就像給這個(gè) “儲(chǔ)物間” 貼上了一個(gè)地址標(biāo)簽,方便后續(xù)的訪問(wèn)和管理;stackblock_size則標(biāo)記了棧的大小,告訴我們這個(gè) “儲(chǔ)物間” 到底有多大。通過(guò)這些步驟,NPTL 成功地實(shí)現(xiàn)了線程棧的獨(dú)立,為多線程編程提供了堅(jiān)實(shí)的基礎(chǔ)。

下面我將編寫(xiě)一個(gè) C 語(yǔ)言程序,演示使用 NPTL 庫(kù)創(chuàng)建線程并展示線程棧的獨(dú)立性。這個(gè)程序會(huì)創(chuàng)建多個(gè)線程,每個(gè)線程會(huì)打印自己的 ID 以及棧上變量的地址,以此展示每個(gè)線程擁有獨(dú)立的??臻g:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

// 線程函數(shù),打印線程ID和棧上變量地址
void *thread_function(void *arg) {
    int thread_num = *(int *)arg;
    free(arg); // 釋放動(dòng)態(tài)分配的內(nèi)存

    // 在棧上創(chuàng)建一個(gè)變量
    int stack_variable;

    // 打印線程ID和棧變量地址
    printf("線程 %d: pthread ID = %lu, 棧變量地址 = %p\n",
           thread_num,
           (unsigned long)pthread_self(),
           &stack_variable);

    // 簡(jiǎn)單的循環(huán),讓線程持續(xù)一段時(shí)間
    for (int i = 0; i < 5; i++) {
        usleep(100000); // 休眠0.1秒
    }

    return NULL;
}

int main() {
    const int NUM_THREADS = 5;
    pthread_t threads[NUM_THREADS];

    printf("主線程: pthread ID = %lu\n", (unsigned long)pthread_self());

    // 創(chuàng)建多個(gè)線程
    for (int i = 0; i < NUM_THREADS; i++) {
        // 動(dòng)態(tài)分配內(nèi)存?zhèn)鬟f參數(shù),避免競(jìng)爭(zhēng)條件
        int *thread_num = malloc(sizeof(int));
        *thread_num = i + 1;

        int result = pthread_create(&threads[i], NULL, thread_function, thread_num);
        if (result != 0) {
            fprintf(stderr, "創(chuàng)建線程 %d 失敗\n", i + 1);
            return 1;
        }
    }

    // 等待所有線程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("所有線程已完成\n");
    return 0;
}
  1. 程序創(chuàng)建了 5 個(gè)線程,每個(gè)線程都有自己獨(dú)立的??臻g
  2. 每個(gè)線程打印自己的 pthread ID 和棧上變量的地址
  3. 通過(guò)觀察輸出的棧變量地址,你會(huì)發(fā)現(xiàn)它們位于不同的內(nèi)存區(qū)域,這證明了每個(gè)線程擁有獨(dú)立的??臻g
  4. 使用 pthread_self () 獲取線程 ID,展示線程的唯一性

編譯和運(yùn)行方法:

gcc thread_demo.c -o thread_demo -lpthread
./thread_demo

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出(地址會(huì)有所不同):

主線程: pthread ID = 140572037066560
線程 1: pthread ID = 140572020367104, 棧變量地址 = 0x7fda3a6f9e5c
線程 2: pthread ID = 140572011974400, 棧變量地址 = 0x7fda39ef8e5c
線程 3: pthread ID = 140572003581696, 棧變量地址 = 0x7fda396f7e5c
線程 4: pthread ID = 140571995188992, 棧變量地址 = 0x7fda38ef6e5c
線程 5: pthread ID = 140571986796288, 棧變量地址 = 0x7fda386f5e5c
所有線程已完成

注意觀察棧變量地址,它們明顯位于不同的內(nèi)存區(qū)域,這直觀地展示了NPTL為每個(gè)線程分配獨(dú)立??臻g的特性。

1.3 為什么越界會(huì)觸發(fā)段錯(cuò)誤?

在 Linux 系統(tǒng)中,虛擬內(nèi)存保護(hù)機(jī)制就像是一個(gè)嚴(yán)格的 “邊界守護(hù)者”,時(shí)刻保護(hù)著棧空間的安全。當(dāng)線程棧在使用過(guò)程中,如果不小心越過(guò)了分配的空間邊界,就會(huì)觸發(fā)一個(gè)嚴(yán)重的錯(cuò)誤 —— 段錯(cuò)誤(Segmentation fault) 。這就好比你在自己的土地上建房,卻不小心把房子建到了鄰居的土地上,肯定會(huì)引發(fā)一系列問(wèn)題。

為了防止這種情況的發(fā)生,Linux 在棧的底部設(shè)置了一個(gè)特殊的 “guard page”,它就像是棧空間邊界的一道 “警戒線”,是一塊不可訪問(wèn)的內(nèi)存區(qū)域。當(dāng)棧向下生長(zhǎng)時(shí),如果超過(guò)了分配的棧大小,就會(huì)觸碰到這個(gè) “guard page”,此時(shí)系統(tǒng)會(huì)認(rèn)為這是一次非法的內(nèi)存訪問(wèn),于是觸發(fā)SIGSEGV信號(hào),也就是我們常說(shuō)的棧溢出。這個(gè)過(guò)程就像是你在靠近鄰居土地的邊界時(shí),觸發(fā)了警報(bào)系統(tǒng),提醒你已經(jīng)越界了。

例如,當(dāng)我們編寫(xiě)一個(gè)遞歸函數(shù)時(shí),如果沒(méi)有設(shè)置正確的終止條件,每一次遞歸調(diào)用都會(huì)在棧上壓入一個(gè)新的棧幀,就像不斷地往一個(gè)有限大小的桶里放東西。隨著遞歸深度的增加,棧幀越來(lái)越多,最終會(huì)突破stacksize的限制,導(dǎo)致棧溢出。此時(shí),程序就會(huì)收到SIGSEGV信號(hào),然后異常終止,就像一個(gè)失控的機(jī)器突然停止運(yùn)轉(zhuǎn)。所以,在編寫(xiě)多線程程序時(shí),我們一定要時(shí)刻注意棧空間的使用,避免出現(xiàn)棧溢出的情況,確保程序的穩(wěn)定運(yùn)行。

下面我將編寫(xiě)一個(gè) C 程序,演示 Linux 系統(tǒng)中的虛擬內(nèi)存保護(hù)機(jī)制,特別是棧溢出和 guard page 的作用。這個(gè)程序會(huì)創(chuàng)建一個(gè)線程并故意讓其發(fā)生棧溢出,以此展示系統(tǒng)如何通過(guò)段錯(cuò)誤來(lái)保護(hù)內(nèi)存安全。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

// 全局變量,用于計(jì)數(shù)遞歸深度
volatile int recursion_depth = 0;

// 信號(hào)處理函數(shù),捕獲段錯(cuò)誤信號(hào)
void handle_sigsegv(int sig) {
    printf("\n捕獲到段錯(cuò)誤(SIGSEGV)!遞歸深度: %d\n", recursion_depth);
    printf("這是由于棧溢出觸發(fā)了guard page保護(hù)機(jī)制\n");
    exit(EXIT_FAILURE);
}

// 遞歸函數(shù),不斷消耗??臻g
void recursive_function() {
    // 在棧上分配一塊內(nèi)存,增加棧使用量
    char stack_buffer[1024];
    memset(stack_buffer, 0, sizeof(stack_buffer));

    // 增加遞歸深度計(jì)數(shù)
    recursion_depth++;

    // 每100次遞歸打印一次狀態(tài)
    if (recursion_depth % 100 == 0) {
        printf("遞歸深度: %d, 棧變量地址: %p\n", recursion_depth, stack_buffer);
    }

    // 繼續(xù)遞歸,直到棧溢出
    recursive_function();
}

// 線程函數(shù)
void *thread_function(void *arg) {
    printf("線程開(kāi)始運(yùn)行,嘗試遞歸直到棧溢出...\n");

    // 調(diào)用遞歸函數(shù),開(kāi)始消耗??臻g
    recursive_function();

    return NULL;
}

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    size_t stack_size;

    // 注冊(cè)信號(hào)處理函數(shù),捕獲段錯(cuò)誤
    signal(SIGSEGV, handle_sigsegv);

    // 初始化線程屬性
    pthread_attr_init(&attr);

    // 獲取默認(rèn)棧大小
    pthread_attr_getstacksize(&attr, &stack_size);
    printf("線程默認(rèn)棧大小: %zu 字節(jié)\n", stack_size);

    // 可以在這里設(shè)置更小的棧大小,讓溢出更快發(fā)生
    // stack_size = 1024 * 64; // 64KB
    // pthread_attr_setstacksize(&attr, stack_size);
    // printf("已設(shè)置線程棧大小: %zu 字節(jié)\n", stack_size);

    // 創(chuàng)建線程
    if (pthread_create(&thread, &attr, thread_function, NULL) != 0) {
        fprintf(stderr, "創(chuàng)建線程失敗\n");
        return 1;
    }

    // 等待線程結(jié)束
    pthread_join(thread, NULL);

    // 銷(xiāo)毀線程屬性
    pthread_attr_destroy(&attr);

    return 0;
}
  1. 程序創(chuàng)建了一個(gè)線程,并獲取 / 設(shè)置其棧大小
  2. 在線程中運(yùn)行一個(gè)遞歸函數(shù),每次遞歸都會(huì)在棧上分配 1024 字節(jié)的緩沖區(qū)
  3. 隨著遞歸深度增加,??臻g不斷被消耗,棧指針不斷向下生長(zhǎng)
  4. 當(dāng)??臻g耗盡并觸及 guard page 時(shí),會(huì)觸發(fā)段錯(cuò)誤 (SIGSEGV)
  5. 注冊(cè)的信號(hào)處理函數(shù)會(huì)捕獲這個(gè)錯(cuò)誤并打印相關(guān)信息

編譯和運(yùn)行方法

gcc stack_overflow_demo.c -o stack_overflow_demo -lpthread
./stack_overflow_demo

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出:

線程默認(rèn)棧大小: 8388608 字節(jié)
線程開(kāi)始運(yùn)行,嘗試遞歸直到棧溢出...
遞歸深度: 100, 棧變量地址: 0x7f8a5f5f7bb0
遞歸深度: 200, 棧變量地址: 0x7f8a5f5f7770
...
遞歸深度: 7800, 棧變量地址: 0x7f8a5ef7fbb0

捕獲到段錯(cuò)誤(SIGSEGV)!遞歸深度: 7842
這是由于棧溢出觸發(fā)了guard page保護(hù)機(jī)制

從輸出可以觀察到:

  • 棧變量的地址在不斷減小(棧向下生長(zhǎng))
  • 當(dāng)達(dá)到一定遞歸深度后,觸發(fā)了段錯(cuò)誤
  • 這證明了 Linux 系統(tǒng)通過(guò) guard page 機(jī)制保護(hù)??臻g的安全性

如果取消注釋設(shè)置棧大小的代碼,可以讓棧溢出更快發(fā)生,方便觀察這一現(xiàn)象。

二、三大致命陷阱:踩過(guò)的坑都在這里

在實(shí)際開(kāi)發(fā)中,線程棧內(nèi)存管理稍有不慎就會(huì)引發(fā)各種 “詭異” 的問(wèn)題,就像隱藏在暗處的陷阱,讓你的程序隨時(shí)陷入崩潰的邊緣。下面我們就來(lái)看看這些常見(jiàn)的陷阱以及如何避開(kāi)它們。

2.1 棧溢出(Stack Overflow):遞歸太深的惡果

棧溢出是多線程編程中最常見(jiàn)的問(wèn)題之一,它就像是一個(gè)無(wú)底洞,不斷吞噬著??臻g。當(dāng)線程的遞歸調(diào)用層數(shù)過(guò)深,或者局部變量占用的??臻g過(guò)大時(shí),就可能導(dǎo)致棧溢出 。比如,我們有一個(gè)簡(jiǎn)單的遞歸函數(shù):

#include <stdio.h>

void recursive_function(int depth) {
    int local_variable[1024]; // 占用1024個(gè)整型的??臻g
    printf("Depth: %d\n", depth);
    recursive_function(depth + 1);
}

int main() {
    recursive_function(1);
    return 0;
}

在這個(gè)例子中,recursive_function函數(shù)每調(diào)用一次,就會(huì)在棧上分配 1024 個(gè)整型的空間用于local_variable數(shù)組,并且沒(méi)有設(shè)置遞歸終止條件。隨著遞歸深度的增加,棧空間會(huì)被迅速耗盡,最終導(dǎo)致棧溢出。當(dāng)棧溢出發(fā)生時(shí),程序會(huì)收到SIGSEGV信號(hào),然后異常終止,就像一個(gè)失控的汽車(chē)直接撞上了墻。

在實(shí)際場(chǎng)景中,棧溢出可能會(huì)在一些復(fù)雜的算法實(shí)現(xiàn)中悄悄出現(xiàn),比如深度優(yōu)先搜索(DFS)算法,如果沒(méi)有正確處理遞歸終止條件,就很容易觸發(fā)棧溢出。假設(shè)我們的線程棧大小設(shè)置為 2MB,而每次遞歸調(diào)用需要占用 1KB 的??臻g,那么理論上遞歸調(diào)用 2000 次左右就會(huì)觸發(fā)棧溢出 。所以,在編寫(xiě)遞歸函數(shù)時(shí),一定要設(shè)置正確的終止條件,并且盡量減少局部變量的占用空間,避免陷入棧溢出的陷阱。

2.2 內(nèi)存泄漏:忘記回收的 “僵尸線程”

在 Linux 線程中,線程的狀態(tài)分為joinable和unjoinable ,這兩種狀態(tài)就像是線程的兩種 “命運(yùn)”,決定了線程退出后棧資源的去向。

默認(rèn)情況下,線程是joinable狀態(tài),這意味著當(dāng)線程退出時(shí),它的棧資源不會(huì)被立即釋放,就像一個(gè)離開(kāi)房間卻不收拾東西的人,留下了一堆垃圾。此時(shí),需要調(diào)用pthread_join函數(shù)來(lái)回收這些資源,否則這些未釋放的棧資源就會(huì)逐漸累積,最終導(dǎo)致內(nèi)存泄漏。例如:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* thread_function(void* arg) {
    // 線程執(zhí)行的任務(wù)
    return NULL;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
    // 沒(méi)有調(diào)用pthread_join回收線程資源
    return 0;
}

在這個(gè)例子中,我們創(chuàng)建了一個(gè)線程,但沒(méi)有調(diào)用pthread_join來(lái)等待線程結(jié)束并回收其資源。隨著程序中這種情況的不斷發(fā)生,內(nèi)存中的 “垃圾” 會(huì)越來(lái)越多,最終導(dǎo)致系統(tǒng)內(nèi)存不足,無(wú)法創(chuàng)建新的線程,程序也就陷入了困境。

而unjoinable狀態(tài)的線程則不同,就像一個(gè)離開(kāi)房間后會(huì)自動(dòng)收拾好東西的人。通過(guò)pthread_detach函數(shù)可以將線程設(shè)置為unjoinable狀態(tài),這樣當(dāng)線程退出時(shí),它的棧資源會(huì)被自動(dòng)釋放 。這種狀態(tài)適合那些不需要返回值,并且在后臺(tái)默默運(yùn)行的任務(wù),比如一些守護(hù)線程。例如:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* thread_function(void* arg) {
    // 線程執(zhí)行的任務(wù)
    pthread_detach(pthread_self()); // 將自身設(shè)置為unjoinable狀態(tài)
    return NULL;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
    // 不需要調(diào)用pthread_join
    return 0;
}

在這個(gè)例子中,我們?cè)诰€程函數(shù)內(nèi)部調(diào)用了pthread_detach(pthread_self()),將線程設(shè)置為unjoinable狀態(tài),這樣線程退出時(shí)就會(huì)自動(dòng)釋放資源,避免了內(nèi)存泄漏的問(wèn)題。所以,在編寫(xiě)多線程程序時(shí),一定要根據(jù)線程的實(shí)際需求,合理設(shè)置線程的狀態(tài),及時(shí)回收線程資源,讓程序的內(nèi)存管理更加高效和穩(wěn)定。

2.3 地址空間耗盡:萬(wàn)線程時(shí)代的挑戰(zhàn)

在 64 位系統(tǒng)中,雖然進(jìn)程理論上擁有 128TB 的用戶(hù)空間,聽(tīng)起來(lái)非常龐大,但在實(shí)際應(yīng)用中,每個(gè)線程默認(rèn)會(huì)占用 8MB 的棧空間 ,這就像是在一個(gè)有限的大房間里,每個(gè)物品都要占用很大的空間。當(dāng)我們嘗試創(chuàng)建大量線程時(shí),很快就會(huì)發(fā)現(xiàn)這個(gè)空間其實(shí)非常有限。

例如,當(dāng)我們想要?jiǎng)?chuàng)建 10 萬(wàn)個(gè)線程時(shí),僅僅線程棧就需要占用 800GB 的空間(10 萬(wàn) × 8MB) ,這遠(yuǎn)遠(yuǎn)超過(guò)了大多數(shù)系統(tǒng)實(shí)際可用的內(nèi)存空間。隨著線程數(shù)量的不斷增加,虛擬地址空間會(huì)被迅速耗盡,就像一個(gè)被填滿(mǎn)的倉(cāng)庫(kù),再也放不下任何東西。

當(dāng)?shù)刂房臻g耗盡時(shí),pthread_create函數(shù)會(huì)返回EAGAIN錯(cuò)誤,同時(shí)系統(tǒng)日志中會(huì)出現(xiàn) “Resource temporarily unavailable” 的提示 ,這就像是系統(tǒng)在無(wú)奈地告訴你:“我已經(jīng)沒(méi)有空間了,無(wú)法再創(chuàng)建新的線程了?!?此時(shí),程序的并發(fā)性能會(huì)受到嚴(yán)重影響,甚至無(wú)法正常運(yùn)行。為了避免這種情況的發(fā)生,我們需要在設(shè)計(jì)多線程程序時(shí),充分考慮系統(tǒng)的資源限制,合理控制線程的數(shù)量,避免過(guò)度創(chuàng)建線程導(dǎo)致地址空間耗盡。

可以通過(guò)優(yōu)化算法,減少對(duì)線程的依賴(lài),或者采用線程池等技術(shù)來(lái)復(fù)用線程,提高資源利用率,讓程序在有限的資源下更加穩(wěn)定地運(yùn)行。下面我將編寫(xiě)一個(gè) C 程序,演示在 64 位系統(tǒng)中創(chuàng)建大量線程時(shí)可能遇到的地址空間耗盡問(wèn)題。這個(gè)程序會(huì)嘗試創(chuàng)建盡可能多的線程,直到系統(tǒng)資源耗盡,以此展示線程棧對(duì)虛擬內(nèi)存的消耗。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// 線程計(jì)數(shù)
volatile int thread_count = 0;

// 簡(jiǎn)單的線程函數(shù),什么也不做,立即返回
void *thread_function(void *arg) {
    // 線程不需要執(zhí)行任何操作,立即退出
    return NULL;
}

int main() {
    pthread_t *threads;
    int max_threads = 1000000; // 嘗試創(chuàng)建最多100萬(wàn)個(gè)線程
    int i, result;

    // 分配存儲(chǔ)線程ID的數(shù)組
    threads = (pthread_t *)malloc(max_threads * sizeof(pthread_t));
    if (threads == NULL) {
        fprintf(stderr, "內(nèi)存分配失敗\n");
        return 1;
    }

    printf("開(kāi)始創(chuàng)建線程...\n");
    printf("每個(gè)線程默認(rèn)棧大小約為8MB\n");

    // 嘗試創(chuàng)建盡可能多的線程
    for (i = 0; i < max_threads; i++) {
        result = pthread_create(&threads[i], NULL, thread_function, NULL);

        if (result != 0) {
            // 檢查是否是資源暫時(shí)不可用的錯(cuò)誤
            if (result == EAGAIN) {
                fprintf(stderr, "\n創(chuàng)建第 %d 個(gè)線程失敗: %s\n", i + 1, strerror(result));
                fprintf(stderr, "虛擬地址空間耗盡 - 無(wú)法創(chuàng)建更多線程\n");
            } else {
                fprintf(stderr, "\n創(chuàng)建第 %d 個(gè)線程失敗: %s\n", i + 1, strerror(result));
            }
            break;
        }

        thread_count++;

        // 每創(chuàng)建1000個(gè)線程打印一次進(jìn)度
        if (thread_count % 1000 == 0) {
            printf("已創(chuàng)建 %d 個(gè)線程...\r", thread_count);
            fflush(stdout);
        }
    }

    printf("\n成功創(chuàng)建的線程總數(shù): %d\n", thread_count);
    printf("估計(jì)消耗的??臻g: %.2f GB\n", (thread_count * 8.0) / 1024.0);

    // 等待所有線程完成
    for (i = 0; i < thread_count; i++) {
        pthread_join(threads[i], NULL);
    }

    free(threads);
    return 0;
}
  1. 程序嘗試創(chuàng)建大量線程,最多可達(dá) 100 萬(wàn)個(gè)
  2. 每個(gè)線程創(chuàng)建后立即退出,不執(zhí)行任何實(shí)際工作
  3. 程序會(huì)實(shí)時(shí)顯示創(chuàng)建進(jìn)度,并在創(chuàng)建失敗時(shí)捕獲錯(cuò)誤
  4. 當(dāng)系統(tǒng)無(wú)法創(chuàng)建更多線程時(shí)(通常是由于虛擬地址空間耗盡),pthread_create 會(huì)返回 EAGAIN 錯(cuò)誤
  5. 最后會(huì)顯示成功創(chuàng)建的線程總數(shù)及其估計(jì)消耗的??臻g

編譯和運(yùn)行方法:

gcc thread_limit_demo.c -o thread_limit_demo -lpthread
./thread_limit_demo

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出:

開(kāi)始創(chuàng)建線程...
每個(gè)線程默認(rèn)棧大小約為8MB
已創(chuàng)建 10000 個(gè)線程...
...
創(chuàng)建第 123456 個(gè)線程失敗: Resource temporarily unavailable
虛擬地址空間耗盡 - 無(wú)法創(chuàng)建更多線程
成功創(chuàng)建的線程總數(shù): 123455
估計(jì)消耗的棧空間: 964.50 GB

注意:實(shí)際能創(chuàng)建的線程數(shù)量取決于系統(tǒng)配置和可用資源;運(yùn)行此程序可能會(huì)使系統(tǒng)暫時(shí)變得緩慢,請(qǐng)?jiān)跍y(cè)試環(huán)境中運(yùn)行;可以通過(guò)設(shè)置更小的線程棧大小來(lái)創(chuàng)建更多線程(使用 pthread_attr_setstacksize)。這個(gè)程序很好地說(shuō)明了為什么在設(shè)計(jì)多線程應(yīng)用時(shí)需要考慮線程資源限制,以及為什么線程池等技術(shù)在處理大量并發(fā)任務(wù)時(shí)更為高效。

三、實(shí)戰(zhàn)優(yōu)化:從參數(shù)設(shè)置到代碼設(shè)計(jì)

理解了線程棧的原理和常見(jiàn)問(wèn)題后,接下來(lái)就是如何在實(shí)戰(zhàn)中優(yōu)化線程棧的內(nèi)存使用,讓你的程序既高效又穩(wěn)定。

3.1 精準(zhǔn)控制棧大?。簞e讓線程 “吃太飽”

線程棧大小設(shè)置不當(dāng),就像給運(yùn)動(dòng)員分配的裝備不合適,不是太小影響發(fā)揮,就是太大成為負(fù)擔(dān)。我們需要根據(jù)實(shí)際情況,精準(zhǔn)地為線程分配??臻g。

在創(chuàng)建線程時(shí),通過(guò)pthread_attr_setstacksize函數(shù)可以動(dòng)態(tài)設(shè)置棧大小 。比如,對(duì)于一個(gè)遞歸深度較深的二叉樹(shù)遍歷算法,假設(shè)每個(gè)遞歸調(diào)用的棧幀大小為 1KB,預(yù)計(jì)最大遞歸深度為 1000 層,那么我們可以為線程棧預(yù)留 1000KB 的空間,再加上 20% 的余量以應(yīng)對(duì)可能的突發(fā)情況,即設(shè)置棧大小為 1200KB 。這樣的計(jì)算方式可以確保線程在執(zhí)行過(guò)程中不會(huì)因?yàn)闂?臻g不足而溢出。

不同類(lèi)型的任務(wù)對(duì)??臻g的需求也不同。對(duì)于 IO 密集型的線程,它們大部分時(shí)間都在等待 IO 操作完成,實(shí)際執(zhí)行的代碼較少,棧幀占用空間也相對(duì)較小,所以可以將棧大小設(shè)置為 512KB 。而計(jì)算密集型的線程,它們需要頻繁進(jìn)行復(fù)雜的計(jì)算,函數(shù)調(diào)用和局部變量較多,棧幀占用空間較大,此時(shí)將棧大小設(shè)置為 2MB 會(huì)更加合適。通過(guò)這種場(chǎng)景化的配置,可以避免 “一刀切” 帶來(lái)的資源浪費(fèi)或棧溢出問(wèn)題。下面我將編寫(xiě)一個(gè) C++ 程序,演示如何根據(jù)不同類(lèi)型的任務(wù)動(dòng)態(tài)設(shè)置線程棧大小,以?xún)?yōu)化資源利用并避免棧溢出問(wèn)題:

#include <iostream>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <chrono>
#include <thread>

// 遞歸深度計(jì)數(shù)器
volatile int recursion_depth = 0;
// 最大遞歸深度
const int MAX_RECURSION_DEPTH = 1000;

// 遞歸函數(shù) - 模擬需要較大棧空間的計(jì)算密集型任務(wù)
void recursive_task(int current_depth) {
    // 在棧上分配一些數(shù)據(jù),模擬棧幀大?。s1KB)
    char stack_data[1024];
    memset(stack_data, 0, sizeof(stack_data));

    recursion_depth = current_depth;

    // 每100層遞歸打印一次狀態(tài)
    if (current_depth % 100 == 0) {
        printf("遞歸深度: %d, 棧數(shù)據(jù)地址: %p\n", current_depth, stack_data);
    }

    // 繼續(xù)遞歸,直到達(dá)到最大深度
    if (current_depth < MAX_RECURSION_DEPTH) {
        recursive_task(current_depth + 1);
    }
}

// 計(jì)算密集型任務(wù)的線程函數(shù)
void *compute_intensive_task(void *arg) {
    int task_id = *(int *)arg;
    free(arg);

    printf("計(jì)算密集型任務(wù) %d 開(kāi)始執(zhí)行\(zhòng)n", task_id);

    // 執(zhí)行遞歸任務(wù)
    recursive_task(1);

    printf("計(jì)算密集型任務(wù) %d 完成,最大遞歸深度: %d\n", task_id, recursion_depth);
    return NULL;
}

// IO密集型任務(wù)的線程函數(shù)
void *io_intensive_task(void *arg) {
    int task_id = *(int *)arg;
    free(arg);

    printf("IO密集型任務(wù) %d 開(kāi)始執(zhí)行\(zhòng)n", task_id);

    // 模擬IO操作 - 多次短暫休眠
    for (int i = 0; i < 5; i++) {
        printf("IO密集型任務(wù) %d 正在等待IO操作...\n", task_id);
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }

    printf("IO密集型任務(wù) %d 完成\n", task_id);
    return NULL;
}

int main() {
    pthread_t compute_thread, io_thread;
    pthread_attr_t compute_attr, io_attr;
    size_t default_stack_size;

    // 初始化線程屬性
    pthread_attr_init(&compute_attr);
    pthread_attr_init(&io_attr);

    // 獲取默認(rèn)棧大小
    pthread_attr_getstacksize(&compute_attr, &default_stack_size);
    printf("默認(rèn)線程棧大小: %zu 字節(jié)\n", default_stack_size);

    // 為計(jì)算密集型任務(wù)設(shè)置棧大小
    // 每個(gè)遞歸調(diào)用約1KB,最大深度1000,加上20%余量 = 1200KB
    size_t compute_stack_size = 1200 * 1024; // 1200KB
    pthread_attr_setstacksize(&compute_attr, compute_stack_size);
    printf("為計(jì)算密集型任務(wù)設(shè)置棧大小: %zu 字節(jié)\n", compute_stack_size);

    // 為IO密集型任務(wù)設(shè)置棧大小
    size_t io_stack_size = 512 * 1024; // 512KB
    pthread_attr_setstacksize(&io_attr, io_stack_size);
    printf("為IO密集型任務(wù)設(shè)置棧大小: %zu 字節(jié)\n", io_stack_size);

    // 創(chuàng)建計(jì)算密集型任務(wù)線程
    int *compute_task_id = (int *)malloc(sizeof(int));
    *compute_task_id = 1;
    if (pthread_create(&compute_thread, &compute_attr, compute_intensive_task, compute_task_id) != 0) {
        fprintf(stderr, "創(chuàng)建計(jì)算密集型任務(wù)線程失敗\n");
        return 1;
    }

    // 創(chuàng)建IO密集型任務(wù)線程
    int *io_task_id = (int *)malloc(sizeof(int));
    *io_task_id = 1;
    if (pthread_create(&io_thread, &io_attr, io_intensive_task, io_task_id) != 0) {
        fprintf(stderr, "創(chuàng)建IO密集型任務(wù)線程失敗\n");
        return 1;
    }

    // 等待線程完成
    pthread_join(compute_thread, NULL);
    pthread_join(io_thread, NULL);

    // 銷(xiāo)毀線程屬性
    pthread_attr_destroy(&compute_attr);
    pthread_attr_destroy(&io_attr);

    printf("所有任務(wù)完成\n");
    return 0;
}
  1. 棧大小動(dòng)態(tài)設(shè)置:使用pthread_attr_t結(jié)構(gòu)和pthread_attr_setstacksize函數(shù)設(shè)置線程棧大小,展示了系統(tǒng)默認(rèn)棧大小與自定義棧大小的對(duì)比。
  2. 任務(wù)類(lèi)型差異化配置:為計(jì)算密集型任務(wù)設(shè)置 1200KB ??臻g(包含遞歸調(diào)用所需空間),為 IO 密集型任務(wù)設(shè)置 512KB ??臻g(較小,因?yàn)?IO 操作等待時(shí)不消耗棧)。
  3. 遞歸深度與??臻g計(jì)算:模擬每個(gè)遞歸調(diào)用消耗約 1KB ??臻g,針對(duì) 1000 層遞歸深度,預(yù)留 20% 余量,計(jì)算出所需 1200KB 棧空間。

編譯和運(yùn)行方法:

g++ thread_stack_demo.cpp -o thread_stack_demo -lpthread
./thread_stack_demo

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出:

默認(rèn)線程棧大小: 8388608 字節(jié)
為計(jì)算密集型任務(wù)設(shè)置棧大小: 1228800 字節(jié)
為IO密集型任務(wù)設(shè)置棧大小: 524288 字節(jié)
計(jì)算密集型任務(wù) 1 開(kāi)始執(zhí)行
遞歸深度: 100, 棧數(shù)據(jù)地址: 0x7f5c8b3f8b60
遞歸深度: 200, 棧數(shù)據(jù)地址: 0x7f5c8b3f8720
...
遞歸深度: 1000, 棧數(shù)據(jù)地址: 0x7f5c8b3f4b60
計(jì)算密集型任務(wù) 1 完成,最大遞歸深度: 1000
IO密集型任務(wù) 1 開(kāi)始執(zhí)行
IO密集型任務(wù) 1 正在等待IO操作...
...
IO密集型任務(wù) 1 完成
所有任務(wù)完成

通過(guò)這個(gè)示例可以看到,根據(jù)任務(wù)特性合理設(shè)置棧大小的重要性:

  • 計(jì)算密集型任務(wù)因需要大量??臻g(如深層遞歸)而設(shè)置較大棧
  • IO 密集型任務(wù)因主要時(shí)間在等待而設(shè)置較小棧,節(jié)省系統(tǒng)資源
  • 適當(dāng)?shù)挠嗔吭O(shè)置可以避免棧溢出,保證程序穩(wěn)定性

這種場(chǎng)景化的棧大小配置方法,既避免了 "一刀切" 帶來(lái)的資源浪費(fèi),又能防止棧溢出問(wèn)題。

3.2 根治泄漏:分離線程或主動(dòng)回收

線程棧內(nèi)存泄漏就像房間里的垃圾越堆越多,最終會(huì)讓整個(gè)系統(tǒng)陷入混亂。為了避免這種情況,我們有兩種有效的方法。

(1)后臺(tái)任務(wù)首選分離:對(duì)于那些在后臺(tái)默默運(yùn)行,不需要返回值,并且執(zhí)行完畢后就可以自動(dòng)清理的任務(wù),比如日志記錄線程、定時(shí)任務(wù)線程等,我們可以在創(chuàng)建線程后,立即調(diào)用pthread_detach函數(shù)將其設(shè)置為unjoinable狀態(tài) 。這樣,當(dāng)線程退出時(shí),系統(tǒng)會(huì)自動(dòng)回收它的棧資源,就像一個(gè)自動(dòng)清理的房間,不會(huì)留下任何垃圾。例如:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* background_task(void* arg) {
    // 后臺(tái)任務(wù)的具體邏輯
    return NULL;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, background_task, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
    if (pthread_detach(thread) != 0) {
        perror("pthread_detach");
        return 1;
    }
    return 0;
}

(2)必須 join 時(shí)用批量等待:當(dāng)線程的返回值對(duì)程序很重要,必須使用pthread_join等待線程結(jié)束時(shí),為了避免逐個(gè)等待帶來(lái)的性能損耗,我們可以將所有需要等待的線程 ID 存儲(chǔ)在一個(gè)數(shù)組中,然后在合適的時(shí)機(jī),通過(guò)循環(huán)批量調(diào)用pthread_join 。比如在一個(gè)多線程數(shù)據(jù)處理程序中,多個(gè)線程分別處理不同的數(shù)據(jù)塊,最后需要匯總結(jié)果:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define THREAD_NUM 5

void* data_processing(void* arg) {
    int id = *(int*)arg;
    // 數(shù)據(jù)處理的具體邏輯
    return (void*)(long)id;
}

int main() {
    pthread_t threads[THREAD_NUM];
    int ids[THREAD_NUM];
    void* results[THREAD_NUM];

    for (int i = 0; i < THREAD_NUM; i++) {
        ids[i] = i;
        if (pthread_create(&threads[i], NULL, data_processing, &ids[i]) != 0) {
            perror("pthread_create");
            return 1;
        }
    }

    for (int i = 0; i < THREAD_NUM; i++) {
        if (pthread_join(threads[i], &results[i]) != 0) {
            perror("pthread_join");
            return 1;
        }
        printf("Thread %d returned: %ld\n", (int)(long)results[i], (long)results[i]);
    }

    return 0;
}

通過(guò)這種批量等待的方式,可以減少線程等待的時(shí)間,提高程序的整體性能。

3.3 線程池:拒絕 “無(wú)限創(chuàng)建” 的惡性循環(huán)

在高并發(fā)場(chǎng)景下,頻繁創(chuàng)建和銷(xiāo)毀線程就像不停地建造和拆除房子,不僅耗費(fèi)資源,還容易導(dǎo)致地址空間耗盡。線程池就像是一個(gè)預(yù)先建好的房子集合,可以有效地復(fù)用線程,避免這種資源浪費(fèi)。

在 C++ 中,我們可以使用std::thread結(jié)合任務(wù)隊(duì)列來(lái)實(shí)現(xiàn)簡(jiǎn)單的線程池 。例如,通過(guò)ThreadPoolExecutor類(lèi)庫(kù)來(lái)創(chuàng)建線程池,將核心線程數(shù)設(shè)置為 CPU 核心數(shù) ×2 ,這樣可以充分利用 CPU 資源,又不會(huì)因?yàn)榫€程過(guò)多而導(dǎo)致資源耗盡。對(duì)于 IO 密集型的應(yīng)用,可以適當(dāng)增加核心線程數(shù),以提高系統(tǒng)的并發(fā)處理能力。比如在一個(gè)網(wǎng)絡(luò)爬蟲(chóng)程序中,每個(gè)頁(yè)面的下載和解析都是一個(gè) IO 密集型任務(wù),我們可以將核心線程數(shù)設(shè)置為 CPU 核心數(shù) ×4 ,以加快爬蟲(chóng)的速度。

案例:在一個(gè)處理萬(wàn)級(jí)連接的 Web 服務(wù)器中,如果為每個(gè)連接都分配一個(gè)獨(dú)立的線程,那么當(dāng)連接數(shù)達(dá)到一定數(shù)量時(shí),系統(tǒng)很容易因?yàn)榈刂房臻g耗盡而崩潰。而改用線程池結(jié)合異步 IO 的方式后,線程池中的線程可以復(fù)用,大大降低了棧內(nèi)存的總消耗 。同時(shí),異步 IO 可以讓線程在等待 IO 操作完成時(shí),去處理其他任務(wù),提高了系統(tǒng)的整體性能。例如,Nginx 服務(wù)器就是采用了這種方式,通過(guò)高效的線程池和異步 IO 機(jī)制,能夠穩(wěn)定地處理大量的并發(fā)連接,成為了 Web 服務(wù)器領(lǐng)域的佼佼者。

下面我將實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 C++ 線程池,展示如何在高并發(fā)場(chǎng)景下通過(guò)線程復(fù)用提高資源利用率。這個(gè)線程池會(huì)創(chuàng)建固定數(shù)量的工作線程,從任務(wù)隊(duì)列中獲取任務(wù)并執(zhí)行,避免了頻繁創(chuàng)建和銷(xiāo)毀線程的開(kāi)銷(xiāo)。

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <chrono>
#include <cmath>
#include <numeric>

// 線程池類(lèi)
class ThreadPool {
private:
    // 工作線程數(shù)量
    size_t num_threads;
    // 工作線程容器
    std::vector<std::thread> workers;
    // 任務(wù)隊(duì)列
    std::queue<std::function<void()>> tasks;

    // 同步機(jī)制
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

public:
    // 構(gòu)造函數(shù),默認(rèn)線程數(shù)為CPU核心數(shù)×2
    ThreadPool(size_t threads = std::thread::hardware_concurrency() * 2) 
        : num_threads(threads), stop(false) {

        // 創(chuàng)建工作線程
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                // 工作線程循環(huán),處理任務(wù)
                while (true) {
                    std::function<void()> task;

                    // 加鎖獲取任務(wù)
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        // 等待任務(wù)或停止信號(hào)
                        this->condition.wait(lock, 
                            [this] { return this->stop || !this->tasks.empty(); });

                        // 如果停止且任務(wù)隊(duì)列為空,則退出
                        if (this->stop && this->tasks.empty())
                            return;

                        // 獲取任務(wù)
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }

                    // 執(zhí)行任務(wù)
                    task();
                }
            });
        }

        std::cout << "線程池初始化完成,工作線程數(shù)量: " << threads << std::endl;
        std::cout << "系統(tǒng)CPU核心數(shù): " << std::thread::hardware_concurrency() << std::endl;
    }

    // 析構(gòu)函數(shù),停止所有工作線程
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }

        // 喚醒所有工作線程
        condition.notify_all();

        // 等待所有工作線程完成
        for (std::thread &worker : workers)
            worker.join();

        std::cout << "線程池已關(guān)閉" << std::endl;
    }

    // 向任務(wù)隊(duì)列添加任務(wù)
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type> {

        using return_type = typename std::result_of<F(Args...)>::type;

        // 包裝任務(wù)為shared_ptr
        auto task = std::make_shared< std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

        // 獲取future用于獲取任務(wù)結(jié)果
        std::future<return_type> res = task->get_future();

        // 將任務(wù)添加到隊(duì)列
        {
            std::unique_lock<std::mutex> lock(queue_mutex);

            // 如果線程池已停止,不能添加任務(wù)
            if (stop)
                throw std::runtime_error("向已停止的線程池添加任務(wù)");

            tasks.emplace([task]() { (*task)(); });
        }

        // 喚醒一個(gè)工作線程
        condition.notify_one();

        return res;
    }
};

// 模擬CPU密集型任務(wù):計(jì)算素?cái)?shù)
bool is_prime(int n) {
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 == 0 || n % 3 == 0) return false;

    for (int i = 5; i * i <= n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0)
            return false;
    }
    return true;
}

// 模擬IO密集型任務(wù):休眠一段時(shí)間
void io_task(int task_id, int delay_ms) {
    std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
    std::cout << "IO任務(wù) " << task_id << " 完成,耗時(shí) " << delay_ms << "ms\n";
}

int main() {
    // 創(chuàng)建線程池,使用默認(rèn)線程數(shù)(CPU核心數(shù)×2)
    ThreadPool pool;

    // 存儲(chǔ)任務(wù)的future
    std::vector<std::future<bool>> prime_results;
    std::vector<std::future<void>> io_results;

    std::cout << "\n=== 提交CPU密集型任務(wù) ===" << std::endl;
    // 提交一系列CPU密集型任務(wù):檢查大數(shù)字是否為素?cái)?shù)
    for (int i = 0; i < 20; ++i) {
        int num = 100000000 + i * 10000 + rand() % 10000;
        prime_results.emplace_back(
            pool.enqueue(is_prime, num)
        );

        // 打印提交狀態(tài)
        if (i % 5 == 0) {
            std::cout << "已提交 " << i + 1 << " 個(gè)素?cái)?shù)檢查任務(wù)\n";
        }
    }

    std::cout << "\n=== 提交IO密集型任務(wù) ===" << std::endl;
    // 提交一系列IO密集型任務(wù)
    for (int i = 0; i < 15; ++i) {
        int delay = 100 + rand() % 400; // 100-500ms隨機(jī)延遲
        io_results.emplace_back(
            pool.enqueue(io_task, i + 1, delay)
        );

        if (i % 5 == 0) {
            std::cout << "已提交 " << i + 1 << " 個(gè)IO任務(wù)\n";
        }
    }

    // 等待所有CPU任務(wù)完成并輸出結(jié)果
    std::cout << "\n=== CPU任務(wù)結(jié)果 ===" << std::endl;
    for (size_t i = 0; i < prime_results.size(); ++i) {
        int num = 100000000 + i * 10000 + rand() % 10000;
        bool result = prime_results[i].get();
        std::cout << "數(shù)字 " << num << (result ? " 是" : " 不是") << " 素?cái)?shù)\n";
    }

    // 等待所有IO任務(wù)完成
    std::cout << "\n=== 等待所有IO任務(wù)完成 ===" << std::endl;
    for (auto &f : io_results) {
        f.get();
    }

    std::cout << "\n所有任務(wù)處理完畢" << std::endl;
    return 0;
}
  1. 預(yù)先創(chuàng)建固定數(shù)量的工作線程(默認(rèn) CPU 核心數(shù) ×2)
  2. 使用任務(wù)隊(duì)列存儲(chǔ)待執(zhí)行的任務(wù)
  3. 通過(guò)互斥鎖和條件變量實(shí)現(xiàn)線程同步
  4. 工作線程循環(huán)從隊(duì)列中獲取并執(zhí)行任務(wù),實(shí)現(xiàn)線程復(fù)用

編譯和運(yùn)行方法:

g++ -std=c++11 thread_pool.cpp -o thread_pool -lpthread
./thread_pool

運(yùn)行后,你會(huì)看到線程池如何高效地分配任務(wù)給工作線程,實(shí)現(xiàn)任務(wù)的并發(fā)處理。與為每個(gè)任務(wù)創(chuàng)建新線程的方式相比,這種方式大大減少了系統(tǒng)資源消耗,特別是在處理大量任務(wù)的場(chǎng)景下優(yōu)勢(shì)更加明顯。

在實(shí)際應(yīng)用中,你可以根據(jù)任務(wù)類(lèi)型(CPU 密集型或 IO 密集型)調(diào)整線程池大小,以達(dá)到最佳性能。例如,IO 密集型應(yīng)用可以使用更多線程,而 CPU 密集型應(yīng)用通常線程數(shù)不宜超過(guò) CPU 核心數(shù)的 2-3 倍。

3.4 遞歸轉(zhuǎn)迭代:用 “手動(dòng)?!?防溢出

遞歸算法雖然簡(jiǎn)潔直觀,但在多線程環(huán)境中,它就像一個(gè)隱藏的炸彈,隨時(shí)可能因?yàn)闂R绯龆鴮?dǎo)致程序崩潰。將遞歸轉(zhuǎn)換為迭代,使用 “手動(dòng)?!?來(lái)模擬遞歸過(guò)程,可以有效地避免棧溢出問(wèn)題。

(1)反模式:以經(jīng)典的斐波那契數(shù)列計(jì)算為例,遞歸實(shí)現(xiàn)如下:

#include <stdio.h>

int fibonacci_recursive(int n) {
    if (n == 0 || n == 1) {
        return n;
    }
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
}

int main() {
    int n = 30;
    int result = fibonacci_recursive(n);
    printf("Fibonacci(%d) = %d\n", n, result);
    return 0;
}

在這個(gè)實(shí)現(xiàn)中,隨著n的增大,遞歸調(diào)用的層數(shù)會(huì)迅速增加,??臻g也會(huì)被快速耗盡,最終導(dǎo)致棧溢出。

(2)優(yōu)化版:使用迭代和手動(dòng)棧的方式實(shí)現(xiàn)斐波那契數(shù)列計(jì)算:

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int n;
    int result;
} StackFrame;

int fibonacci_iterative(int n) {
    StackFrame *stack = (StackFrame*)malloc((n + 1) * sizeof(StackFrame));
    if (stack == NULL) {
        perror("malloc");
        return -1;
    }
    int top = -1;

    stack[++top].n = n;
    while (top >= 0) {
        StackFrame *current = &stack[top];
        if (current->n == 0 || current->n == 1) {
            current->result = current->n;
            top--;
            if (top >= 0) {
                StackFrame *parent = &stack[top];
                if (parent->n == current->n + 1) {
                    parent->result += current->result;
                } else {
                    stack[++top].n = parent->n - 2;
                }
            }
        } else {
            stack[++top].n = current->n - 1;
        }
    }

    int result = stack[0].result;
    free(stack);
    return result;
}

int main() {
    int n = 30;
    int result = fibonacci_iterative(n);
    printf("Fibonacci(%d) = %d\n", n, result);
    return 0;
}

在這個(gè)優(yōu)化版本中,我們使用malloc動(dòng)態(tài)分配了一個(gè) “手動(dòng)棧”,通過(guò)模擬遞歸調(diào)用的過(guò)程,將計(jì)算結(jié)果逐步保存和累加,避免了遞歸調(diào)用帶來(lái)的棧溢出風(fēng)險(xiǎn)。雖然代碼變得稍微復(fù)雜一些,但卻提高了程序的穩(wěn)定性和性能,就像從走鋼絲變成了走平穩(wěn)的橋梁,更加安全可靠。

四、診斷工具:快速定位棧內(nèi)存問(wèn)題

即便我們?cè)诖a中做了各種優(yōu)化,但在實(shí)際運(yùn)行中,線程棧還是可能出現(xiàn)各種問(wèn)題。這時(shí),就需要借助一些強(qiáng)大的診斷工具,快速定位和解決問(wèn)題。

4.1 查看棧布局:proc 文件系統(tǒng)的魔法

/proc文件系統(tǒng)就像是 Linux 系統(tǒng)的 “內(nèi)部監(jiān)控室”,通過(guò)它我們可以輕松獲取進(jìn)程和線程的各種信息。如何通過(guò) /proc 文件系統(tǒng)查看線程的棧地址信息。程序會(huì)創(chuàng)建多個(gè)線程,并讀取對(duì)應(yīng) /proc 路徑下的 maps 文件來(lái)獲取線程棧的內(nèi)存映射信息:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>

// 全局變量,控制線程運(yùn)行
volatile int running = 1;

// 線程函數(shù),簡(jiǎn)單休眠等待
void *thread_function(void *arg) {
    int thread_num = *(int *)arg;
    free(arg);

    printf("線程 %d 啟動(dòng),TID: %lu,PID: %d\n", 
           thread_num, (unsigned long)pthread_self(), getpid());

    // 線程循環(huán)等待,保持存活以便查看信息
    while (running) {
        sleep(1);
    }

    printf("線程 %d 退出\n", thread_num);
    return NULL;
}

// 讀取并解析/proc/[pid]/task/[tid]/maps文件,查找棧信息
void print_thread_stack_info(pid_t pid, pthread_t tid) {
    char filename[256];
    FILE *file;
    char line[1024];

    // 構(gòu)造maps文件路徑
    snprintf(filename, sizeof(filename), "/proc/%d/task/%lu/maps", pid, (unsigned long)tid);

    // 打開(kāi)文件
    file = fopen(filename, "r");
    if (!file) {
        fprintf(stderr, "無(wú)法打開(kāi)文件 %s: %s\n", filename, strerror(errno));
        return;
    }

    printf("\n線程 TID: %lu 的內(nèi)存映射信息 (棧相關(guān)部分):\n", (unsigned long)tid);
    printf("-------------------------------------------------\n");

    // 讀取文件內(nèi)容,查找棧相關(guān)的條目
    while (fgets(line, sizeof(line), file)) {
        // 棧通常標(biāo)記為 "[stack]" 或沒(méi)有明確標(biāo)記但具有讀寫(xiě)權(quán)限
        if (strstr(line, "[stack]") || 
            (strstr(line, "rw-p") && !strstr(line, " [") && !strstr(line, "/"))) {
            printf("%s", line);
        }
    }

    printf("-------------------------------------------------\n");
    fclose(file);
}

int main() {
    const int NUM_THREADS = 3;
    pthread_t threads[NUM_THREADS];
    pid_t pid = getpid();

    printf("主線程啟動(dòng),PID: %d\n", pid);
    printf("將創(chuàng)建 %d 個(gè)子線程...\n", NUM_THREADS);

    // 創(chuàng)建多個(gè)線程
    for (int i = 0; i < NUM_THREADS; i++) {
        int *thread_num = malloc(sizeof(int));
        *thread_num = i + 1;

        if (pthread_create(&threads[i], NULL, thread_function, thread_num) != 0) {
            fprintf(stderr, "創(chuàng)建線程 %d 失敗\n", i + 1);
            return 1;
        }
    }

    // 等待所有線程啟動(dòng)
    sleep(2);

    // 查看每個(gè)線程的棧信息
    for (int i = 0; i < NUM_THREADS; i++) {
        print_thread_stack_info(pid, threads[i]);
    }

    printf("\n線程棧信息已顯示完畢\n");
    printf("提示: 你也可以在終端中運(yùn)行以下命令查看詳細(xì)信息:\n");
    for (int i = 0; i < NUM_THREADS; i++) {
        printf("  cat /proc/%d/task/%lu/maps\n", pid, (unsigned long)threads[i]);
    }

    // 通知線程退出并等待
    running = 0;
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("\n所有線程已退出\n");
    return 0;
}
  1. 創(chuàng)建多線程:程序創(chuàng)建了 3 個(gè)線程,每個(gè)線程都會(huì)打印自己的 TID (線程 ID) 和 PID (進(jìn)程 ID)
  2. 訪問(wèn) /proc 文件系統(tǒng):構(gòu)造路徑/proc/[pid]/task/[tid]/maps,其中 [pid] 是進(jìn)程 ID,[tid] 是線程 ID,讀取該文件內(nèi)容,解析出與線程棧相關(guān)的內(nèi)存映射信息。
  3. 棧信息解析:在 maps 文件中,棧通常標(biāo)記為 "[stack]",程序會(huì)篩選并顯示具有讀寫(xiě)權(quán)限 (rw-p) 且可能是棧的內(nèi)存區(qū)域,輸出的每一行包含內(nèi)存地址范圍、權(quán)限、偏移量、設(shè)備號(hào)和 inode 信息

編譯和運(yùn)行方法:

gcc proc_thread_stack.c -o proc_thread_stack -lpthread
./proc_thread_stack

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出:

主線程啟動(dòng),PID: 12345
將創(chuàng)建 3 個(gè)子線程...
線程 1 啟動(dòng),TID: 12346,PID: 12345
線程 2 啟動(dòng),TID: 12347,PID: 12345
線程 3 啟動(dòng),TID: 12348,PID: 12345

線程 TID: 12346 的內(nèi)存映射信息 (棧相關(guān)部分):
-------------------------------------------------
7f8a3a6f9000-7f8a3a77a000 rw-p 00000000 00:00 0                          [stack]
-------------------------------------------------
...

提示: 你也可以在終端中運(yùn)行以下命令查看詳細(xì)信息:
  cat /proc/12345/task/12346/maps
  cat /proc/12345/task/12347/maps
  cat /proc/12345/task/12348/maps

所有線程已退出

通過(guò)這個(gè)程序,你可以理解如何通過(guò) /proc 文件系統(tǒng)這個(gè) "內(nèi)部監(jiān)控室" 來(lái)查看線程的棧地址信息。在實(shí)際開(kāi)發(fā)中,這對(duì)于調(diào)試和理解程序內(nèi)存布局非常有用;還可以按照程序提示的命令,在另一個(gè)終端中手動(dòng)查看更詳細(xì)的內(nèi)存映射信息,進(jìn)一步了解每個(gè)線程的??臻g分布。

4.2 監(jiān)控棧限制:ulimit 命令顯身手

ulimit命令是我們監(jiān)控和調(diào)整棧限制的得力助手;要查看當(dāng)前棧大小限制,可以在終端中輸入ulimit -s ,它會(huì)以 KB 為單位輸出當(dāng)前棧的軟限制值。例如,輸出8192表示當(dāng)前棧的默認(rèn)大小為 8MB 。這個(gè)值就像是一個(gè) “警戒線”,提醒我們棧空間的最大可用范圍。

如果我們需要臨時(shí)調(diào)整單個(gè)進(jìn)程的棧限制,可以使用ulimit -s [size]命令 ,其中[size]是我們想要設(shè)置的棧大小,單位同樣是 KB 。比如,我們想將棧大小臨時(shí)調(diào)整為 4MB,可以執(zhí)行ulimit -s 4096 。不過(guò)要注意,這種調(diào)整只對(duì)當(dāng)前終端會(huì)話有效,一旦退出終端,設(shè)置就會(huì)失效。

如何在代碼中獲取和設(shè)置棧大小限制,這對(duì)應(yīng)于 ulimit 命令的功能。程序?qū)⒄故井?dāng)前棧限制、修改棧限制,并測(cè)試不同棧大小對(duì)程序運(yùn)行的影響:

#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <string.h>
#include <errno.h>

// 遞歸函數(shù),用于測(cè)試棧大小
void recursive_test(int depth) {
    // 在棧上分配一些內(nèi)存
    char stack_buffer[1024];

    // 每100層打印一次信息
    if (depth % 100 == 0) {
        printf("遞歸深度: %d, 棧位置: %p\n", depth, stack_buffer);
    }

    // 繼續(xù)遞歸
    recursive_test(depth + 1);
}

// 打印當(dāng)前棧大小限制
void print_stack_limit() {
    struct rlimit rl;

    if (getrlimit(RLIMIT_STACK, &rl) == 0) {
        printf("當(dāng)前棧大小限制:\n");
        printf("  軟限制: %lld KB\n", (long long)rl.rlim_cur / 1024);
        printf("  硬限制: %lld KB\n", (long long)rl.rlim_max / 1024);
    } else {
        fprintf(stderr, "獲取棧限制失敗: %s\n", strerror(errno));
    }
}

// 設(shè)置棧大小限制
int set_stack_limit(size_t new_soft_limit_kb, size_t new_hard_limit_kb) {
    struct rlimit rl;

    // 轉(zhuǎn)換為字節(jié)
    rl.rlim_cur = new_soft_limit_kb * 1024;
    rl.rlim_max = new_hard_limit_kb * 1024;

    if (setrlimit(RLIMIT_STACK, &rl) == 0) {
        printf("已設(shè)置新的棧大小限制:\n");
        printf("  新軟限制: %zu KB\n", new_soft_limit_kb);
        printf("  新硬限制: %zu KB\n", new_hard_limit_kb);
        return 0;
    } else {
        fprintf(stderr, "設(shè)置棧限制失敗: %s\n", strerror(errno));
        return 1;
    }
}

int main() {
    printf("=== 初始棧大小限制 ===\n");
    print_stack_limit();

    // 嘗試將棧大小限制修改為4MB(軟限制)和8MB(硬限制)
    printf("\n=== 嘗試修改棧大小限制 ===\n");
    if (set_stack_limit(4096, 8192) == 0) {
        printf("\n=== 修改后的棧大小限制 ===\n");
        print_stack_limit();
    }

    // 測(cè)試當(dāng)前棧大小
    printf("\n=== 開(kāi)始棧深度測(cè)試 ===\n");
    printf("將遞歸直到棧溢出...\n");

    try {
        recursive_test(1);
    } catch (...) {
        fprintf(stderr, "捕獲到異常,可能是棧溢出\n");
    }

    return 0;
}
  1. 獲取棧大小限制:使用getrlimit(RLIMIT_STACK, &rl)函數(shù)獲取當(dāng)前棧的軟限制和硬限制,軟限制是當(dāng)前生效的限制,硬限制是軟限制能達(dá)到的最大值,對(duì)應(yīng)ulimit -s命令查看當(dāng)前棧大小限制的功能。
  2. 設(shè)置棧大小限制:使用setrlimit(RLIMIT_STACK, &rl)函數(shù)修改棧限制,對(duì)應(yīng)ulimit -s [size]命令修改棧大小限制的功能,程序中嘗試將軟限制設(shè)為 4MB,硬限制設(shè)為 8MB。
  3. 棧大小測(cè)試:通過(guò)遞歸函數(shù)不斷在棧上分配內(nèi)存,直到棧溢出,展示不同棧大小限制對(duì)程序運(yùn)行的實(shí)際影響

編譯和運(yùn)行方法:

gcc stack_limit_demo.c -o stack_limit_demo
./stack_limit_demo

運(yùn)行后,你會(huì)看到類(lèi)似這樣的輸出:

=== 初始棧大小限制 ===
當(dāng)前棧大小限制:
  軟限制: 8192 KB
  硬限制: -1 KB

=== 嘗試修改棧大小限制 ===
已設(shè)置新的棧大小限制:
  新軟限制: 4096 KB
  新硬限制: 8192 KB

=== 修改后的棧大小限制 ===
當(dāng)前棧大小限制:
  軟限制: 4096 KB
  硬限制: 8192 KB

=== 開(kāi)始棧深度測(cè)試 ===
將遞歸直到棧溢出...
遞歸深度: 100, 棧位置: 0x7ffd9b5e96e0
遞歸深度: 200, 棧位置: 0x7ffd9b5e92a0
...
段錯(cuò)誤 (核心已轉(zhuǎn)儲(chǔ))

注意事項(xiàng):

  • 普通用戶(hù)不能將硬限制設(shè)置得比系統(tǒng)默認(rèn)值更高
  • 棧大小限制的修改只對(duì)當(dāng)前進(jìn)程及其子進(jìn)程有效,類(lèi)似于 ulimit 命令的會(huì)話有效性
  • 程序最終會(huì)因棧溢出而崩潰,這是正?,F(xiàn)象,用于展示棧限制的實(shí)際效果

通過(guò)這個(gè)程序,你可以理解 ulimit 命令背后的工作原理,以及如何在代碼中直接操作這些限制。

4.3 調(diào)試神器 gdb:定位棧溢出現(xiàn)場(chǎng)

當(dāng)程序出現(xiàn)段錯(cuò)誤,懷疑是棧溢出導(dǎo)致時(shí),gdb(GNU Debugger)就是我們的 “終極武器”。

首先,使用gdb加載發(fā)生段錯(cuò)誤的可執(zhí)行文件和對(duì)應(yīng)的core文件(如果有生成的話)。然后,通過(guò)backtrace(縮寫(xiě)為bt)命令查看函數(shù)調(diào)用棧 。例如,當(dāng)我們運(yùn)行g(shù)db并加載文件后,輸入bt,可能會(huì)得到如下輸出:

#0  0x00007ffff7a2b830 in deep_recursion (n=1234) at test.c:12
#1  0x00007ffff7a2b7f0 in deep_recursion (n=1235) at test.c:12
#2  0x00007ffff7a2b7f0 in deep_recursion (n=1236) at test.c:12
#3  0x00007ffff7a2b830 in deep_recursion (n=1234) at test.c:12

從這個(gè)輸出中,我們可以清晰地看到函數(shù)的調(diào)用關(guān)系和遞歸的深度。#0表示當(dāng)前棧幀,也就是發(fā)生錯(cuò)誤的位置,這里顯示錯(cuò)誤發(fā)生在test.c文件的第 12 行,函數(shù)deep_recursion中,當(dāng)時(shí)的參數(shù)n為 1234 。通過(guò)這些信息,我們就可以迅速定位到棧溢出的源頭,進(jìn)而有針對(duì)性地進(jìn)行修復(fù)。

五、Linux 線程棧案例實(shí)戰(zhàn)

線程棧是線程獨(dú)立擁有的內(nèi)存區(qū)域,用于存儲(chǔ)函數(shù)調(diào)用、局部變量等信息。在 Linux 系統(tǒng)中,我們可以通過(guò) pthread 庫(kù)來(lái)管理線程棧。下面通過(guò)實(shí)際案例來(lái)展示線程棧的使用和相關(guān)操作

5.1默認(rèn)線程棧演示

首先我們來(lái)看一個(gè)使用默認(rèn)線程棧的簡(jiǎn)單示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/resource.h>

// 線程函數(shù)
void *thread_func(void *arg) {
    int local_var; // 局部變量,存儲(chǔ)在棧上
    printf("子線程: 局部變量地址: %p\n", &local_var);

    // 獲取線程棧大小限制
    struct rlimit rl;
    if (getrlimit(RLIMIT_STACK, &rl) == 0) {
        printf("子線程: 棧大小限制 - 軟限制: %ld, 硬限制: %ld\n", 
               (long)rl.rlim_cur, (long)rl.rlim_max);
    }

    sleep(1);
    return NULL;
}

int main() {
    pthread_t tid;
    int ret;

    // 創(chuàng)建線程,使用默認(rèn)棧設(shè)置
    ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
        fprintf(stderr, "創(chuàng)建線程失敗\n");
        exit(EXIT_FAILURE);
    }

    printf("主線程: 等待子線程完成...\n");
    pthread_join(tid, NULL);
    printf("主線程: 子線程已結(jié)束\n");

    return 0;
}

編譯運(yùn)行方法:

gcc default_stack_demo.c -o default_stack -lpthread ./default_stack

5.2自定義線程棧大小

我們可以通過(guò) pthread_attr_t 結(jié)構(gòu)體來(lái)設(shè)置線程的棧大?。?/span>

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define STACK_SIZE (1024 * 1024) // 1MB 棧大小

void *thread_func(void *arg) {
    int local_var;
    printf("子線程: 局部變量地址: %p\n", &local_var);
    printf("子線程: 正在運(yùn)行...\n");
    sleep(1);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    int ret;
    size_t stack_size;
    void *stack_addr;

    // 初始化線程屬性
    ret = pthread_attr_init(&attr);
    if (ret != 0) {
        fprintf(stderr, "初始化線程屬性失敗: %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }

    // 分配棧內(nèi)存
    stack_addr = malloc(STACK_SIZE);
    if (stack_addr == NULL) {
        fprintf(stderr, "內(nèi)存分配失敗\n");
        exit(EXIT_FAILURE);
    }

    // 設(shè)置棧大小和棧地址
    ret = pthread_attr_setstack(&attr, stack_addr, STACK_SIZE);
    if (ret != 0) {
        fprintf(stderr, "設(shè)置棧失敗: %s\n", strerror(ret));
        free(stack_addr);
        exit(EXIT_FAILURE);
    }

    // 獲取并打印棧大小
    ret = pthread_attr_getstacksize(&attr, &stack_size);
    if (ret != 0) {
        fprintf(stderr, "獲取棧大小失敗: %s\n", strerror(ret));
    } else {
        printf("主線程: 設(shè)置的線程棧大小為: %zu 字節(jié)\n", stack_size);
    }

    // 創(chuàng)建線程
    ret = pthread_create(&tid, &attr, thread_func, NULL);
    if (ret != 0) {
        fprintf(stderr, "創(chuàng)建線程失敗: %s\n", strerror(ret));
        free(stack_addr);
        exit(EXIT_FAILURE);
    }

    // 等待線程結(jié)束
    pthread_join(tid, NULL);
    printf("主線程: 子線程已結(jié)束\n");

    // 清理資源
    pthread_attr_destroy(&attr);
    free(stack_addr);

    return 0;
}

編譯運(yùn)行方法:

gcc custom_stack_demo.c -o custom_stack -lpthread
./custom_stack

5.3線程棧溢出演示

當(dāng)線程使用的??臻g超過(guò)其分配的大小時(shí),會(huì)發(fā)生棧溢出,通常會(huì)導(dǎo)致程序崩潰:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define SMALL_STACK_SIZE (4096) // 非常小的棧大小

// 遞歸函數(shù),會(huì)消耗大量??臻g
void recursive_func(int depth) {
    char large_array[1024]; // 每次遞歸分配1KB
    printf("遞歸深度: %d, 數(shù)組地址: %p\n", depth, large_array);

    // 繼續(xù)遞歸,直到棧溢出
    recursive_func(depth + 1);
}

void *thread_func(void *arg) {
    printf("子線程: 開(kāi)始執(zhí)行,嘗試遞歸調(diào)用...\n");
    recursive_func(1); // 開(kāi)始遞歸
    return NULL; // 這行永遠(yuǎn)不會(huì)執(zhí)行
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    int ret;
    void *stack_addr;

    // 初始化線程屬性
    ret = pthread_attr_init(&attr);
    if (ret != 0) {
        fprintf(stderr, "初始化線程屬性失敗: %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }

    // 分配小棧內(nèi)存
    stack_addr = malloc(SMALL_STACK_SIZE);
    if (stack_addr == NULL) {
        fprintf(stderr, "內(nèi)存分配失敗\n");
        exit(EXIT_FAILURE);
    }

    // 設(shè)置小棧大小
    ret = pthread_attr_setstack(&attr, stack_addr, SMALL_STACK_SIZE);
    if (ret != 0) {
        fprintf(stderr, "設(shè)置棧失敗: %s\n", strerror(ret));
        free(stack_addr);
        exit(EXIT_FAILURE);
    }

    // 創(chuàng)建線程
    ret = pthread_create(&tid, &attr, thread_func, NULL);
    if (ret != 0) {
        fprintf(stderr, "創(chuàng)建線程失敗: %s\n", strerror(ret));
        free(stack_addr);
        exit(EXIT_FAILURE);
    }

    // 等待線程結(jié)束
    pthread_join(tid, NULL);
    printf("主線程: 子線程已結(jié)束\n"); // 棧溢出后可能不會(huì)執(zhí)行到這里

    // 清理資源
    pthread_attr_destroy(&attr);
    free(stack_addr);

    return 0;
}

編譯運(yùn)行方法:

gcc stack_overflow_demo.c -o stack_overflow -lpthread
./stack_overflow


責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2023-02-07 08:55:04

進(jìn)程棧內(nèi)存底層

2025-04-07 03:02:00

電腦內(nèi)存數(shù)據(jù)

2025-09-02 03:33:00

2025-04-02 07:29:14

2025-08-29 01:45:00

2025-10-27 01:22:00

HTTP接口API

2020-04-28 22:12:30

Nginx正向代理反向代理

2022-02-28 10:05:12

組件化架構(gòu)設(shè)計(jì)從原組件化模塊化

2025-04-03 00:03:00

數(shù)據(jù)內(nèi)存網(wǎng)絡(luò)

2025-09-05 07:13:13

2025-02-25 12:00:00

Java線程開(kāi)發(fā)

2024-03-27 10:14:48

2025-05-28 08:45:00

2025-10-30 07:45:06

2025-09-08 07:14:25

2017-03-14 18:48:06

Android性能優(yōu)化內(nèi)存優(yōu)化

2021-05-11 07:51:30

React ref 前端

2025-09-29 01:50:00

2024-07-07 21:49:22

2025-09-15 01:45:00

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)