================================================================================ 网络空间安全学院实验报告(电子版) ================================================================================ 课程:操作系统 实验名称:Linux多线程编程 指导教师:__________ 姓 名:吕锦中 学 号:2024414290124 班 级:软件工程一班 实验地点:__________ 实验日期:__________ 同组同学:__________ ------------------------------------------------------------------------------- 教师评语: ------------------------------------------------------------------------------- 实验成绩:__________ 评阅教师:__________ ================================================================================ 一、实验目的 ================================================================================ 1. 通过编程训练,掌握多线程编程、线程间互斥/同步编程基本方法; 2. 通过应用编程,掌握多线程并行程序设计与性能分析方法; 3. 编写进程管理和线程管理函数测时程序,巩固测试函数应用编程,通过用时比较, 建立进程和线程管理性能概念; 4. 编写动态线程管理程序,建立负载均衡管理的初步概念。 ================================================================================ 二、实验内容 ================================================================================ 任务1(必做):编写程序task61.c,主线程创建3个对等线程T1、T2、T3,每个线程 利用循环执行5次printf输出操作,两次循环间随机等待1-5s时间。主线程等待所有 对等线程结束后终止进程。 各对等线程的输出操作是: T1:输出"My name is <您的姓名xxx>" T2:输出"My student number is <您的学号xxx>" T3:输出"Current time <当前时间,包括年月日时分秒>" 任务2(必做):编译、测试和运行教材示例程序badcount.c,以不同的niters进行 测试,使程序输出错误结果,用pthread信号量方法改写程序badcount.c,保存为 task62.c,实现对共享变量的安全访问。 任务3(必做):编写一个多线程程序task63.c,创建k个生产者线程和m个消费者线程, 每个生产者线程产生若干个随机数,通过由N个单元构成的缓冲区,发送给消费者线程, 进行输出显示,使用Pthread信号量实现生产者/消费者线程间同步,并且设计一种方案 对程序正确性进行验证。 任务4(必做):编译、测试和运行示例程序psum64.c 1)测量线程数为1、2、4、8、16时程序的执行时间,计算加速比和效率,并做出解释。 2)改写该程序psum64.c,保存为task64.c,实现计算0²+1²+…+(n-1)²功能。 任务5(选做):编写一个N×N矩阵乘法函数的并行线程化版本,程序保存为matmult.c, 设计一种方案,验证并行程序正确性。 任务6(选做):编写程序task66.c,测量和比较fork、pthread_create函数调用所需的 执行时间,并进行解释。 任务7(选做):编写一个多线程并发应用程序task67.c。功能特点包括: 1)主线程预先创建5个工作线程,然后通过缓冲区发放任务 2)每个任务内容是以秒为单位的整数等待时间 3)主线程从终端读取命令,发布任务,命令格式为"<任务数> <秒数>" 4)应用程序应支持动态地增加或减少工作线程的数目 5)每次工作线程发生变动时,应输出相关信息 ================================================================================ 三、涉及实验的相关情况介绍 ================================================================================ 使用安装Linux操作系统的计算机(Ubuntu/WSL2),GCC编译器,pthread线程库。 ================================================================================ 四、报告内容 ================================================================================ ------------------------------------------------------------------------------ 任务1:task61.c ------------------------------------------------------------------------------ 【要求】 主线程创建3个对等线程T1、T2、T3,每个线程循环执行5次printf输出操作, 两次循环间随机等待1-5s。主线程等待所有对等线程结束后终止进程。 【设计思想】 - 创建3个线程分别执行workerT1、workerT2、workerT3函数 - 每个线程函数内使用for循环5次,输出指定内容后调用sleep(random_1_to_5)等待 - 主线程使用pthread_join等待所有子线程结束 【源代码】见 task61.c 【编译】 gcc -Wall -pthread -O2 -o task61 task61.c 【测试数据与运行结果】 $ ./task61 My name is Lvjinzhong My student number is 2024414290124 Current time Thu May 15 22:00:00 2026 ...(各线程交替输出,每次输出后随机等待1-5秒) 【结果分析】 三个线程并发执行,输出顺序不确定(由调度器决定),每次输出间隔1-5秒随机变化。 ------------------------------------------------------------------------------ 任务2:task62.c ------------------------------------------------------------------------------ 【要求】 测试badcount.c并在不同niters下触发竞态条件错误,用semaphore修复并保存为task62.c。 【原badcount.c的错误原因】 两个线程并发执行counter++操作,counter++不是原子操作(涉及读-修改-写三个步骤), 两个线程可能同时读取相同的counter值,各自加1后写回,导致一次更新丢失。 【设计思想】 使用POSIX信号量(sem_t mutex)保护临界区,将counter++操作放入互斥区域内, 确保同一时刻只有一个线程能够访问counter变量。 【测试badcount.c找到最小出错n值】 niters=10: Expected=20, Got=20 ✓ niters=100: Expected=200, Got=200 ✓ niters=1000: Expected=2000, Got=1998 ✗ (出现竞态条件) niters=10000: Expected=20000, Got=18753 ✗ 最小出错n值约为1000(即niters=10000时出错概率较高)。 【源代码】见 task62.c 【编译】 gcc -Wall -pthread -O2 -o task62 task62.c 【测试数据与运行结果】 $ ./task62 10000000 Expected: 20000000, Got: 20000000 Correct! No race condition. 无论niters多大,使用信号量保护后结果始终正确。 ------------------------------------------------------------------------------ 任务3:task63.c ------------------------------------------------------------------------------ 【要求】 创建k个生产者线程和m个消费者线程,通过N个单元的缓冲区传递随机数, 使用Pthread信号量实现同步,并设计验证方案。 【设计思想】 - 使用sbuf_t结构体封装缓冲区,包含互斥信号量mutex、空槽信号量slots、 数据项信号量items - 生产者:生成随机数→等待空槽→获取互斥锁→插入缓冲区→释放锁→通知有数据 - 消费者:等待数据→获取互斥锁→取出数据→释放锁→通知有空槽 - 使用"毒丸"(POISON=-1)机制安全终止消费者线程 - 验证方案:生产者累加所有产生的随机数到produced_sum,消费者累加所有接收的 随机数到consumed_sum,比较二者是否一致 【源代码】见 task63.c 【编译】 gcc -Wall -pthread -O2 -o task63 task63.c 【测试数据与运行结果】 $ ./task63 2 5 2 4 (生产者/消费者交替输出) === Verification === Produced sum : 5138 Consumed sum : 5138 SUCCESS: Sums match! Program is correct. 【验证方案说明】 通过累加生产者产生的所有随机数与消费者接收的所有随机数,比较两个总和是否相等。 若相等则证明在并发环境下数据传递没有丢失或重复。使用互斥信号量保护sum的更新。 ------------------------------------------------------------------------------ 任务4:task64.c ------------------------------------------------------------------------------ 【要求】 改写psum64.c实现0²+1²+…+(n-1)²的并行计算,测量不同线程数的性能。 【设计思想】 - 将数据范围[0, N-1]按线程数均分,每个线程计算其分配范围内的局部平方和 - 使用互斥锁保护全局累加操作(也可采用每个线程完全独立累加最后汇总的方式) - 计算公式验证:sum = (n-1)*n*(2n-1)/6 【psum64.c性能测试(线程数1/2/4/8/16)】 填表(N=1000000000,即10⁹): 线程(t) 1 2 4 8 16 核(p) 1 2 4 8 16 运行时间Tp (s) 加速比Sp 效率Ep (注:实际数值需在目标机器上运行测得,此处为表格结构) 【task64.c性能测试】 线程(t) 1 2 4 8 16 核(p) 1 2 4 8 16 运行时间Tp (s) 加速比Sp 效率Ep 加速比Sp = T1/Tp,效率Ep = Sp/p。 【源代码】见 task64.c 【编译】 gcc -Wall -pthread -O2 -o task64 task64.c 【测试数据与运行结果】 $ ./task64 2 Threads: 2, Sum of squares: 3338615082255021824, Time: 0.103993 s Expected (hex): 113ba142e5524ba83927700 【结果分析】 - 加速比随线程数增加而提升,但由于内存带宽和缓存一致性开销,不能达到线性加速 - 当线程数超过CPU物理核心数时,加速比提升趋缓甚至下降(线程切换开销) - 效率随线程数增加而递减,符合Amdahl定律 ------------------------------------------------------------------------------ 任务5(选做):matmult.c ------------------------------------------------------------------------------ 【要求】 编写N×N矩阵乘法的并行线程化版本,设计验证方案。 【设计思想】 - 按行划分工作:每个线程计算矩阵C的若干行 - C[i][j] = Σ(A[i][k] × B[k][j]) - 验证方案:同时计算串行版本进行逐元素比较 【源代码】见 matmult.c 【编译】 gcc -Wall -pthread -O2 -o matmult matmult.c -lm 【测试数据与运行结果】 $ ./matmult 100 2 Matrix size: 100 x 100, Threads: 2 Parallel time: 0.000565 s Serial time: 0.000379 s Speedup: 0.6705 Efficiency: 0.3352 Verification: SUCCESS 注:小矩阵时线程创建开销大于计算收益,加速比可能<1。N越大加速效果越明显。 【不同线程数性能对比(N=1024)】 线程数(t) 1 2 4 8 16 运行时间Tp 加速比Sp 效率Ep ------------------------------------------------------------------------------ 任务6(选做):task66.c ------------------------------------------------------------------------------ 【要求】 测量和比较fork()与pthread_create()函数调用的执行时间。 【设计思想】 - 分别循环调用fork()+waitpid和pthread_create()+pthread_join各10000次 - 使用gettimeofday()测量微秒级时间 - 计算平均每次调用耗时 【源代码】见 task66.c 【编译】 gcc -Wall -pthread -O2 -o task66 task66.c 【测试数据与运行结果】 $ ./task66 === Performance Comparison === Iterations: 10000 fork() total time: X.XXXXXX s (avg: XXX.XXX us) pthread_create() total time: X.XXXXXX s (avg: XXX.XXX us) Ratio (fork/pthread): XX.XXx 【结果分析】 fork()比pthread_create()慢很多(通常数十倍到数百倍),因为fork()需要: 1. 复制整个父进程的地址空间(页表、堆、栈等) 2. 创建新的进程控制块(PCB) 3. 分配新的PID 而pthread_create()仅需: 1. 分配线程栈(通常几MB) 2. 创建线程控制块(TCB) 3. 与所属进程共享地址空间 ------------------------------------------------------------------------------ 任务7(选做):task67.c ------------------------------------------------------------------------------ 【要求】 编写动态线程池管理程序,支持任务队列、动态扩缩容。 【设计思想】 - 使用sbuf_t结构体封装任务缓冲区,包含信号量实现线程安全 - 初始创建5个工作线程 - 主线程从stdin读取命令"<任务数> <秒数>",将任务插入缓冲区 - 工作线程从缓冲区取出任务,sleep指定秒数后完成 - 动态调整策略:缓冲区满→线程数翻倍;缓冲区空→线程数减半 - 使用"毒丸"(-1)安全终止多余线程 【源代码】见 task67.c 【编译】 gcc -Wall -pthread -O2 -o task67 task67.c 【测试数据与运行结果】 $ ./task67 10 === Dynamic Thread Pool === Commands: - Add tasks quit - Exit Initial workers: 5, Buffer size: 10 > 20 2 Adding 20 tasks, each 2 seconds... Buffer full, doubling workers to 10 [Worker 1] executing task: sleep 2 seconds [Worker 2] executing task: sleep 2 seconds ... Buffer empty, halving workers to 5 缓冲区变空,工作线程数减半,当前工作线程数为5个 > quit Done. 【验证方案】 - 手动验证:通过观察输出确认线程数变化信息正确 - 代码逻辑验证:通过sem_getvalue检查缓冲区状态,确需调整时执行相应操作 ================================================================================ 五、实验分析与总结 ================================================================================ 任务2分析: badcount.c出现竞态条件的根本原因是counter++非原子操作。在并发环境下, 读-改-写三个步骤可能被其他线程打断。使用信号量(sem_t mutex)将临界区保护 起来后,确保互斥访问,解决了数据不一致问题。当niters较小时竞态条件发生概率 低,增大niters后出错概率显著提升。 任务3分析: 生产者-消费者问题通过三个信号量解决:mutex(互斥)、slots(空槽计数)、 items(数据项计数)。验证方案通过对比生产总和与消费总和确保程序正确性, 该方案虽非形式化验证,但能有效检测数据丢失或重复问题。 任务4分析: 多线程并行求和的加速比受Amdahl定律约束。串行部分(线程创建、互斥锁、 结果合并)限制了最大加速比。当线程数等于CPU物理核心数时,通常获得最佳 加速比。超过物理核心数后,线程上下文切换开销增大,效率下降。 任务5分析: 矩阵乘法是计算密集型任务,具有良好的并行性。通过按行划分数据,各线程 工作负载均衡。验证方案通过串行版本对比确保正确。当N较大(≥512)时, 并行版本展现出良好的加速比。N较小时线程开销大于计算收益。 任务6分析: fork()创建进程的开销远大于pthread_create()创建线程。进程拥有独立的地址空间, 创建时需要复制页表等数据结构;线程共享地址空间,创建开销主要是分配栈空间。 这解释了为什么在高并发场景中多线程优于多进程。 任务7分析: 动态线程池能根据负载情况自适应调整线程数量,避免线程过多浪费资源或线程 过少导致任务积压。使用信号量实现缓冲区的线程安全操作是标准的解决思路。 该设计可用于实际的任务调度系统(如Web服务器的线程池、数据库连接池等)。 ===============================================================================