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

news2025/4/18 14:26:57

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

一、父进程与子进程

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

源代码:

#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 检查

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2331865.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【开源宝藏】30天学会CSS - DAY12 第十二课 从左向右填充的文字标题动画

用伪元素搞定文字填充动效&#xff1a;一行 JS 不写&#xff0c;效果炸裂 你是否曾经在设计页面标题时&#xff0c;觉得纯文字太寡淡&#xff1f;或者想做一个有动感的文字特效&#xff0c;但又不想引入 JS 甚至 SVG&#xff1f; 在这篇文章中&#xff0c;我们将通过 一段不到…

nginx或tengine服务器,配置HTTPS下使用WebSocket的线上环境实践!

问题描述&#xff1a; HTTPS 下发起WS连接&#xff0c;连接失败&#xff0c;Chrom 浏览器报错。 socket.js:19 Mixed Content: The page at https://app.XXX.com was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ws://172.16.10.80:903…

【Oracle篇】跨字符集迁移:基于数据泵的ZHS16GBK转AL32UTF8全流程迁移

&#x1f4ab;《博主主页》&#xff1a;奈斯DB-CSDN博客 &#x1f525;《擅长领域》&#xff1a;擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(MongoDB)有了解 &#x1f496;如果觉得文章对你有所帮…

西门子S7-1200PLC 工艺指令PID_Temp进行控温

1.硬件需求&#xff1a; 西门子PLC&#xff1a;CPU 1215C DC/DC/DC PLC模块&#xff1a;SM 1231 TC模块 个人电脑&#xff1a;已安装TIA Portal V17软件 加热套&#xff1a;带加热电源线以及K型热电偶插头 固态继电器&#xff1a;恩爵 RT-SSK4A2032-08S-F 其他&#xff1…

vant4+vue3上传一个pdf文件并实现pdf的预览。使用插件pdf.js

注意下载的插件的版本"pdfjs-dist": "^2.2.228", npm i pdfjs-dist2.2.228 然后封装一个pdf的遮罩。因为pdf文件有多页&#xff0c;所以我用了swiper轮播的形式展示。因为用到移动端&#xff0c;手动滑动页面这样比点下一页下一页的方便多了。 直接贴代码…

2025 数字中国创新大赛数字安全赛道数据安全产业积分争夺赛初赛-东部赛区WriteUp

2025 数字中国创新大赛数字安全赛道数据安全产业积分争夺赛初赛-东部赛区WriteUp 数据安全:ez_upload(60分)&#xff1a; 模型安全&#xff1a;数据分析&#xff1a;溯源与取证&#xff1a;1-1&#xff1a;1-2&#xff1a; 数据社工&#xff1a;2-2:2-3:2-4: 数据跨境&#xff…

2025 年网络安全终极指南

我们生活在一个科技已成为日常生活不可分割的一部分的时代。对数字世界的依赖性日益增强的也带来了更大的网络风险。 网络安全并不是IT专家的专属特权&#xff0c;而是所有用户的共同责任。通过简单的行动&#xff0c;我们可以保护我们的数据、隐私和财务&#xff0c;降低成为…

1.6-抓包技术(Burp Suite\Yakit抓包\Web、APP、小程序)

1.6-抓包技术&#xff08;Burp Suite\Yakit抓包\Web、APP、小程序&#xff09; 如果要使用抓包软件&#xff0c;基本上第一步都是要安装证书的。原因如下&#xff1a; 客户端&#xff08;浏览器或应用&#xff09;会检测到证书不受信任&#xff0c;并弹出 证书错误&#xff0…

图解力扣回溯及剪枝问题的模板应用

文章目录 选哪个的问题17. 电话号码的字母组合题目描述解题代码图解复杂度 选不选的问题78. 子集题目描述解题代码图解复杂度 两相转化77. 组合题目描述解题代码法一&#xff1a;按选哪个的思路法二&#xff1a;按选不选的思路 图解选哪个&#xff1a;选不选 复杂度 选哪个的问…

Elasticsearch 8.X 如何利用嵌入向量提升搜索能力?

众所周知&#xff0c;Elasticsearch 是一个非常流行的搜索引擎&#xff0c;因为它速度快、扩展性强&#xff0c;尤其擅长全文搜索。 近两年&#xff0c;向量嵌入&#xff08;Vector Embedding&#xff09;技术的引入&#xff0c;让 Elasticsearch 在处理高级搜索场景时变得更强…

