【APUE】补充 — 基于管道的线程池

news2025/1/17 6:07:08

目录

一、引言

二、代码实现 

三、思考 


一、引言

在线程章节的 3.2 部分,我们曾经提到过线程池的实现

在当时的代码中,我们仅仅用的一个 int 类型的变量来表示这个“池”,用来存放任务

显然这个池太小了,如果下游线程很多,可能会出现以下情况:

我们只需要将任务池的容量增大点,就可以很好地减少上述提到的多次调度带来的上下文切换开销

池需要用一个数据结构来维护其池中的任务(数据),我们选用队列进行实现

其实队列这个数据结构的特征和管道非常类似,数据从队列尾入队,从队列首出队数据写入管道的写端,然后从管道的读端读取数据

我们接下来将动手实现一个队列(管道),并基于该管道扩充池的容量。其实内核也提供了现成的管道,我们不直接使用内核提供的管道主要有以下两点考虑:

  • 内核提供的管道可用于进程间通信,线程间通信没必要用内核提供的机制,用的话就有点儿大材小用了
  • 自己实现管道能够深入了解管道的特点,为后续讲解进程间通信做铺垫(到时候会用到内核提供的管道)

二、代码实现 

mypipe.h,暴露接口

#ifndef MYPIPE_H__
#define MYPIPE_H__

#define PIPESIZE 1024
#define MYPIPE_READ 0b00000001UL
#define MYPIPE_WRITE 0b00000010UL

typedef void mypipe_t;

mypipe_t* mypipe_init(void);	// 创建(初始化)一个管道

int mypipe_register(mypipe_t *, int opmap);	// 注册用户身份

int mypipe_unregister(mypipe_t *, int opmap);	// 取消注册用户身份

int mypipe_read(mypipe_t *, int *buf, size_t count);	// 读管道

int mypipe_write(mypipe_t *, const int *buf, size_t count);	// 写管道

int mypipe_destroy(mypipe_t *);	// 销毁管道	

#endif

mypipe.c,实现接口。注意如何将在 mypipe.h 中隐藏的数据结构解除隐藏

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#include "mypipe.h"

struct mypipe_st {	// 并发队列
	int head;	// 指向队头
	int tail;	// 指向队尾
	int data[PIPESIZE];	// 存放数据的地方
	int datasize;	// 队列中有效数据的个数
	pthread_mutex_t mutex;	// 互斥量
	pthread_cond_t cond;	// 条件变量
	int count_rd, count_wr;	// 读者写者计数
				// 从这里看出,我们需要用户指定其读者/写者的身份
};

mypipe_t * mypipe_init(void) {	// 初始化一个管道
	struct mypipe_st *me;
	me = malloc(sizeof(*me));
	if (me == NULL)
		return NULL;
	me->head = 0;
	me->tail = 0;
	me->datasize = 0;
	me->count_rd = 0;	// 读者个数为0
	me->count_wr = 0;	// 写者个数为0
	pthread_mutex_init(&me->mutex, NULL);
	pthread_cond_init(&me->cond, NULL);
	return me;
}

int mypipe_register(mypipe_t *ptr, int opmap) {	// 用户通过该函数指定其身份
	struct mypipe_st * me = ptr;	// 之前在.h文件中隐藏了ptr所表征的数据结构,在这里取消隐藏
	pthread_mutex_lock(&me->mutex);	// 可能多个线程同时注册身份,需要互斥
	if (opmap & MYPIPE_READ)	// 将宏看成位图,注意位图操作
		me->count_rd++;
	if (opmap & MYPIPE_WRITE)
		me->count_wr++;
	pthread_mutex_unlock(&me->mutex);	
	return 0;
}

int mypipe_unregister(mypipe_t *ptr, int opmap) {	// 用户通过该函数取消注册其身份
	struct mypipe_st * me = ptr;
	pthread_mutex_lock(&me->mutex);
	if (opmap & MYPIPE_READ)
		me->count_rd--;
	if (opmap & MYPIPE_WRITE)
		me->count_wr--;
	pthread_cond_broadcast(&me->cond);	// 可能读者或者写者被减为0了,需要通知一下read及write
	pthread_mutex_unlock(&me->mutex);
	return 0;
}

