本篇旨在通过几个案例来学习父子进程的线程异步性
一、父进程与子进程
我们将要做的: 创建父子进程,观察父子进程执行的顺序,了解进程执行的异步行为
源代码:
#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 检查 |