MySQL体系架构(一)

1.1.MySQL的分支与变种 MySQL变种有好几个,主要有三个久经考验的主流变种:Percona Server,MariaDB和 Drizzle。它们都有活跃的用户社区和一些商业支持,均由独立的服务供应商支持。同时还有几个优秀的开源关系数据库,值得我们了解一下。 1.1.1.Drizzle Drizzle是真正的M…

深度强化学习基础 0:通用学习方法

过去自己学习深度强化学习的痛点&#xff1a; 只能看到各种术语、数学公式勉强看懂&#xff0c;没有建立清晰且准确关联 多变量交互关系浮于表面&#xff0c;有时候连环境、代理控制的变量都混淆 模型种类繁多&#xff0c;概念繁杂难整合、对比或复用&#xff0c;无框架分析所…

虚幻5的C++调试踩坑

本地调试VS附加调试 踩坑1 预编译版本的UE5没有符号文件&#xff0c;无法调试源码 官方代码调试所需要的符号文件bdp需要下载导入。我安装的5.5.4是预编译版本&#xff0c;并非ue5源码。所以不含bdp文件。需要调试官方代码则需要通过EPIC中下载安装。右键UE版本&#xff0c;打…

通信协议详解(十):PSI5 —— 汽车安全传感器的“抗干扰狙击手”

一、PSI5是什么&#xff1f; 一句话秒懂 PSI5就像传感器界的“防弹信使”&#xff1a;在汽车安全系统&#xff08;如气囊&#xff09;中&#xff0c;用两根线同时完成供电数据传输&#xff0c;即便车祸时线路受损&#xff0c;仍能确保关键信号准确送达&#xff01; 基础概念…

从零开始学Python游戏编程18-函数3

《从零开始学Python游戏编程17-函数2》中&#xff0c;通过代码重构的方式将游戏的主要代码写入到自定义函数runGame()中。对于runGame()中的代码&#xff0c;可以继续对其进行重构&#xff0c;以达到简化代码结构的目的。 1 自定义函数askPlayer() 1.1 函数作用 自定义函数a…

Spring事务传播机制

Spring 事务传播机制定义了在多个事务方法相互调用时&#xff0c;事务如何在这些方法间传播。它决定了一个事务方法调用另一个事务方法时&#xff0c;新的事务是如何开启、是否要加入已有的事务等情况。Spring 提供了 7 种事务传播行为&#xff0c;下面是详细介绍。 解释说明&…

Java常用工具算法-5--哈希算法、加密算法、签名算法关系梳理

1、哈希算法 数学本质&#xff1a;将任意长度输入&#xff08;明文&#xff09;映射为固定长度输出&#xff08;哈希值&#xff0c;也称摘要&#xff09;。主要特点&#xff1a; 不可逆性&#xff1a;无法通过哈希值反推原始输入。雪崩效应&#xff1a;输入的微小变化导致哈希…

python之安装PaddlePaddle和PaddleX解析pdf表格

目录标题 飞桨PaddlePaddle本地安装教程1-1. 基于 Docker 安装飞桨1-2. 基于 pip 安装飞桨2. 我两个环境 都选择的是pip 安装10. 如果报错10. 离线安装 飞桨PaddlePaddle本地安装教程 源码下载&#xff1a;https://github.com/PaddlePaddle/PaddleX/blob/release/3.0-beta1/do…

【11408学习记录】英语语法核心突破:揭秘表语从句结构与通知写作实战技巧

表语从句与通知写作 英语语法总结——主从复合句表语从句语句结构系动词表语从句的位置 写作通知写作第二段第三段落款 每日一句词汇第一步&#xff1a;找谓语第二步&#xff1a;断句第三步&#xff1a;简化第一句第二句第三句第四句第五句 英语 语法总结——主从复合句 表语…

封装红黑树实现map和set

前言&#xff1a; 之前我们学习了set与map容器的如何使用&#xff0c;红黑树的实现。接下来我们来看看如何通过封装红黑树&#xff0c;实现我们自己的set与map 相关文章&#xff1a;oi&#xff01;让我来给你唠唠咋实现红黑树☝️-CSDN博客 超详细介绍map&…