int mypipe_read(mypipe_t * ptr, int *buf, size_t count) {
	struct mypipe_st * me = ptr;
	pthread_mutex_lock(&me->mutex);
	while (me->datasize <= 0 && me->count_wr > 0)	// 当管道中没有数据,但是还有写者,就继续等待
	{
		pthread_cond_wait(&me->cond, &me->mutex);	// 等待管道中有数据
								// 等待写者数量变为0
	}
	if (me->datasize <= 0 && me->count_wr <= 0)
	{
		pthread_mutex_unlock(&me->mutex);	// 没有写者且管道中没有数据,就直接返回读取到的字节数为0
		return 0;
	}

	int i;
	for (i = 0; i < count; ++i) {	// 读取
		if (me->datasize <= 0)
			break;
		*(buf+i) = me->data[me->head];
		me->head = (me->head + 1)%PIPESIZE;	// 读出来了,相当于队列首出队了一个元素
		me->datasize--;
	}
	pthread_cond_broadcast(&me->cond);	// 告诉写者能写了
	pthread_mutex_unlock(&me->mutex);
	return i;
}

int mypipe_write(mypipe_t *ptr, const int *buf, size_t count) {
	struct mypipe_st * me = ptr;
	pthread_mutex_lock(&me->mutex);
	while (me->datasize >= PIPESIZE && me->count_rd > 0)	// 当管道满,但是还有读者,就继续等待 
		pthread_cond_wait(&me->cond, &me->mutex);	// 等待读者读出数据,使管道不满
								// 等待读者数量变为0

	if (me->datasize >= PIPESIZE && me->count_rd <= 0)	// 没有读者后,且管道已经满了,就没必要继续写了,直接返回
	{
		pthread_mutex_unlock(&me->mutex);
		return 0;
	}

	int i;
	for (i = 0; i < count; ++i) {	// 写入
		if (me->datasize == PIPESIZE)
			break;
		me->data[me->tail] = *(buf+i);
		me->tail = (me->tail + 1)%PIPESIZE;
		me->datasize++;
	}
	pthread_cond_broadcast(&me->cond);	// 告诉读者能读了
	pthread_mutex_unlock(&me->mutex);
	return i;
}

int mypipe_destroy(mypipe_t * ptr) {
	struct mypipe_st * me = ptr;
	pthread_mutex_destroy(&me->mutex);
	pthread_cond_destroy(&me->cond);
	free(ptr);
	return 0;
}

main.c,演示用户如何使用接口。main 首先创建 10 个线程(这 10 个线程负责从池中获取数据并判断是否为质数),然后 main 线程自身源源不断往池中写入数据。我们需要用我们上述写的管道来表征这个池,注意如何使用我们的接口:

  1. 创建管道
  2. 往管道写入数据前需要注册写者身份,写入完毕后需要取消注册的身份
  3. 从管道读取数据前需要注册读者身份,读取完毕后需要取消注册的身份
  4. 管道用完后需要销毁管道
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "mypipe.h"

#define NUM_THREADS 10
#define START_NUM 30000000
#define END_NUM 30000200

// 质数判断函数
int is_prime(int number) {
	if (number <= 1) return 0;
	for (int i = 2; i * i <= number; i++) {
		if (number % i == 0) return 0;
	}
	return 1;
}

// 线程函数
void *reader_function(void *arg) {
	mypipe_t *pipe = (mypipe_t *)arg;
	int number;

	mypipe_register(pipe, MYPIPE_READ);

	while (mypipe_read(pipe, &number, 1) > 0) {
		if (is_prime(number)) {
			printf("Prime number: %d\n", number);
		}
	}

	mypipe_unregister(pipe, MYPIPE_READ);
	return NULL;
}

int main() {
	pthread_t threads[NUM_THREADS];
	mypipe_t *pipe = mypipe_init();

	// 创建读线程
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_create(&threads[i], NULL, reader_function, pipe);
	}

	// 主线程写入数据
	mypipe_register(pipe, MYPIPE_WRITE);
	for (int i = START_NUM; i <= END_NUM; i++) {
		mypipe_write(pipe, &i, 1);
	}
	mypipe_unregister(pipe, MYPIPE_WRITE);
	// 等待所有读线程完成
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_join(threads[i], NULL);
	}

	// 销毁管道
	mypipe_destroy(pipe);
	return 0;
}

结果示例如下

