【操作系统(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 检查

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

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

相关文章

系统性能核心指标:QPS、TPS、RT、并发量详解

系统性能核心指标&#xff1a;QPS、TPS、RT、并发量详解 1. 引言 在分布式系统、高并发架构设计中&#xff0c;QPS、TPS、RT、并发量 等指标是衡量系统性能的关键。本文深入解析这些术语的定义、计算方法、关联性及优化策略&#xff0c;帮助开发者更好地进行系统性能评估与调…

PortswiggerLab:Exploiting a mass assignment vulnerability

实验目标 To solve the lab, find and exploit a mass assignment vulnerability to buy a Lightweight l33t Leather Jacket. You can log in to your own account using the following credentials: wiener:peter. 官方WP In Burps browser, log in to the application using…

卡尔曼滤波器的工作原理

原文: https://www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/ 1 概述 你可以对某个动态系统有不确定信息的任何地方使用卡尔曼滤波器&#xff0c;并且对系统下一步的状态做出有根据的猜测。即使出现混乱的现实状态&#xff0c;卡尔曼滤波器都会给出一个合理的结果。…

PDFtk

如果下载的pdf文件有秘钥的话&#xff0c;使用下面linux命令去掉秘钥&#xff1a; pdftk 纳税记录.pdf input_pw 261021 output 纳税记录_output.pdf将多个单页pdf合并为一个pdf的linux命令: pdftk 自然人电子税务局1.pdf 自然人电子税务局2.pdf 自然人电子税务局3.pdf 自然人…

Openlayers:海量图形渲染之WebGL渲染

最近由于在工作中涉及到了海量图形渲染的问题&#xff0c;因此我开始研究相关的解决方案。我在网络上寻找相关的解决方案时发现许多的文章都提到利用Openlayers中的WebGLPointsLayer类&#xff0c;可以实现渲染海量的点&#xff0c;之后我又了解到利用WebGLVectorLayer类可以渲…

替换jeecg图标

替换jeecg图标 ant-design-vue-jeecg/src/components/tools/Logo.vue <!-- <img v-else src"~/assets/logo.svg" alt"logo">-->

Codeforces Round 970 (Div. 3)题解

题目地址 https://codeforces.com/contest/2008 锐评 本次D3的前四题还是比较简单的&#xff0c;没啥难度区分&#xff0c;基本上差不多&#xff0c;属于手速题。E的码量比F大一些&#xff0c;实现略显复杂一些。G的数学思维较明显&#xff0c;如果很久没有训练这个知识点&a…

操作系统:线程间同步之事件集

事件集是线程间同步的机制之一&#xff0c;一个事件集可以包含多个事件&#xff0c;利用事件集可以完成一对多、多对多的线程间同步。 目录 一、事件集举例说明 二、事件集工作机制 三、RT-Thread为实例说明 四、事件集的应用场合 一、事件集举例说明 以坐公交车为例&…

基于springboot钻孔数据管理系统的设计与实现(源码+lw+部署文档+讲解),源码可白嫖!

摘要 本钻孔数据管理系统采用B/S架构&#xff0c;数据库是MySQL&#xff0c;网站的搭建与开发采用了先进的Java语言、Hadoop、数据可视化技术进行编写&#xff0c;使用了Spring Boot框架。该系统从两个对象&#xff1a;由管理员和用户来对系统进行设计构建。用户主要功能包括&…

全双工分轨语音数据集:让AI实现无缝对话

清晨&#xff0c;智能音箱根据指令-播放音乐&#xff1b;驾驶途中&#xff0c;车载助手同步处理导航与来电&#xff1b;智能会议工具无缝切换多语种对话……语音交互技术正快速融入生活。然而&#xff0c;用户对于对话体验追求更自然、更流畅&#xff0c;实时理解&#xff0c;动…

Python 网络请求利器:requests 包详解与实战

诸神缄默不语-个人技术博文与视频目录 文章目录 一、前言二、安装方式三、基本使用1. 发起 GET 请求2. 发起 POST 请求 四、requests请求调用常用参数1. URL2. 数据data3. 请求头 headers4. 参数 params5. 超时时间 timeout6. 文件上传 file&#xff1a;上传纯文本文件流7. jso…

linux入门四:Linux 编译器

一、C 语言编译器 GCC&#xff1a;开启编程之旅 1.1 GCC 安装&#xff1a;一站式工具链 GCC&#xff08;GNU Compiler Collection&#xff09;是 Linux 下最常用的 C/C 编译器&#xff0c;支持多种编程语言。安装命令&#xff08;适用于 Debian/Ubuntu 系统&#xff09;&…

建筑兔零基础自学记录69|爬虫Requests-2

Requests库初步尝试 #导入requests库 import requests #requests.get读取百度网页 rrequests.get(http://www.baidu.com) #输出读取网页状态 print(r.status_code) #输出网页源代码 print(r.text) HTTP 状态码是三位数字&#xff0c;用于表示 HTTP 请求的结果。常见的状态码有…

Web测试流程及注意点

在Web工程过程中&#xff0c;基于Web系统的测试、确认和验收是一项重要而富有挑战性的工作。基于Web的系统测试与传统的软件测试不同&#xff0c;它不但需要检查和验证是否按照设计的要求运行&#xff0c;而且还要测试系统在不同用户的浏览器端的显示是否合适。 重要的是&…

基于MATLAB/simulink的信号调制仿真--AM调制

实验内容&#xff1a; 假设y(t)(20.5*2cos&#xff08;2*pi*1000*t&#xff09;)*5cos&#xff08;2*pi*2*1e4*t&#xff09;调幅系统&#xff0c;请将一个频率为1000HZ的余弦波信号&#xff0c;通过进行AM调制&#xff0c;载波信号频率为20kHZ的余弦波&#xff0c;调制度ma0.…

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

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

数据结构与算法-图论-复习1(单源最短路,全源最短路,最小生成树)

1. 单源最短路 单一边权 BFS 原理&#xff1a;由于边权为单一值&#xff0c;可使用广度优先搜索&#xff08;BFS&#xff09;来求解最短路。BFS 会逐层扩展节点&#xff0c;由于边权相同&#xff0c;第一次到达某个节点时的路径长度就是最短路径长度。 用法&#xff1a;适用…

【WRF理论第十七期】单向/双向嵌套机制(含namelist.input详细介绍)

WRF运行的单向/双向嵌套机制 准备工作&#xff1a;WRF运行的基本流程namelist.input的详细设置&time_control 设置&domain 嵌套结构&bdy_control 配置部分 namelist 其他注意事项Registry.EM 运行 ARW 嵌套双向嵌套&#xff08;two-way nesting&#xff09;单向嵌套…

怎么查看苹果手机和ipad的设备信息和ios udid

你知道吗&#xff1f;我们每天使用的iPhone和iPad&#xff0c;其实隐藏着大量详细的硬件与系统信息。除了常见的系统版本和序列号外&#xff0c;甚至连电池序列号、摄像头序列号、销售地区、芯片型号等信息&#xff0c;也都可以轻松查到&#xff01; 如果你是开发者、维修工程…

matlab内置的git软件版本管理功能

1、matlab多人协作开发比普通的嵌入式软件开发困难很多 用过matlab的人都知道&#xff0c;版本管理对于matlab来说真的很费劲&#xff0c;今天介绍的这个工具也不是说它就解决了这个痛点&#xff0c;只是让它变得简单一点。版本管理肯定是不可或缺的&#xff0c;干就完了 2、…