本篇旨在通過幾個案例來學習父子進程的線程異步性
一、父進程與子進程
我們將要做的: 創建父子進程,觀察父子進程執行的順序,了解進程執行的異步行為
源代碼:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> // 定義了 POSIX 操作系統 API(Unix/Linux 下的系統調用函數)
#include <stdlib.h>int main()
{pid_t pid; // 進程idchar*msg; // 信息緩沖區int k; // 變量,后面用于控制執行打印的次數printf("觀察父子進程執行的先后順序,了解調度算法的特征\n");pid=fork(); // 創建子進程switch(pid){case 0:msg="子進程在運行";k=3;break;case -1:msg="進程創建失敗";break; default:msg="父進程在運行";k=5;break;}while(k>0){puts(msg); sleep(1); k--; }exit(0);
}
🔧1. 頭文件講解
#include <sys/types.h>
- 作用:定義數據類型,如
pid_t
。 pid_t
是一個整型,用于表示進程 ID,確保跨平臺一致性。- 一般與
fork()
、wait()
等系統調用一起使用。
#include <unistd.h>
- 作用:定義了 POSIX 操作系統 API(Unix/Linux 下的系統調用函數)。
- 提供本程序中用到的:
fork()
:創建子進程sleep()
:讓進程休眠若干秒- 還包括
getpid()
(獲取進程ID)、exec
族函數(程序替換)等。
#include <stdlib.h>
- 作用:標準庫函數,如內存分配、程序控制等。
- 本程序中使用了:
exit(0)
:正常退出當前進程(0 表示正常退出)
🧠 2. 核心函數講解
fork()
- 函數原型:
pid_t fork(void);
- 作用:創建一個新的子進程,該子進程是調用它的進程的副本。
- 返回值:
- 在父進程中,
fork()
返回子進程的 PID(大于 0) - 在子進程中,
fork()
返回 0 - 創建失敗返回 -1
- 在父進程中,
switch(pid)
- 根據
fork()
的返回值來判斷當前是:- 子進程(
pid == 0
) - 父進程(
pid > 0
) - 創建失敗(
pid == -1
)
- 子進程(
puts(msg)
- 輸出字符串
msg
并自動換行,功能類似于printf("%s\\n", msg);
,但更簡單。
sleep(1)
- 暫停當前線程執行 1 秒鐘,模擬處理過程,也便于觀察進程輸出順序。
exit(0)
- 正常終止當前進程。系統看到返回值 0,認為程序成功執行。
📌 3. 程序運行邏輯總結
- 調用
fork()
創建子進程,得到兩個并發執行的進程。 - 每個進程根據
fork()
的返回值設定自己的輸出內容(msg
)和輸出次數(k
)。 - 每個進程都進入
while(k>0)
循環,每秒輸出一次msg
,共輸出k
次。 - 最終執行
exit(0)
正常退出。
🧪 4. 運行效果說明
實際運行時,輸出類似:
觀察父子進程執行的先后順序,了解調度算法的特征
父進程在運行
子進程在運行
子進程在運行
父進程在運行
...
由于父子進程是并發執行的,它們輸出的先后順序會隨著調度器算法、系統負載等因素而變化。
二、主進程與子進程
我們將做的: 創建主線程和子線程,觀察多線程執行的順序,了解線程執行的異步行為
源代碼:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h> // POSIX 線程庫函數static int run=1; // 子線程循環判斷條件,主線程設置為 0 后子線程結束
static int retvalue; // 子線程退出時返回的值,供主線程獲取void *threadfunc(void*arg)
{int*running=arg; // 接受主線程傳入的參數printf("子線程初始化完畢,傳入參數為:%d\n",*running); while(*running) //子線程通過 *running 控制循環是否繼續{printf("子線程正在運行\n");usleep(1); // 微秒級休眠}printf("子線程退出\n");retvalue=8;pthread_exit((void*)&retvalue); // 返回 retvalue 的地址給主線程
}
int main()
{pthread_t tid; // 線程idint ret=-1;int times=3;int i=0;int *ret_join=NULL;// 創建一個線程,線程函數為threadfunc,傳入參數為&runret=pthread_create(&tid,NULL,(void*)threadfunc,&run); if(ret!=0){printf("建立線程失敗\n");return 1;}printf("主線程創建子線程后在運行...\n");usleep(1); // 主線程短暫休眠,為了讓子線程有機會先運行printf("主線程調用usleep(1)...\n");for(;i<times;i++){printf("主線程打印i=%d\n",i);usleep(1);}run=0; // 子進程控制參數設置為0,通知子進程結束pthread_join(tid,(void*)&ret_join); printf("線程返回值為:%d\n",*ret_join);return 0;
}
這段代碼是一個使用 pthread
實現多線程的基礎示例,展示了如何創建線程、線程間共享數據、線程退出返回值,以及主線程如何等待子線程完成。下面詳細逐行講解:
? 1. 頭文件講解
#include <pthread.h> // POSIX 線程庫函數
pthread.h
是 POSIX 標準線程庫頭文件,提供線程創建、同步、退出等函數定義。
🧠 2. 全局變量定義
static int run = 1; // 子線程循環判斷條件,主線程設置為 0 后子線程結束
static int retvalue; // 子線程退出時返回的值,供主線程獲取
run
是主線程與子線程共享的控制變量。retvalue
將作為子線程pthread_exit
返回值的地址,供主線程獲取。
🚀 3. 線程函數 threadfunc
void *threadfunc(void* arg)
{int* running = arg;printf("子線程初始化完畢,傳入參數為:%d\n", *running); while (*running){printf("子線程正在運行\n");usleep(1); // 微秒級休眠(1 微秒 = 0.001 毫秒)}printf("子線程退出\n");retvalue = 8;pthread_exit((void*)&retvalue); // 返回 retvalue 的地址給主線程
}
? 關鍵點說明:
void *threadfunc(void* arg)
是 pthread 要求的線程函數格式。arg
是傳入的參數,實際是主線程傳入&run
。- 子線程通過
*running
控制循環是否繼續。 - 使用
pthread_exit()
顯式結束線程,并返回結果指針。
🧵 4. 主線程 main
pthread_t tid; // 聲明線程 id
int ret = -1; // 初始化返回值
int times = 3; // 打印次數
int i = 0;
int *ret_join = NULL;
ret = pthread_create(&tid, NULL, (void*)threadfunc, &run);
- 創建一個線程,線程函數為
threadfunc
,傳入參數為&run
。 ret
為返回值,0 表示成功。
if(ret != 0)
{printf("建立線程失敗\n");return 1;
}
printf("主線程創建子線程后在運行...\n");
usleep(1);
printf("主線程調用usleep(1)...\n");
- 主線程短暫停頓,為了讓子線程有機會先運行。
for(; i < times; i++)
{printf("主線程打印i=%d\n", i);usleep(1);
}
- 主線程執行 3 次循環,每次 sleep 1 微秒,并打印當前
i
。
run = 0;
- 將共享變量
run
設置為 0,通知子線程退出。
pthread_join(tid, (void*)&ret_join);
- 等待子線程結束,并獲取返回值。
- 注意這里
ret_join
是int *
類型,用于接收retvalue
的地址。
printf("線程返回值為:%d\n", *ret_join);
- 打印子線程返回的值
8
。
🧪 5. 運行輸出示例(大致)
主線程創建子線程后在運行...
主線程調用usleep(1)...
子線程初始化完畢,傳入參數為:1
子線程正在運行
主線程打印i=0
子線程正在運行
主線程打印i=1
子線程正在運行
主線程打印i=2
子線程正在運行
子線程退出
線程返回值為:8
(線程調度不確定,輸出順序可能變化)
📌 6. 知識點總結
項目 | 內容說明 |
---|---|
pthread_create | 創建新線程 |
pthread_join | 等待線程結束,獲取返回值 |
pthread_exit | 子線程退出并返回值 |
usleep(x) | 微秒級休眠,適合線程示例中短暫等待 |
共享變量通信 | 主線程修改 run ,控制子線程退出 |
?? 7. 建議與優化
usleep(1)
睡眠時間太短(1 微秒),可以改為usleep(100000)
(即 0.1 秒)方便觀察輸出。- 變量
retvalue
設置為static
是為了確保其生命周期足夠長,返回地址有效。 - 多線程程序應考慮線程安全問題,如資源競爭、內存可見性等,在多核機器中尤為重要。
三、多線程對共享變量的非互斥訪問
我們將要做的: 構造「多線程共享變量競爭」的現象,并分析現象發生的原因,進而思考解決方式。
源代碼:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>int num=30,count=10;void *sub1(void *arg) {int i = 0,tmp;for (; i <count; i++){tmp=num-1;usleep(13);num=tmp;printf("線程1 num減1后值為: %d\n",num);}return ((void *)0);
}
void *sub2(void *arg){int i=0,tmp;for(;i<count;i++){tmp=num-1;usleep(31);num=tmp;printf("線程2 num減1后值為: %d\n",num);}return ((void *)0);
}
int main(int argc, char** argv) {pthread_t tid1,tid2; // 兩個子線程的idint err,i=0,tmp;void *tret; // 線程返回值err=pthread_create(&tid1,NULL,sub1,NULL);if(err!=0){printf("pthread_create error:%s\n",strerror(err));exit(-1);}err=pthread_create(&tid2,NULL,sub2,NULL);if(err!=0){printf("pthread_create error:%s\n",strerror(err));exit(-1);}for(;i<count;i++){tmp=num-1;usleep(5);num=tmp;printf("main num減1后值為: %d\n",num);}printf("兩個線程運行結束\n");err=pthread_join(tid1,&tret);if(err!=0){printf("can not join with thread1:%s\n",strerror(err));exit(-1);}printf("thread 1 exit code %d\n",(int)tret);err=pthread_join(tid2,&tret);if(err!=0){printf("can not join with thread1:%s\n",strerror(err));exit(-1);}printf("thread 2 exit code %d\n",(int)tret);return 0;
}
🧠 1. 程序功能概述
創建了兩個線程 sub1
和 sub2
,以及主線程三者共同對一個全局變量 num
執行減 1 操作,共減去 count * 3 = 30
次。
初始值:
int num = 30, count = 10;
所以理論上最終 num == 0
,但實際上并不一定!
?? 2. 存在的核心問題:數據競爭(Race Condition)
? 對 num--
是分三步執行的:
tmp = num - 1;
usleep(x);
num = tmp;
這個過程不是原子操作,多個線程可能“交叉”訪問這個變量,造成競態條件(Race Condition)。
中間插入
usleep()
只是為了放大并發寫入帶來的沖突概率,模擬真實環境下的并發問題。
舉例說明:
假設此時 num = 10
,兩個線程同時讀到:
線程1:tmp1 = 10 - 1 = 9,睡眠
線程2:tmp2 = 10 - 1 = 9,睡眠
然后:
線程1醒來執行 num = 9
線程2醒來執行 num = 9 (覆蓋了線程1的操作)
🔴 這樣 num 實際只減少了一次,而我們期望它減少兩次(一個線程分別減少一次)!
🔍 3. 運行效果舉例(輸出可能類似):
線程1 num減1后值為: 29
線程2 num減1后值為: 28
main num減1后值為: 27
線程1 num減1后值為: 27 ←? 重復了
main num減1后值為: 26
線程2 num減1后值為: 26 ←? 再次重復
最終 num
的值可能 不是 0,甚至是更高。原因就是上面說的:很多次減法操作失效了。
?4. 如何解決?使用線程同步機制:互斥鎖 pthread_mutex_t
例如,添加全局互斥鎖:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
將每個對 num
的訪問部分用鎖保護:
pthread_mutex_lock(&lock);
tmp = num - 1;
usleep(13); // 保留你原來的模擬處理
num = tmp;
pthread_mutex_unlock(&lock);
🔒 這樣確保每次只有一個線程在訪問和修改 num
。
🛠? 5. 修改后關鍵片段示例(以 sub1
為例)
void *sub1(void *arg) {int i = 0, tmp;for (; i < count; i++) {pthread_mutex_lock(&lock);tmp = num - 1;usleep(13);num = tmp;printf("線程1 num減1后值為: %d\n", num);pthread_mutex_unlock(&lock);}return ((void *)0);
}
主線程、sub2
中也要加鎖。
🔚 6. 總結
問題 | 說明 |
---|---|
數據競爭 | 多線程訪問全局變量未加鎖 |
后果 | num 最終值不確定,減法丟失 |
解決 | 使用 pthread_mutex 互斥鎖 |
調試 | 建議加 -fsanitize=thread 或使用 valgrind --tool=helgrind 檢查 |