我们判断的是从 30000000 到 30000200 之间的质数,结果和线程章节的 3.2 中所实现代码的运行结果一致,说明代码没毛病 

我们从我们自己创建的管道,可以看出管道的如下几个特点

  • 管道通信是单工的。一方作为读者,另一方作为写者。写永远写在尾部,读永远是从首部读
  • 管道必须凑齐读写双方才能正常运行。注意 mypipe_read 和 mypipe_write 中的 while 循环条件:只要缺失读者或者写者,另一方就可能直接返回而不会等待新的数据(哪怕后面可能有新的读者或者写者加入并读写管道)
  • 管道内部自带同步与互斥机制

三、思考 

上述代码的一个缺陷是:没有办法约束用户的行为!

比如我是一个用户,我注册了写者身份后却调用的 write......

那可不妥!!一种好的思路是引入权限的概念,并对上述接口再封装一层......

上述蓝色字体表示封装出来的新的接口,以供用户调用绿色字体介绍了这样封装的思想和逻辑。其实这就是 UNIX “一切皆文件”思想的部分实现方式!即:就算最底层可能是完全不一样的东东(操作管道、操作设备、操作普通文件......),也会再封装一层,并提供通用的接口(如 open、read、write、close 和文件描述符 fd......)。这样一来,在用户的视角里,操作文件的接口也可以操作很多不一样的东东,因此“一切皆文件”

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

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

相关文章

强化学习在文生图中的应用:Training Diffusion Models with Reinforcement Learning

论文链接:Training Diffusion Models with Reinforcement Learning项目地址:Training Diffusion Models with Reinforcement Learning官方代码:https://github.com/kvablack/ddpo-pytorch/tree/maintrl实现:https://huggingface.co/docs/trl/ddpo_trainer🤗关注公众号 fu…

【LeetCode刷题-双指针】--16.最接近的三数之和

16.最接近的三数之和 方法&#xff1a;排序双指针 class Solution {public int threeSumClosest(int[] nums, int target) {Arrays.sort(nums);int ans nums[0] nums[1] nums[2];for(int i 0;i<nums.length;i){int start i1,end nums.length - 1;while(start < en…

复合、委托、继承

1. 单例模式 静态实例对象在getInstance函数中定义&#xff0c;这样只有在调用函数时才会生成对象 2. 复合 1. 类中封装另一个类某些功能&#xff1b; 2. 构造、析构的调用过程 指明了复合中如何调用被包含类的构造函数&#xff0c;可以直接写在初始化列表位置&#xff1b; 3.…

Java的IO流-缓冲流

