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

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

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

一、父进程与子进程

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

源代码:

#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. 头文件讲解

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

创建了两个线程 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 检查

相关文章:

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

本篇旨在通过几个案例来学习父子进程的线程异步性 一、父进程与子进程 我们将要做的&#xff1a; 创建父子进程&#xff0c;观察父子进程执行的顺序&#xff0c;了解进程执行的异步行为 源代码&#xff1a; #include <stdio.h> #include <sys/types.h> #include…...

Go 语言范围 (Range)

Go 语言范围 (Range) Go 语言是一种静态强类型、编译型、并发型编程语言&#xff0c;由 Google 开发。它的简洁性和高效性使其成为众多开发者的首选。在 Go 语言中&#xff0c;range 是一个非常有用的关键字&#xff0c;用于遍历数组、切片、字符串以及通道&#xff08;channe…...

【开源宝藏】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…...

WSA(Windows 安卓子系统)过检测教程

windows安卓子系统WSA的root和magisk的安装教程 安卓子系统WSLWSA的rootmagisk安装 WSA&#xff08;Windows 安卓子系统&#xff09;过检测的方法与思路 一、引言 Windows 安卓子系统&#xff08;WSA&#xff09;为 Windows 用户提供了在电脑上运行安卓应用的便利。然而&…...

蓝桥杯 B3620 x 进制转 10 进制

题目描述 给一个小整数 x 和一个 x 进制的数 S。将 S 转为 10 进制数。对于超过十进制的数码&#xff0c;用 A&#xff0c;B&#xff0c;… 表示。 输入格式 第一行一个整数 x; 第二行一个字符串 S。 输出格式 输出仅包含一个整数&#xff0c;表示答案。 输入输出样例 …...

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

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

Qt子模块的功能介绍

一、Qt 主要子模块的功能介绍 1. 核心模块 模块名称功能描述QtCore核心非GUI功能(信号槽、线程、文件IO、容器类、JSON/XML处理等)QtGui基础图形绘制(窗口系统集成、OpenGL抽象、图像处理、字体管理等)QtConcurrent高级多线程API(并行计算框架,如QtConcurrent::run)QtN…...

FRP练手:hello,world实现

方案一&#xff1a;使用 Flask&#xff08;推荐&#xff09; from flask import Flaskapp Flask(__name__)app.route(/) def hello_world():return "你好啊世界"if __name__ __main__:# 监听所有网络接口&#xff08;0.0.0.0&#xff09;&#xff0c;端口 3344app.…...

《深入探秘:分布式软总线自发现、自组网技术原理》

在当今数字化浪潮中&#xff0c;分布式系统的发展日新月异&#xff0c;而分布式软总线作为实现设备高效互联的关键技术&#xff0c;其自发现与自组网功能宛如打开智能世界大门的钥匙&#xff0c;为多设备协同工作奠定了坚实基础。 分布式软总线的重要地位 分布式软总线是构建…...

西门子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…...

提升Windows安全的一些措施

由简单到复杂&#xff0c;仅供参考 一、杀毒软件&#xff1a; 1、杀毒能力&#xff1a; https://haokan.hao123.com/v?vid3883775443252827335&pdhaokan_share 2、使用注意&#xff1a; 一台主机只安装一个杀毒软件就可以了 杀毒软件会误报&#xff0c;造成正常文件…...

Jupyter notebook定制字体

一、生成配置文件 运行Anaconda Powershell Prompt终端&#xff0c;输入下面一行代码&#xff1a; jupyter notebook --generate-config 将生成文件“C:\Users\XXX\.jupyter\jupyter_notebook_config.py”&#xff0c;XXX为计算机账户名字。 二、修改配置文件 c.NotebookAp…...

内存分配中的堆(Memory Heap)详解

在计算机科学中&#xff0c;"堆"这个术语确实容易让人混淆&#xff0c;因为它同时用于描述两种完全不同的概念&#xff1a;数据结构中的堆和内存管理中的堆。上次我们讨论了数据结构中的堆&#xff0c;今天我将详细解释内存分配中的堆&#xff08;Memory Heap&#x…...

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

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

JS | 函数柯里化

函数柯里化&#xff08;Currying&#xff09;&#xff1a;将一个接收多个参数函数&#xff0c;转换为一系列只接受一个参数的函数的过程。即 逐个接收参数。 例子&#xff1a; 普通函数&#xff1a; function add(a, b, c) {return a b c; } add(1, 2, 3); // 输出 6柯里化…...

软件工程基础之设计模式

目录 单例模式(Singleton Pattern)工厂方法模式(Factory Method Pattern)抽象工厂模式(Abstract Factory Pattern)原型模式(Prototype Pattern)适配器模式(Adapter Pattern)单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。应用场景:…...

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…...

【Docker项目实战】使用Docker部署ToDoList任务管理工具

【Docker项目实战】使用Docker部署ToDoList任务管理工具 一、ToDoList介绍1.1 ToDoList简介1.2 ToDoList主要特点二、本次实践规划2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、下载ToDoList镜像…...

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

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

Traefik应用:配置容器多个网络时无法访问问题

Traefik应用&#xff1a;配置容器多个网络时无法访问问题 介绍解决方法问题原因&#xff1a; **容器多网络归属导致 Traefik 无法正确发现路由规则**。解决方案方法 1&#xff1a;将应用容器 **仅连接** 到 traefik-public 网络方法 2&#xff1a;显式指定 Traefik 监听的网络 …...

虚幻5的C++调试踩坑

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

react 中将生成二维码保存到相册

需求&#xff1a;生成二维码&#xff0c;能保存到相册 框架用的 react 所以直接 qrcode.react 插件&#xff0c;然后直接用插件生成二维码&#xff0c;这里一定要写 renderAs{‘svg’} 属性&#xff0c;否则会报错&#xff0c;这里为什么会报错&#xff1f;&#xff1f;&#…...

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

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

C语言【模仿strcpy】

题目 模仿strcpy 思路&#xff08;注意事项&#xff09; 注意需要在复制的字符串结尾加\0表示字符串的终止 纯代码 #include<stdio.h>void cpy(const char *a, char *b){int i 0;while (a[i] ! \0){b[i] a[i];i ;}b[i] \0; } int main(){char a[] "HELLO&quo…...