当前位置: 首页 > news >正文

【操作系统(Linux)】——通过案例学习父子进程的线程异步性

本篇旨在通过几个案例来学习父子进程的线程异步性

一、父进程与子进程

我们将要做的: 创建父子进程,观察父子进程执行的顺序,了解进程执行的异步行为

源代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> // 定义了 POSIX 操作系统 API(Unix/Linux 下的系统调用函数)
#include <stdlib.h>

int main()
{
    pid_t pid; // 进程id
    char*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. 头文件讲解

  1. #include <sys/types.h>
  • 作用:定义数据类型,如 pid_t
  • pid_t 是一个整型,用于表示进程 ID,确保跨平台一致性。
  • 一般与 fork()wait() 等系统调用一起使用。
  1. #include <unistd.h>
  • 作用:定义了 POSIX 操作系统 API(Unix/Linux 下的系统调用函数)。
  • 提供本程序中用到的:
    • fork():创建子进程
    • sleep():让进程休眠若干秒
    • 还包括 getpid()(获取进程ID)、exec族函数(程序替换)等。
  1. #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. 程序运行逻辑总结

  1. 调用 fork() 创建子进程,得到两个并发执行的进程。
  2. 每个进程根据 fork() 的返回值设定自己的输出内容(msg)和输出次数(k)。
  3. 每个进程都进入 while(k>0) 循环,每秒输出一次 msg,共输出 k 次。
  4. 最终执行 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; // 线程id
	int ret=-1;
	int times=3;
	int i=0;
	int *ret_join=NULL;
	// 创建一个线程,线程函数为threadfunc,传入参数为&run
	ret=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_joinint * 类型,用于接收 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. 建议与优化

  1. usleep(1) 睡眠时间太短(1 微秒),可以改为 usleep(100000)(即 0.1 秒)方便观察输出。
  2. 变量 retvalue 设置为 static 是为了确保其生命周期足够长,返回地址有效。
  3. 多线程程序应考虑线程安全问题,如资源竞争、内存可见性等,在多核机器中尤为重要。

三、多线程对共享变量的非互斥访问

我们将要做的: 构造「多线程共享变量竞争」的现象,并分析现象发生的原因,进而思考解决方式。

源代码:

#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; // 两个子线程的id
    int 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. 程序功能概述

创建了两个线程 sub1sub2,以及主线程三者共同对一个全局变量 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 检查

相关文章:

  • C#核心学习(十五)面向对象--关联知识点(1)命名空间
  • VSCode优雅的使用debug
  • QML面试笔记--UI设计篇04交互控件
  • 虚幻基础:碰撞帧运算
  • 【13】数据结构之树结构篇章
  • AI自动化制作web和手机应用软件的利器:bolt.new
  • Python设计模式:策略模式
  • 主机协议端口安全
  • 网络问题之TCP/UDP协议
  • 【Windows自带的图片查看软件photo】
  • 回文日期2
  • Process Explorer 性能调优实战:精准定位资源泄漏与高负载进程
  • ZYNQ笔记(五):AXI GPIO 中断
  • C++指针(四)万字图文详解!
  • 【特权FPGA】之乘法器
  • P8623 [蓝桥杯 2015 省 B] 移动距离
  • go学习记录(第一天)
  • 一、TorchRec里边的输入输出类型
  • 为什么要将函数变量化?
  • django rest framework相关面试题
  • 老铁推荐个2021网站好吗/兰州seo快速优化报价
  • 你认为网络营销是什么/宁波seo关键词排名
  • 中山网站建设找阿 n 2/友情链接平台赚钱吗
  • 南昌 网站制作/sem竞价推广是什么
  • 网站两边广告/重庆seo顾问
  • 做系统用什么网站/交换友情链接的网站标准是什么