字节缓冲流 package com.itheima.d2;import java.io.*;public class Test1 {public static void main(String[] args) {try (InputStream is new FileInputStream("IO/src/itheima01.txt");//1、定义一个字节缓冲输入流包装原始的字节输入流InputStream bis new Bu…

企业是否需要单独一套设备管理系统?

在现代企业中&#xff0c;设备管理是一个至关重要的环节。随着科技的不断进步和信息化的发展&#xff0c;企业对设备管理的要求也越来越高。为了提高设备管理的效率和准确性&#xff0c;许多企业开始考虑是否需要单独一套设备管理系统。本文将从设备管理系统的介绍、和其他系统…

融合语言模型中的拓扑上下文和逻辑规则实现知识图谱补全11.18

融合语言模型中的拓扑上下文和逻辑规则实现知识图谱补全 摘要1 引言2 相关工作2.1 事实嵌入法2.2 拓扑嵌入方法2.3 规则融合方法2.4 基于LM的方法 3 准备3.1 知识图谱和拓扑上下文3.2 KG中的逻辑规则4.3 三元组嵌入 5 实验和结果5.1 数据集和评价指标 摘要 知识图补全&#xf…

电子学会C/C++编程等级考试2021年06月(一级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:数的输入和输出 输入一个整数和双精度浮点数,先将浮点数保留2位小数输出,然后输出整数。 时间限制:1000 内存限制:65536输入 一行两个数,分别为整数N(不超过整型范围),双精度浮点数F,以一个空格分开。输出 一行两个数,分…

c语言:模拟实现qsort函数

qsort函数的功能&#xff1a; qsort相较于冒泡排序法&#xff0c;不仅效率更快&#xff0c;而且能够比较不同类型的元素&#xff0c;如&#xff1a;浮点数&#xff0c;结构体等等。这里我们来模拟下qsort是如何实现这一功能的&#xff0c;方便我们对指针数组有一个更深层次的理…

电子画册真的好好用,制作也简单,都快来学学!

同纸质画册相比&#xff0c;电子画册无需受时间、空间、地域等限制&#xff0c;它通过手机、电脑即可发送文件&#xff0c;轻松实现在线浏览&#xff0c;使用起来更方便。 如何制作电子画册&#xff1f;这里同大家分享一下超简单的电子画册制作教程&#xff0c;0基础也能轻松上…

JDBC,Java连接数据库

下载 JDBC https://mvnrepository.com/ 创建项目&#xff0c;然后创建一个目录并将下载好的 jar 包拷贝进去 选择 Add as Library&#xff0c;让这个目录能被项目识别 连接数据库服务器 在 JDBC 里面&#xff0c;使用 DataSource 类来描述数据库的位置 import com.mysql.cj.…

The ultimate UI kit and design system for Figma 组件库下载

Untitled UI 是世界上最大的 Figma UI 套件和设计系统。可以启动任何项目&#xff0c;为您节省数千小时&#xff0c;并祝您升级为专业设计师。 采用 100% 自动布局 5.0、变量、智能变体和 WCAG 可访问性精心制作。 900全局样式、变量&#xff1a;超级智能的全局颜色、排版和效…

基于机器学习的居民消费影响因子分析预测

项目视频讲解: 基于机器学习的居民消费影响因子分析预测_哔哩哔哩_bilibili 主要工作内容: 完整代码: import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import missingno as msno import warnings warnings.filterwarnin…

1.rk3588的yolov5运行:pt_onnx_rknn转换及rknn在rk3588系统python运行

自己有点笨&#xff0c;查资料查了一周才完美的实现了yolov5在rk3588环境下的运行&#xff0c;在这里写具体步骤希望大家少走弯路。具体步骤如下&#xff1a; 一、yolov5的原代码下载及pt文件转换为onnx文件 1.yolov5的原代码下载及环境搭建 在这里一定要下载正确版本的源代码…

vue引入前端工程内的图片

一、public目录下的图片 public目录下的图片引入方式&#xff1a; <!--/images/图片名称&#xff0c;这种属于绝对路径&#xff0c;/指向public目录 --> <img src"/images/image.png"> 二、src目录下的图片 先在vue.config.js进行配置&#xff0c;并指…

Flutter笔记:Matrix4矩阵变换与案例

Flutter笔记 Matrix4矩阵变换及其案例 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/134474764 【简介…

Lec14 File systems 笔记

文件系统中核心的数据结构就是inode和file descriptor 分层的文件系统&#xff1a; 在最底层是磁盘&#xff0c;也就是一些实际保存数据的存储设备&#xff0c;正是这些设备提供了持久化存储。在这之上是buffer cache或者说block cache&#xff0c;这些cache可以避免频繁的读…

使用共享内存进行通信的代码和运行情况分析,共享内存的特点(拷贝次数,访问控制),加入命名管道进行通信的代码和运行情况分析

目录 示例代码 头文件(comm.hpp) log.hpp 基础版 -- 服务端 代码 运行情况 加入客户端 代码 运行情况 两端进行通信 客户端 代码 注意点 服务端 代码 两端运行情况 共享内存特点 拷贝次数少 管道的拷贝次数 共享内存的拷贝次数 没有访问控制 管道 共享…

三十、W5100S/W5500+RP2040树莓派Pico<PPPoE>

文章目录 1 前言2 简介2 .1 什么是PPPoE&#xff1f;2.2 PPPoE的优点2.3 PPPoE数据交互原理2.4 PPPOE应用场景 3 WIZnet以太网芯片4 PPPOE示例概述以及使用4.1 流程图4.2 准备工作核心4.3 连接方式4.4 主要代码概述4.5 结果演示 5 注意事项6 相关链接 1 前言 PPPoE是一种在以太…

【机器学习 | 假设检验】那些经常被忽视但重要无比的假设检验!! 确定不来看看?(附详细案例)

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

代码随想录算法训练营Day 54 || 392.判断子序列、115.不同的子序列

392.判断子序列 力扣题目链接(opens new window) 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;&quo…