├── .DS_Store ├── Client └── client.cc ├── LICENSE ├── README-CN.md ├── README.md ├── Server └── server.cc ├── Utils ├── Log.hpp ├── comm.hpp ├── lockGuard.hpp ├── thread.hpp └── threadControl.hpp ├── assets ├── .DS_Store ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── structure-english.png └── structure.png ├── docs ├── diff_lambda_test-CN.md ├── diff_lambda_test.md ├── reuse-the-threadpool-CN.md └── reuse-the-threadpool.md └── makefile /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/.DS_Store -------------------------------------------------------------------------------- /Client/client.cc: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : client.cc 4 | // Author : Fengcheng Yu 5 | // Date : 2024.4.10 6 | // Description : 本项目中client进程client.cc编写 7 | // client.cc中需要提供tc对象运行时的worker方法和connector方法 8 | //============================================================================ 9 | 10 | #include "../Utils/comm.hpp" 11 | #include "../Utils/threadControl.hpp" 12 | 13 | void WriteToIpc(int fd, std::string msg) { 14 | // 2. ipc过程 15 | // 把数据写到管道中去 16 | std::string buffer = msg; 17 | write(fd, buffer.c_str(), buffer.size()); 18 | } 19 | 20 | void* worker(void* args) { 21 | __thread_data* td = (__thread_data*)args; 22 | thread_control* tc = (thread_control*)td->__args; 23 | // 在这里构造Task 24 | std::random_device rd; 25 | std::mt19937 gen(rd()); 26 | std::exponential_distribution<> dist(tc->get_lambda()); // 这里用命令行传递过来的参数 27 | while (true) { 28 | double interval = dist(gen); // 生成符合负指数分布的随机数 29 | unsigned int sleepTime = static_cast(std::floor(interval)); // 负指数 30 | sleep(sleepTime); 31 | // sleep(1); 32 | std::string msg = "hi I'm client, from thread: " + td->__name + "\n\tmy pid: " + std::to_string(getpid()) + "\n"; 33 | std::cout << "client generate a mesg: " << msg << std::endl; 34 | lockGuard lockguard(tc->get_mutex()); 35 | while (tc->is_full()) 36 | tc->wait_full_cond(); 37 | tc->get_queue().push(msg); 38 | pthread_cond_signal(tc->get_empty_cond()); // 唤醒connection线程,让他去取数据放到pipe里面 39 | } 40 | return nullptr; 41 | } 42 | 43 | void* connector(void* args) // 线程去调用,参数一定是void* args,如果不加statics,就多了个this 44 | { 45 | // routine 本质就是消费过程 46 | __thread_data* td = (__thread_data*)args; 47 | thread_control* tc = (thread_control*)td->__args; 48 | // 通过这种方式,就可以让static方法调用类内的非staic属性了! 49 | size_t msg_number = 0; 50 | while (true) { 51 | // 1. lock 52 | // 2. while看看条件变量是否符合(队列中是否有任务)如果不符合,阻塞! 53 | // 3. 拿到任务 54 | // 4. unlock 55 | // 5. 处理任务: 如果是生产者->任务就是把东西放到20个slot的缓冲区里面 56 | // 如果是消费者->任务就是把东西从20个slot把东西拿出来并执行 57 | std::string msg; 58 | { 59 | // 在这个代码块中,线程是安全的 60 | // 这里用个代码块,lockGuard直接就帮我们解锁了 61 | lockGuard lockguard(tc->get_mutex()); 62 | while (tc->is_empty()) { 63 | tc->wait_empty_cond(); 64 | } 65 | // 读取任务 66 | msg = tc->get_task(); // 任务队列是共享的 67 | pthread_cond_signal(tc->get_full_cond()); 68 | // 解锁了 69 | } 70 | WriteToIpc(tc->get_fd(), msg); // 把东西写到ipc中 71 | msg_number++; 72 | if (msg_number == MESG_NUMBER) { 73 | logMessage(NORMAL, "client quit\n"); 74 | tc->__quit_signal = true; 75 | } 76 | } 77 | return nullptr; 78 | } 79 | 80 | void Usage() { 81 | std::cerr << "Usage: " << std::endl 82 | << "\t./client lambda" << std::endl; 83 | } 84 | 85 | int main(int argc, char** argv) { 86 | // 0. 计算运行时间 87 | auto start = std::chrono::high_resolution_clock::now(); 88 | if (argc != 2) { 89 | Usage(); 90 | exit(1); 91 | } 92 | // 0. 提取命令行参数 93 | double lambda_arg = std::stod(argv[1]); 94 | // 1. 获取管道文件 95 | int fd = open(ipcPath.c_str(), O_WRONLY); // 按照写的方式打开 96 | assert(fd >= 0); 97 | // 2. 构造并运行tc对象 98 | thread_control* tc = new thread_control(worker, connector, 3, fd, lambda_arg); 99 | tc->run(); 100 | // 3. 关闭管道文件 101 | close(fd); 102 | // 4. 计算运行时间 103 | auto end = std::chrono::high_resolution_clock::now(); 104 | std::chrono::duration elapsed = end - start; 105 | sleep((unsigned int)WAIT_IO_DONE); // 统一睡眠相同时间,等待所有io执行完,再执行下面的io,避免打印的时候重叠了 106 | std::cout << "\t[client run time: ]" << elapsed.count() << "(s)" << std::endl; 107 | return 0; 108 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Fengcheng Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # 基于双线程池的管道通信系统框架 2 | 3 | - [简体中文](./README-CN.md) 4 | - [English](./README.md) 5 | 6 | 7 | *** 8 | 9 | - [基于双线程池的管道通信系统框架](#基于双线程池的管道通信系统框架) 10 | - [复用本项目的线程池方法](#复用本项目的线程池方法) 11 | - [项目基本信息](#项目基本信息) 12 | - [项目可以优化的方向](#项目可以优化的方向) 13 | - [系统整体架构](#系统整体架构) 14 | - [项目实现效果](#项目实现效果) 15 | - [项目运行方式](#项目运行方式) 16 | - [不同lambda参数组合实验及其分析](#不同lambda参数组合实验及其分析) 17 | - [threadControl对象详细设计](#threadcontrol对象详细设计) 18 | - [Q: 为什么threadControl对象在这个版本上,只能用std::string作为数据?其他类型为什么暂时还不行?](#q-为什么threadcontrol对象在这个版本上只能用stdstring作为数据其他类型为什么暂时还不行) 19 | - [整体设计](#整体设计) 20 | - [threadControl的成员变量](#threadcontrol的成员变量) 21 | - [threadControl的成员函数](#threadcontrol的成员函数) 22 | - [run()方法](#run方法) 23 | - [get\_task()方法](#get_task方法) 24 | - [其他方法](#其他方法) 25 | - [server/client的worker/connector](#serverclient的workerconnector) 26 | - [server](#server) 27 | - [client](#client) 28 | - [lockGuard.hpp RAII风格的锁封装](#lockguardhpp-raii风格的锁封装) 29 | - [Log.hpp 封装](#loghpp-封装) 30 | - [comm.hpp](#commhpp) 31 | 32 | ## 复用本项目的线程池方法 33 | 34 | 本项目的线程池组建(threadControl.hpp)对象具有高维护性和高代码可读性,除了在这项目中能用之外,很其他项目,这个组建都可以做为线程池使用。 35 | 36 | 大家可以直接查看调用方法的文档。 37 | 38 | - **[reuse-the-threadpool.md](./docs/reuse-the-threadpool-CN.md)** 39 | 40 | ## 项目基本信息 41 | 42 | 本项目从零开始实现了一个基于双线程池的管道通信系统框架,本质上是进程之间的通信。 43 | 44 | **具体实现如下:** 45 | 46 | - 客户端和服务端的通信,客户端随机产生数据,三个生产者 线程(数量可以通过参数设定),一个管道线程。生产者线程生产数据后,push 到客户端的缓冲区 中,等待管道线程获取,并推送到管道中。客户端数据通过管道发送给服务端进程,服务端也有三个(数量可以通过参数设定)消费线程,一个管道线程,管道线程接受管道中的数据后,push 到服务端维护的缓冲区中,分发给消费线程进行消费。 47 | - 生产和消费按照负指数规律进行,参数 lambda 通过两个进程的命令行传参指定。 48 | - 通过 RAII 风格,手动封装互斥锁,并手动封装 lockGuard 对象,方便加锁和解锁,提高代码可读性,提高解耦性。见`./Utils/lockGuard.hpp`。 49 | - 生产者产生的数据需要带有进程标签和线程标签,因此手动封装原生线程`pthread_t`为`thread`对象,其中可以包括包含线程信息,线程名字等其他信息,方便线程的管理和提高代码的 可读性。见`./Utils/thread.hpp`。 50 | - 封装日志功能`./Utils/Log.hpp`,区分日志的等级。方便debug和调试。 51 | - 封装该项目的核心对象`class thread_control`。本质上是线程池。客户端和服务端均可复用这个对象 的代码,管理所需要的线程,和线程所对应需要做的函数回调。与此同时,可以实现在一份代码中,管理所有线程(客户端或服务端)所有的互斥锁和互斥同步变量。**这个对象我认为是本次项目的核心 所在,它可以避免在客户端进程和服务端进程中,分别编写控制线程的逻辑,使得线程控制的逻辑从 客户端和服务端中解耦出来,大大减少代码的冗余,大大提高了代码的二次开发潜力。具体核心实现 可以见图 1 和文章第一大章节的描述。见`./Utils/threadControl.hpp`。** 52 | 53 | ## 项目可以优化的方向 54 | 55 | 我欢迎各位对我本项目进行持续的优化,如果要对本项目进行优化,大家可以联系我的邮箱(我的主页上有)之后,fork本仓库进行优化,如果我认为这个优化是ok的,我会进行merge操作的! 56 | 57 | - connector线程目前只有一个,可以进行优化,connector线程通过参数进行设定。 58 | - 可以优化使得connector线程结合多路转接技术,具体可以见我另一个仓库,[Multiplexing-high-performance-IO-server](https://github.com/Yufccode/Multiplexing-high-performance-IO-server),同时监听多个文件描述符,这个文件描述符不止来自于管道,还可以来自于网络,来自于硬件等等,因此本项目是可以持续优化的! 59 | - 优化线程池支持的数据类型,目前只支持string类型,因为目前来说,我还没有编写序列化,反序列化的代码,因此只支持string/char*类型。如果本项目要扩展到网络服务器上,或者其他地方,都需要编写反序列化和序列化的代码,维护一个Task类,Task类可以序列化反序列化。同时Task类可以重载operator()方法,表示Task任务的执行和调用。 60 | 61 | ## 系统整体架构 62 | 63 | ![](./assets/structure.png) 64 | 65 | ## 项目实现效果 66 | 67 | ### 项目运行方式 68 | 69 | 克隆仓库。 70 | 71 | ```bash 72 | git clone https://github.com/Yufccode/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework.git 73 | ``` 74 | 75 | 进入目录。 76 | 77 | ```bash 78 | cd Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework 79 | ``` 80 | 81 | 编译。 82 | 83 | 执行`make`或者`make all`,均可以同时生成`./Client/client`可执行文件和`./Server/server`可执行文件。 执行`make client`或者`make server`会分别生成 `client` 和 `server` 的可执行文件。 84 | 执行 `make clean` 会清理所有可执行文件。 85 | 86 | ```bash 87 | make clean; make 88 | ``` 89 | 90 | 编译好之后,开启两个终端,分别进入 Server 目录和 Client 目录。运行的时候要指定负指数生产/消 费的参数,否则程序也是不能正常运行的。如图 3 所示。与此同时,因为管道是 server 创建的,所以要先 运行 server 进程,才能运行 client 进程。 91 | 92 | ![](./assets/3.png) 93 | 94 | 运行server或client需要传递lambda参数。 95 | 96 | ### 不同lambda参数组合实验及其分析 97 | 98 | - **[diff_lambda_test-CN.md](./docs/diff_lambda_test-CN.md)** 99 | 100 | 101 | ## threadControl对象详细设计 102 | 103 | ### Q: 为什么threadControl对象在这个版本上,只能用std::string作为数据?其他类型为什么暂时还不行? 104 | 105 | 管道传输本质就是字符的传输,在这个版本的实现中,只用std::string对象可以很方便的转化成字符串。 106 | 107 | 想要把这个项目扩展到多种数据类型,需要对数据进行一层封装,数据本质是任务,可以封装成`Task`类,这个`Task`类需要重载`operator()`方法,表示任务的处理。 108 | 109 | 最重要的,需要添加序列化和反序列化的方法,将结构化的Task类转化成字符串类型,可以手动编写,也可以使用现在比较常用的方法:json或者protobuf。 110 | 111 | **这个也是本项目可以继续优化的地方。** 112 | 113 | ### 整体设计 114 | 115 | 如图所示。 116 | 117 | ![](./assets/structure.png) 118 | 119 | ### threadControl的成员变量 120 | 121 | 本质是一个线程池,成员函数如下所示。**核心:一把锁+两把同步变量。** 122 | 123 | ```cpp 124 | // 线程池本质就是一个生产者消费者模型 125 | template 126 | class thread_control 127 | { 128 | private: 129 | std::vector __worker_threads; // worker线程 130 | int __thread_num; // worker线程数量 131 | std::queue __cache; // 任务队列->也就是20个slot的缓冲区 132 | size_t __cache_max_size = CACHE_MAX_SIZE_DEFAULT; // 缓冲区最大的大小 133 | pthread_mutex_t __cache_lock; // 多个线程访问缓冲区的锁 134 | pthread_cond_t __cache_empty_cond; // 判断缓冲区是否为空 135 | pthread_cond_t __cache_full_cond; // 判断缓冲区是否满了 136 | thread *__connector_thread; // connector线程,只有一个 137 | int __fd; // 管道文件的fd 138 | double __lambda; // 负指数参数,消费频率/生产频率 139 | public: 140 | bool __quit_signal = false; // 控制进程结束 141 | // member functions 142 | } 143 | ``` 144 | 145 | - `std::vector __worker_threads;` 表示一系列的worker线程,如果是生产者对它进行的调用,那么这个几个线程就负责生产数据,然后丢到生产者维护的缓存中(`std::queue __cache;`)。 146 | - `int __thread_num;`表示worker线程的数量,这个参数由构造函数传递进来进行初始化。 147 | - `size_t __cache_max_size = CACHE_MAX_SIZE_DEFAULT;` 缓冲区的最大的大小,`CACHE_MAX_SIZE_DEFAULT`在comm.hpp中定义。 148 | - 一把锁+两把同步变量。每个tc对象维护一把锁,用来保护缓冲区的多线程访问,因为worker线程有多个,而且connector线程也会去访问缓冲区,所以缓冲区是临界区,需要用锁保护。同步变量:对于生产者维护的tc对象缓冲区满的时候,worker线程就不能继续向缓冲区中加入数据了,因此需要线程同步,与此同时,如果缓冲区为空,connector线程就不能再取数据了。消费者也是同理,反过来而已。 149 | - `int __fd;` 管道文件的文件描述符,这个参数由生产者/消费者的主函数通过构造函数传进来。对于生产者来说,这个fd是管道的写端,对于消费者来说,这个fd是管道的读端。 150 | - `double __lambda;` 生产者/消费者按照负指数生产/消费的参数lambda,通过命令行传参->构造函数传递。 151 | 152 | ### threadControl的成员函数 153 | 154 | 构造和析构这里就不展开了,可以直接看代码。主要就是线程,锁和同步变量的初始化和销毁。 155 | 156 | #### run()方法 157 | 158 | ```cpp 159 | void run() 160 | { 161 | __connector_thread->start(); 162 | logMessage(NORMAL, "%s %s", __connector_thread->name().c_str(), "start\n"); 163 | for (auto &iter : __worker_threads) 164 | { 165 | iter->start(); 166 | logMessage(NORMAL, "%s %s", iter->name().c_str(), "start\n"); 167 | } 168 | // 现在所有的线程都启动好了,要控制退出的逻辑 169 | while (1) 170 | // 现在各个线程都在运行 171 | if (__quit_signal) // 监听退出信号 172 | break; 173 | } 174 | ``` 175 | 176 | 这个方法首先就是运行所有的线程,当然,线程我已经封装成了`thread`类型,调用`thread::start()`本质就是调用`pthread_create()`。 177 | 线程启动之后,需要监听退出信号,让run()方法持续运行,等待某个线程,发送了`__quit_signal`信号之后,才能停止运行。 178 | 179 | #### get_task()方法 180 | 181 | ```cpp 182 | T get_task() 183 | { 184 | T t = __cache.front(); 185 | __cache.pop(); 186 | return t; // 拷贝返回 187 | } 188 | ``` 189 | 190 | 其实就是队列操作,把任务(数据)从队列中取出来。 191 | 192 | #### 其他方法 193 | 194 | ```cpp 195 | pthread_mutex_t *get_mutex() { return &__cache_lock; } 196 | pthread_cond_t *get_empty_cond() { return &__cache_empty_cond; } 197 | pthread_cond_t *get_full_cond() { return &__cache_full_cond; } 198 | std::queue &get_queue() { return this->__cache; } 199 | void wait_empty_cond() { pthread_cond_wait(&__cache_empty_cond, &__cache_lock); } 200 | void wait_full_cond() { pthread_cond_wait(&__cache_full_cond, &__cache_lock); } 201 | bool is_empty() { return __cache.empty(); } 202 | bool is_full() { return __cache.size() >= __cache_max_size; } 203 | int get_fd() { return this->__fd; } 204 | double get_lambda() { return this->__lambda; } 205 | ``` 206 | 207 | 由于有一些成员函数是私有的,所以在编写线程要调用的worker方法/connector方法的时候,无法访问私有成员变量,因此在这里提供一些列外部访问的接口。 208 | 209 | ## server/client的worker/connector 210 | 211 | ### server 212 | 213 | **server的worker的运行流程** 214 | 215 | ```cpp 216 | void *worker(void *args) 217 | ``` 218 | 219 | 通过args参数先获得调用该worker函数的tc对象. 220 | 221 | ```cpp 222 | __thread_data *td = (__thread_data *)args; 223 | thread_control *tc = (thread_control *)td->__args; 224 | ``` 225 | 226 | 构造负指数分布的数据生成器。 227 | 228 | ``` 229 | std::random_device rd; 230 | std::mt19937 gen(rd()); 231 | std::exponential_distribution<> dist(tc->get_lambda()); 232 | ``` 233 | 234 | 然后加锁,接下来就是判断同步变量是否符合条件,即缓冲区是否为空,如果缓冲区还没有数据,阻塞该线程,等待server::connector的唤醒。 235 | 拿到数据之后(在队列中取出来)打印该数据,表示任务的处理。 236 | 237 | 如果收到了"quit",表示收到退出信号,此时应该更改`tc->__quit_signal`为true,这个quit是从connector中产生的,此时,`tc->run()`方法里面正在监听退出信号,收到退出信号之后,会组织所有线程结束。 238 | 239 | **server的connector的运行流程** 240 | 241 | server::connector就是从管道中获取数据,然后放到server的缓存中,等待server::worker来进行处理。 242 | 243 | 流程很简单,同样需要加锁和加同步变量的控制,具体实现可以见代码,就是把东西push到cache里面即可。 244 | 245 | ### client 246 | 247 | client方面和server是相反的,代码基本相同,这里不重复解释了。 248 | 249 | ## lockGuard.hpp RAII风格的锁封装 250 | 251 | RAII 风格的锁封装基本是 C++/C 系统编程的常规做法,基本上可以算是标准实践了。有以下好处: 252 | 253 | - 只用非递归的mutex(即不可重入的 mutex)。 254 | - 不手工调用 lock() 和 unlock() 函数,一切交给栈上的 Guard 对象的构造和析构函数负责。Guard 对象的生命期正好等于临界区。 255 | - 在每次构造 Guard 对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导 致死锁(deadlock)。 256 | - 不使用跨进程的 mutex,进程间通信只用 TCP sockets。 257 | - 加锁、解锁在同一个线程,线程 a 不能去 unlock 线程 b 已经锁住的 mutex(RAII 自动保证)。 ·别忘了解锁(RAII 自动保证)。 258 | - 不重复解锁(RAII 自动保证)。 259 | 260 | 261 | ## Log.hpp 封装 262 | 263 | 定义四种日志等级: 264 | 265 | ```cpp 266 | #define DEBUG 0 267 | #define NORMAL 1 268 | #define WARNING 2 269 | #define ERROR 3 270 | #define FATAL 4 271 | ``` 272 | 273 | 定义函数 274 | 275 | ```cpp 276 | void logMessage(int level, const char *format, ...) {} 277 | ``` 278 | 279 | 允许不定参数传参调用。如下例子所示。 280 | 281 | ```cpp 282 | logMessage(FATAL, "worker function is nullptr or connector fucntion is nullptr, pid: %d", getpid()); 283 | ``` 284 | 285 | 在`#define LOGFILE ""`中可以进行设定,如果`LOGFILE`为`""`,则日志打印到stdout上,如果是一个合法路径,则打印到对应的文件中。 286 | 287 | ## comm.hpp 288 | 289 | 定义了一些共同的头文件,和一些需要用的全局宏参数,具体可以见代码。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework 2 | 3 | - [简体中文](./README-CN.md) 4 | - [English](./README.md) 5 | 6 | *** 7 | 8 | - [Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework](#dual-thread-pool-based-pipeline-communication-system-framework) 9 | - [Reuse the thread pool method of this project](#reuse-the-thread-pool-method-of-this-project) 10 | - [Project basic information](#project-basic-information) 11 | - [Directions in which the project can be optimized](#directions-in-which-the-project-can-be-optimized) 12 | - [Overall system architecture](#overall-system-architecture) 13 | - [Project implementation effect](#project-implementation-effect) 14 | - [Run this project](#run-this-project) 15 | - [Experiments and analysis of different lambda parameter combinations](#experiments-and-analysis-of-different-lambda-parameter-combinations) 16 | - [Detailed design of threadControl object](#detailed-design-of-threadcontrol-object) 17 | - [Q: Why can threadControl objects only use std::string as data in this version? Why are other types not available yet?](#q-why-can-threadcontrol-objects-only-use-stdstring-as-data-in-this-version-why-are-other-types-not-available-yet) 18 | - [overall design](#overall-design) 19 | - [Member variables of threadControl](#member-variables-of-threadcontrol) 20 | - [Member functions of threadControl](#member-functions-of-threadcontrol) 21 | - [run()](#run) 22 | - [get\_task()](#get_task) 23 | - [other member functions](#other-member-functions) 24 | - [server's and client's worker and connector](#servers-and-clients-worker-and-connector) 25 | - [server](#server) 26 | - [client](#client) 27 | - [lockGuard.hpp RAII style lock wrapper](#lockguardhpp-raii-style-lock-wrapper) 28 | - [Log.hpp package](#loghpp-package) 29 | - [comm.hpp](#commhpp) 30 | 31 | 32 | ## Reuse the thread pool method of this project 33 | 34 | The thread pool component (threadControl.hpp) object of this project has high maintainability and high code readability. In addition to being used in this project, this component can be used as a thread pool in many other projects. 35 | 36 | You can directly view the documentation of the calling method. 37 | 38 | - **[reuse-the-threadpool.md](./docs/reuse-the-threadpool.md)** 39 | 40 | ## Project basic information 41 | 42 | This project implements a pipeline communication system framework based on dual thread pools from scratch, which is essentially communication between processes. 43 | 44 | **The specific implementation is as follows:** 45 | 46 | - For communication between the client and the server, the client randomly generates data, three producer threads (the number can be set through parameters), and one pipeline thread. After the producer thread produces the data, it pushes it to the client's buffer, waits for the pipeline thread to obtain it, and pushes it to the pipeline. The client data is sent to the server process through the pipeline. The server also has three (the number can be set by parameters) consumer threads, one pipeline thread. After the pipeline thread receives the data in the pipeline, it pushes it to the buffer maintained by the server. Distributed to consumer threads for consumption. 47 | - Production and consumption proceed according to the law of negative exponential, and the parameter lambda is specified through the command line parameters of the two processes. 48 | - Manually encapsulate mutex locks and lockGuard objects through RAII style to facilitate locking and unlocking, improve code readability, and improve decoupling. See `./Utils/lockGuard.hpp`. 49 | - The data generated by the producer needs to have process labels and thread labels, so the native thread `pthread_t` is manually encapsulated into a `thread` object, which can include thread information, thread name and other information to facilitate thread management and improve code readability. See `./Utils/thread.hpp`. 50 | - Encapsulate the log function `./Utils/Log.hpp` to distinguish the log levels. Convenient for debugging and debugging. 51 | - Encapsulate the core object of this project `class thread_control`. Essentially a thread pool. Both the client and the server can reuse the code of this object to manage the required threads and the function callbacks corresponding to the threads. At the same time, it can be implemented in one code to manage all mutex locks and mutex synchronization variables of all threads (client or server). **I think this object is the core of this project. It can avoid writing thread control logic separately in the client process and server process, so that the thread control logic is decoupled from the client and server. It greatly reduces the redundancy of the code and greatly improves the secondary development potential of the code. The specific core implementation can be seen in Figure 1 and the description in the first chapter of the article. See `./Utils/threadControl.hpp`. ** 52 | 53 | 54 | ## Directions in which the project can be optimized 55 | 56 | I welcome everyone to continue optimizing my project. If you want to optimize this project, you can contact my email address (available on my homepage) and then fork this warehouse for optimization. If I think this optimization is OK, I The merge operation will be performed! 57 | 58 | - There is currently only one connector thread, which can be optimized. The connector thread is set through parameters. 59 | - The connector thread can be optimized to combine with multiplexing technology. For details, please see my other warehouse, [Multiplexing-high-performance-IO-server](https://github.com/Yufccode/Multiplexing-high-performance-IO -server), monitor multiple file descriptors at the same time. This file descriptor not only comes from pipes, but also from the network, from hardware, etc., so this project can be continuously optimized! 60 | - Optimize the data types supported by the thread pool. Currently, only string types are supported. Because at present, I have not written serialization and deserialization code, so only string/char* types are supported. If this project is to be expanded to a network server or other places, it is necessary to write deserialization and serialization code and maintain a Task class, which can be serialized and deserialized. At the same time, the Task class can overload the operator() method to represent the execution and invocation of the Task task. 61 | 62 | ## Overall system architecture 63 | 64 | ![](./assets/structure-english.png) 65 | 66 | ## Project implementation effect 67 | 68 | ### Run this project 69 | 70 | clone the repo。 71 | 72 | ```bash 73 | git clone https://github.com/Yufccode/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework.git 74 | ``` 75 | 76 | enter the dir. 77 | 78 | ```bash 79 | cd Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework 80 | ``` 81 | 82 | make. 83 | 84 | Executing `make` or `make all` can generate `./Client/client` executable file and `./Server/server` executable file at the same time. Executing `make client` or `make server` will generate the executable files of `client` and `server` respectively. 85 | Executing `make clean` will clean all executable files. 86 | 87 | ```bash 88 | make clean; make 89 | ``` 90 | 91 | After compilation, open two terminals and enter the Server directory and Client directory respectively. When running, you must specify the parameters of negative index production/consumption, otherwise the program will not run normally. As shown in Figure 3. At the same time, because the pipe is created by the server, the server process must be run first before the client process can be run. 92 | 93 | ![](./assets/3.png) 94 | 95 | Running the server or client requires passing the lambda parameter. 96 | 97 | ### Experiments and analysis of different lambda parameter combinations 98 | 99 | - **[diff_lambda_test.md](./docs/diff_lambda_test.md)** 100 | 101 | 102 | ## Detailed design of threadControl object 103 | 104 | ### Q: Why can threadControl objects only use std::string as data in this version? Why are other types not available yet? 105 | 106 | The essence of pipeline transmission is the transmission of characters. In this version of the implementation, only the std::string object can be easily converted into a string. 107 | 108 | If you want to expand this project to multiple data types, you need to encapsulate the data. The essence of the data is a task, which can be encapsulated into a `Task` class. This `Task` class needs to overload the `operator()` method to represent the task. processing. 109 | 110 | The most important thing is to add serialization and deserialization methods to convert the structured Task class into a string type. You can write it manually, or you can use the more commonly used methods now: json or protobuf. 111 | 112 | **This is also an area where this project can continue to be optimized. ** 113 | 114 | ### overall design 115 | 116 | as the picture shows. 117 | 118 | ![](./assets/structure-english.png) 119 | 120 | ### Member variables of threadControl 121 | 122 | It is essentially a thread pool, and the member functions are as follows. **Core: one lock + two synchronization variables. ** 123 | 124 | ```cpp 125 | // The thread pool is essentially a producer-consumer model 126 | template 127 | class thread_control 128 | { 129 | private: 130 | std::vector __worker_threads; // worker thread 131 | int __thread_num; // worker thread number 132 | std::queue __cache; // Task queue, that is, the buffer of 20 slots 133 | size_t __cache_max_size = CACHE_MAX_SIZE_DEFAULT; // The maximum size of the buffer 134 | pthread_mutex_t __cache_lock; // Multiple threads access buffer lock 135 | pthread_cond_t __cache_empty_cond; // Determine whether the buffer is empty 136 | pthread_cond_t __cache_full_cond; // Determine whether the buffer is full 137 | thread *__connector_thread; // connector thread, only one 138 | int __fd; // fd of pipe file 139 | double __lambda; // Negative exponential parameter, consumption frequency/production frequency 140 | public: 141 | bool __quit_signal = false; // Control process ends 142 | // member functions 143 | // ... 144 | } 145 | ``` 146 | 147 | - `std::vector __worker_threads;` represents a series of worker threads. If it is called by a producer, then these threads are responsible for producing data and then throwing it into the cache maintained by the producer ( `std::queue __cache;`). 148 | - `int __thread_num;` indicates the number of worker threads. This parameter is passed in by the constructor for initialization. 149 | - `size_t __cache_max_size = CACHE_MAX_SIZE_DEFAULT;` The maximum size of the buffer, `CACHE_MAX_SIZE_DEFAULT` is defined in comm.hpp. 150 | - One lock + two synchronization variables. Each tc object maintains a lock to protect multi-thread access to the buffer. Because there are multiple worker threads, and the connector thread will also access the buffer, the buffer is a critical area and needs to be protected by a lock. Synchronization variables: When the tc object buffer maintained by the producer is full, the worker thread cannot continue to add data to the buffer, so thread synchronization is required. At the same time, if the buffer is empty, the connector thread can no longer access it. Data. The same goes for consumers, it’s just the other way around. 151 | - `int __fd;` The file descriptor of the pipeline file. This parameter is passed in by the main function of the producer/consumer through the constructor. For producers, this fd is the writing end of the pipe, and for consumers, this fd is the reading end of the pipe. 152 | - `double __lambda;` The parameter lambda produced/consumed by the producer/consumer according to the negative index, passed through the command line parameter -> constructor. 153 | 154 | ### Member functions of threadControl 155 | 156 | Construction and destruction will not be expanded here, you can look at the code directly. Mainly the initialization and destruction of threads, locks and synchronization variables. 157 | 158 | #### run() 159 | 160 | ```cpp 161 | void run() 162 | { 163 | __connector_thread->start(); 164 | logMessage(NORMAL, "%s %s", __connector_thread->name().c_str(), "start\n"); 165 | for (auto &iter : __worker_threads) 166 | { 167 | iter->start(); 168 | logMessage(NORMAL, "%s %s", iter->name().c_str(), "start\n"); 169 | } 170 | // Now that all threads have been started, we need to control the exit logic. 171 | while (1) 172 | // Now all threads are running 173 | if (__quit_signal) // Listen for exit signals 174 | break; 175 | } 176 | ``` 177 | 178 | This method first runs all threads. Of course, I have encapsulated the threads into `thread` type. Calling `thread::start()` is essentially calling `pthread_create()`. 179 | After the thread is started, it needs to monitor the exit signal, let the run() method continue to run, wait for a thread, and send the `__quit_signal` signal before it can stop running. 180 | 181 | #### get_task() 182 | 183 | ```cpp 184 | T get_task() 185 | { 186 | T t = __cache.front(); 187 | __cache.pop(); 188 | return t; // copy return 189 | } 190 | ``` 191 | 192 | In fact, it is a queue operation, taking tasks (data) out of the queue. 193 | 194 | #### other member functions 195 | 196 | ```cpp 197 | pthread_mutex_t *get_mutex() { return &__cache_lock; } 198 | pthread_cond_t *get_empty_cond() { return &__cache_empty_cond; } 199 | pthread_cond_t *get_full_cond() { return &__cache_full_cond; } 200 | std::queue &get_queue() { return this->__cache; } 201 | void wait_empty_cond() { pthread_cond_wait(&__cache_empty_cond, &__cache_lock); } 202 | void wait_full_cond() { pthread_cond_wait(&__cache_full_cond, &__cache_lock); } 203 | bool is_empty() { return __cache.empty(); } 204 | bool is_full() { return __cache.size() >= __cache_max_size; } 205 | int get_fd() { return this->__fd; } 206 | double get_lambda() { return this->__lambda; } 207 | ``` 208 | 209 | Since some member functions are private, when writing the worker method/connector method to be called by the thread, private member variables cannot be accessed, so a series of external access interfaces are provided here. 210 | 211 | ## server's and client's worker and connector 212 | 213 | ### server 214 | 215 | **Running process of server worker** 216 | 217 | ```cpp 218 | void *worker(void *args) 219 | ``` 220 | 221 | First obtain the tc object that calls the worker function through the args parameter. 222 | 223 | ```cpp 224 | __thread_data *td = (__thread_data *)args; 225 | thread_control *tc = (thread_control *)td->__args; 226 | ``` 227 | 228 | A data generator for constructing negative exponential distributions. 229 | 230 | ``` 231 | std::random_device rd; 232 | std::mt19937 gen(rd()); 233 | std::exponential_distribution<> dist(tc->get_lambda()); 234 | ``` 235 | 236 | Then lock it, and then determine whether the synchronization variable meets the conditions, that is, whether the buffer is empty. If there is no data in the buffer, block the thread and wait for server::connector to wake up. 237 | After getting the data (taking it out from the queue), print the data to indicate the processing of the task. 238 | 239 | If "quit" is received, it means that the exit signal is received. At this time, `tc->__quit_signal` should be changed to true. This quit is generated from the connector. At this time, the `tc->run()` method is listening Exit signal. After receiving the exit signal, all threads will be organized to end. 240 | 241 | **The running process of the server's connector** 242 | 243 | Server::connector obtains data from the pipeline, then puts it in the server's cache, waiting for server::worker to process it. 244 | 245 | The process is very simple. It also requires the control of locking and synchronization variables. The specific implementation can be seen in the code, which is to push things into the cache. 246 | 247 | ### client 248 | 249 | The client side is opposite to the server side, and the code is basically the same, so I won’t explain it again here. 250 | 251 | ## lockGuard.hpp RAII style lock wrapper 252 | 253 | RAII-style lock encapsulation is basically a common practice in C++/C system programming, and it can basically be regarded as a standard practice. Has the following benefits: 254 | 255 | - Only use non-recursive mutex (that is, non-reentrant mutex). 256 | - Instead of manually calling the lock() and unlock() functions, everything is handled by the constructor and destructor of the Guard object on the stack. The lifetime of a Guard object is exactly equal to the critical section. 257 | - Every time when constructing a Guard object, consider the locks already held along the way (on the call stack) to prevent deadlock (deadlock) caused by different locking sequences. 258 | - No cross-process mutex is used, only TCP sockets are used for inter-process communication. 259 | - Locking and unlocking are performed in the same thread. Thread a cannot unlock the mutex that thread b has locked (RAII automatically guarantees). ·Don’t forget to unlock (RAII automatically guaranteed). 260 | - No duplicate unlocking (RAII automatic guarantee). 261 | 262 | ## Log.hpp package 263 | 264 | Define four log levels: 265 | 266 | ```cpp 267 | #define DEBUG 0 268 | #define NORMAL 1 269 | #define WARNING 2 270 | #define ERROR 3 271 | #define FATAL 4 272 | ``` 273 | 274 | define function 275 | 276 | ```cpp 277 | void logMessage(int level, const char *format, ...) {} 278 | ``` 279 | 280 | Allows calling with variable parameters. As shown in the following example. 281 | 282 | ```cpp 283 | logMessage(FATAL, "worker function is nullptr or connector fucntion is nullptr, pid: %d", getpid()); 284 | ``` 285 | 286 | It can be set in `#define LOGFILE ""`. If `LOGFILE` is `""`, the log is printed to stdout. If it is a legal path, it is printed to the corresponding file. 287 | 288 | ## comm.hpp 289 | 290 | Some common header files are defined, as well as some global macro parameters that need to be used. Please see the code for details. 291 | -------------------------------------------------------------------------------- /Server/server.cc: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : Fengcheng Yu 4 | // Author : 俞沣城 5 | // Date : 2024.4.10 6 | // Description : 本项目中server进程server.cc编写 7 | // server.cc中需要提供tc对象运行时的worker方法和connector方法 8 | //============================================================================ 9 | 10 | #include "../Utils/comm.hpp" 11 | #include "../Utils/threadControl.hpp" 12 | 13 | std::string ReadFromIpc(int fd) { 14 | while (1) { 15 | // 3. 开始通信 16 | // 读文件 17 | char buffer[SIZE]; 18 | // while (true) 19 | // { 20 | memset(buffer, '\0', sizeof(buffer)); // 先把读取的缓冲区设置为0 21 | ssize_t s = read(fd, buffer, sizeof(buffer) - 1); 22 | // 最好不要让缓冲区写满,因为没有\0 23 | if (s > 0) { 24 | return std::string(buffer); 25 | } else if (s == 0) { 26 | logMessage(NORMAL, "client quit, I quit too\n"); 27 | return std::string("quit"); 28 | } 29 | } 30 | } 31 | 32 | // 这里的worker负责在slot里面找msg,然后打印出来 33 | void* worker(void* args) { 34 | __thread_data* td = (__thread_data*)args; 35 | thread_control* tc = (thread_control*)td->__args; 36 | std::random_device rd; 37 | std::mt19937 gen(rd()); 38 | std::exponential_distribution<> dist(tc->get_lambda()); 39 | while (true) { 40 | double interval = dist(gen); // 生成符合负指数分布的随机数 41 | unsigned int sleepTime = static_cast(std::floor(interval)); // 负指数 42 | sleep(sleepTime); 43 | // 去缓冲区里面获取数据并打印 44 | std::string msg; 45 | { 46 | // 在这个代码块中,线程是安全的 47 | // 这里用个代码块,lockGuard直接就帮我们解锁了 48 | lockGuard lockguard(tc->get_mutex()); 49 | while (tc->is_empty()) 50 | tc->wait_empty_cond(); 51 | // 读取任务 52 | logMessage(DEBUG, "queue size: %d\n", tc->get_queue().size()); // 打印一下队列的数据个数 53 | msg = tc->get_task(); // 任务队列是共享的 54 | pthread_cond_signal(tc->get_full_cond()); // 唤醒connector去继续到pipe里面拿数据,因为我刚拿走一个,空出来位置了 55 | if ((msg == "quit" || tc->__quit_signal == true) && tc->get_queue().size() == 0) // 起码要把队列的数据打印完 56 | { 57 | tc->__quit_signal = true; // 控制退出 58 | break; 59 | } 60 | std::cout << "client# " << msg << std::endl; // 打印这个消息 61 | } 62 | } 63 | return nullptr; 64 | } 65 | 66 | // 这里的connector负责在管道中读取数据 67 | void* connector(void* args) { 68 | // routine 本质就是消费过程 69 | __thread_data* td = (__thread_data*)args; 70 | thread_control* tc = (thread_control*)td->__args; 71 | while (true) { 72 | std::string msg = ReadFromIpc(tc->get_fd()); 73 | // 把这个msg写到缓冲区里面去 74 | lockGuard lockguard(tc->get_mutex()); 75 | if (msg == "quit" || tc->__quit_signal == true) { 76 | // 不要再push到缓冲区中了 77 | // 但是也不要继续循环了 78 | logMessage(NORMAL, "server connector quit\n"); 79 | // 但是需要最后push一次,把quit给push进去,告诉worker要结束了 80 | tc->get_queue().push(msg); 81 | pthread_cond_signal(tc->get_empty_cond()); // 唤醒worker线程,让他去取数据并打印 82 | break; 83 | } 84 | while (tc->is_full()) 85 | tc->wait_full_cond(); 86 | tc->get_queue().push(msg); 87 | pthread_cond_signal(tc->get_empty_cond()); // 唤醒worker线程,让他去取数据并打印 88 | } 89 | return nullptr; 90 | } 91 | 92 | void Usage() { 93 | std::cerr << "Usage: " << std::endl 94 | << "\t./server lambda" << std::endl; 95 | } 96 | 97 | int main(int argc, char** argv) { 98 | // 0. 计算运行时间 99 | auto start = std::chrono::high_resolution_clock::now(); 100 | if (argc != 2) { 101 | Usage(); 102 | exit(1); 103 | } 104 | // 0. 提取命令行参数 105 | double lambda_arg = std::stod(argv[1]); 106 | // 1. 创建管道文件 107 | if (mkfifo(ipcPath.c_str(), MODE) < 0) { 108 | // 创建管道文件失败 109 | perror("mkfifo"); 110 | exit(-1); 111 | } 112 | // 2. 正常的文件操作(只读的方式打开管道文件) 113 | int fd = open(ipcPath.c_str(), O_RDONLY); 114 | assert(fd >= 0); // 文件打开失败 115 | // 3. 管道文件fd传给tc对象 116 | thread_control* tc = new thread_control(worker, connector, 3, fd, lambda_arg); 117 | tc->run(); 118 | // 4. 关闭管道文件 119 | close(fd); 120 | // 5. 删除管道文件 121 | unlink("../Utils/fifo.ipc"); 122 | // 6. 计算时间 123 | auto end = std::chrono::high_resolution_clock::now(); 124 | std::chrono::duration elapsed = end - start; 125 | sleep((unsigned int)WAIT_IO_DONE); // 统一睡眠,等待所有io执行完,再执行下面的io,避免打印的时候重叠了 126 | std::cout << "\t[server run time: ]" << elapsed.count() << "(s)" << std::endl; 127 | return 0; 128 | } -------------------------------------------------------------------------------- /Utils/Log.hpp: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : Log.hpp 4 | // Author : Fengcheng Yu 5 | // Date : 2024.4.10 6 | // Description : 日志函数的封装 7 | //============================================================================ 8 | 9 | #ifndef __YUFC_LOG_HPP__ 10 | #define __YUFC_LOG_HPP__ 11 | 12 | // 日志是有日志级别的 13 | // 不同的级别代表不同的响应方式 14 | 15 | #define DEBUG 0 16 | #define NORMAL 1 17 | #define WARNING 2 18 | #define ERROR 3 19 | #define FATAL 4 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | // 获取对应日志等级的字符串,方便打印 28 | const char* gLevelMap[] = { 29 | "DEBUG", 30 | "NORMAL", 31 | "WARNING", 32 | "ERROR", 33 | "FATAL" 34 | }; 35 | 36 | /* 37 | LOGFILE为""时,日志输出到stdout中国呢 38 | LOGFILE为有效路径时,日志输出到路径指定的文件中 39 | */ 40 | #define LOGFILE "" 41 | 42 | void logMessage(int level, const char* format, ...) { 43 | #ifdef __DEBUG_SHOW 44 | if (level == DEBUG) 45 | return; 46 | #else 47 | char stdBuffer[1024]; 48 | time_t timestamp = time(nullptr); 49 | snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp); 50 | char logBuffer[1024]; // 自定义部分 51 | va_list args; 52 | va_start(args, format); // 初始化一下 53 | vsnprintf(logBuffer, sizeof logBuffer, format, args); // 可以直接格式化到字符串中来 54 | va_end(args); 55 | #endif 56 | FILE* fp = nullptr; 57 | if (LOGFILE == "") 58 | fp = stdout; 59 | else 60 | fp = fopen(LOGFILE, "w"); 61 | fprintf(fp, "%s%s", stdBuffer, logBuffer); 62 | fflush(fp); // 刷新一下缓冲区 63 | if (fp == stdout) 64 | return; 65 | fclose(fp); // 如果是stdout就不要close了,close了还得了 66 | } 67 | 68 | #endif -------------------------------------------------------------------------------- /Utils/comm.hpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | //============================================================================ 4 | // Name : comm.hpp 5 | // Author : Fengcheng Yu 6 | // Date : 2024.4.10 7 | // Description : 配置文件头文件 8 | //============================================================================ 9 | 10 | #ifndef _COMM_HPP_ 11 | #define _COMM_HPP_ 12 | 13 | #include "./Log.hpp" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | // ---------------------------------------- 管道文件相关配置 ---------------------------------------- 29 | #define MODE 0666 // 定义管道文件的权限 30 | #define SIZE 128 // 管道设计的大小要足够大,避免在测试打满server缓冲区时崩溃 31 | std::string ipcPath = "../Utils/fifo.ipc"; 32 | // ----------------------------------- threadControl对象相关配置 ----------------------------------- 33 | #define THREAD_NUM_DEFAULT 3 // 默认三个线程 34 | #define CACHE_MAX_SIZE_DEFAULT 20 // 默认缓冲区最大为20 35 | // ----------------------------------- 通信相关配置 ----------------------------------- 36 | #define MESG_NUMBER 50 // 定义发n条消息之后,Client自动退出 37 | #define WAIT_IO_DONE 2 // 最后client和server会打印程序运行时间,避免前面有io没有结束,导致打印时间的语句不实在最后一行,所以在打印最后的时间语句前统一睡眠,等待io结束 38 | #endif -------------------------------------------------------------------------------- /Utils/lockGuard.hpp: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : lockGuard.hpp 4 | // Author : Fengcheng Yu 5 | // Date : 2024.4.10 6 | // Description : RAII风格对pthread_mutex_t锁进行封装 7 | //============================================================================ 8 | 9 | #ifndef __YUFC_LOCK_GUARD__ 10 | #define __YUFC_LOCK_GUARD__ 11 | 12 | #include 13 | #include 14 | 15 | // 封装一个锁 16 | class Mutex { 17 | private: 18 | pthread_mutex_t* __pmtx; 19 | 20 | public: 21 | Mutex(pthread_mutex_t* mtx) 22 | : __pmtx(mtx) { } 23 | ~Mutex() { } 24 | 25 | public: 26 | void lock() { pthread_mutex_lock(__pmtx); } 27 | void unlock() { pthread_mutex_unlock(__pmtx); } 28 | }; 29 | 30 | // 封装RAII的lockGuard 31 | class lockGuard { 32 | private: 33 | Mutex __mtx; 34 | 35 | public: 36 | lockGuard(pthread_mutex_t* mtx) 37 | : __mtx(mtx) { __mtx.lock(); } 38 | ~lockGuard() { __mtx.unlock(); } 39 | }; 40 | 41 | #endif -------------------------------------------------------------------------------- /Utils/thread.hpp: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : thread.hpp 4 | // Author : Fengcheng Yu 5 | // Date : 2024.4.10 6 | // Description : 对线程的名字和线程的pthread_t进行封装 7 | //============================================================================ 8 | 9 | #ifndef __YUFC_THREAD_HPP__ 10 | #define __YUFC_THREAD_HPP__ 11 | 12 | #include "./comm.hpp" 13 | 14 | typedef void* (*func_t_)(void*); // 函数指针 15 | 16 | class __thread_data { 17 | public: 18 | void* __args; 19 | std::string __name; 20 | }; 21 | 22 | class thread { 23 | private: 24 | std::string __name; // 线程的名字 25 | pthread_t __tid; // 线程tid 26 | func_t_ __func; // 线程要调用的函数 27 | __thread_data __t_data; // 这个线程对应的数据 28 | public: 29 | thread(int num, func_t_ callback, void* args) 30 | : __func(callback) { 31 | char name_buffer[64] = { 0 }; 32 | snprintf(name_buffer, sizeof name_buffer, "Thead-%d", num); 33 | __name = name_buffer; 34 | __t_data.__args = args; 35 | __t_data.__name = __name; 36 | } 37 | ~thread() = default; 38 | // 启动线程 39 | void start() { pthread_create(&__tid, nullptr, __func, &__t_data); } 40 | // 等待线程结束 41 | void join() { pthread_join(__tid, nullptr); } 42 | // 返回线程的名字 43 | std::string name() { return __name; } 44 | }; 45 | 46 | #endif -------------------------------------------------------------------------------- /Utils/threadControl.hpp: -------------------------------------------------------------------------------- 1 | 2 | //============================================================================ 3 | // Name : threadControl.hpp 4 | // Author : Fengcheng Yu 5 | // Date : 2024.4.10 6 | // Description : 本项目中client和server都需要维护的tc对象设计 7 | //============================================================================ 8 | 9 | #ifndef __YUFC_THREAD_CONTROL__ 10 | #define __YUFC_THREAD_CONTROL__ 11 | 12 | #include "./comm.hpp" 13 | #include "./lockGuard.hpp" 14 | #include "./thread.hpp" 15 | #include 16 | #include 17 | #include 18 | 19 | // 线程池本质就是一个生产者消费者模型 20 | template 21 | class thread_control { 22 | private: 23 | std::vector __worker_threads; // worker线程 24 | int __thread_num; // worker线程数量 25 | std::queue __cache; // 任务队列->也就是20个slot的缓冲区 26 | size_t __cache_max_size = CACHE_MAX_SIZE_DEFAULT; // 缓冲区最大的大小 27 | pthread_mutex_t __cache_lock; // 多个线程访问缓冲区的锁 28 | pthread_cond_t __cache_empty_cond; // 判断缓冲区是否为空 29 | pthread_cond_t __cache_full_cond; // 判断缓冲区是否满了 30 | thread* __connector_thread; // connector线程,只有一个 31 | int __fd; // 管道文件的fd 32 | double __lambda; // 负指数参数,消费频率/生产频率 33 | public: 34 | bool __quit_signal = false; // 控制进程结束 35 | 36 | public: 37 | // 这里可以继续封装成对象 38 | thread_control(void* (*worker)(void*) = nullptr, void* (*connector)(void*) = nullptr, int thread_num = THREAD_NUM_DEFAULT, int fd = -1, double lambda = 1.0) 39 | : __thread_num(thread_num) 40 | , __fd(fd) 41 | , __lambda(lambda) { 42 | static_assert(std::is_same::value, "Class template parameter T must be std::string"); // 只能允许字符串 43 | if (worker == nullptr || connector == nullptr) { 44 | logMessage(FATAL, "worker function is nullptr or connector fucntion is nullptr"); 45 | exit(1); 46 | } 47 | // 初始化同步变量和锁 48 | pthread_mutex_init(&__cache_lock, nullptr); 49 | pthread_cond_init(&__cache_empty_cond, nullptr); 50 | pthread_cond_init(&__cache_full_cond, nullptr); 51 | for (int i = 1; i <= __thread_num; i++) // 三个线程去进行worker任务 52 | { 53 | __worker_threads.push_back(new thread(i, worker /*线程要去干的事情*/, this /*这里可以传递this,作为thread的args*/)); 54 | } 55 | // 一个线程去执行connector任务 56 | __connector_thread = new thread(0, connector, this); 57 | } 58 | ~thread_control() { 59 | // 等待所有worker线程 60 | for (auto& iter : __worker_threads) { 61 | iter->join(); // 这个线程把自已join()一下 62 | delete iter; 63 | } 64 | // 等待connector线程 65 | __connector_thread->join(); 66 | // 销毁同步变量和锁 67 | pthread_mutex_destroy(&__cache_lock); 68 | pthread_cond_destroy(&__cache_empty_cond); 69 | pthread_cond_destroy(&__cache_full_cond); 70 | } 71 | void run() { 72 | __connector_thread->start(); 73 | logMessage(NORMAL, "%s %s", __connector_thread->name().c_str(), "start\n"); 74 | for (auto& iter : __worker_threads) { 75 | iter->start(); 76 | logMessage(NORMAL, "%s %s", iter->name().c_str(), "start\n"); 77 | } 78 | // 现在所有的线程都启动好了,要控制退出的逻辑 79 | while (1) 80 | // 现在各个线程都在运行 81 | if (__quit_signal) // 监听退出信号 82 | break; 83 | } 84 | 85 | public: 86 | /* 87 | 需要一批,外部成员访问内部属性的接口提供给static的routine,不然routine里面没法加锁 88 | 下面这些接口,都是没有加锁的,因为我们认为,这些函数被调用的时候,都是在安全的上下文中被调用的 89 | 因为这些函数调用之前,已经加锁了,调用完,lockGuard自动解锁 90 | */ 91 | pthread_mutex_t* get_mutex() { return &__cache_lock; } // 获取互斥锁 92 | pthread_cond_t* get_empty_cond() { return &__cache_empty_cond; } // 获取缓存空的同步变量 93 | pthread_cond_t* get_full_cond() { return &__cache_full_cond; } // 获取缓存满的同步变量 94 | std::queue& get_queue() { return this->__cache; } // 获取任务队列(缓存) 95 | void wait_empty_cond() { pthread_cond_wait(&__cache_empty_cond, &__cache_lock); } // 等待缓存为空的同步变量 96 | void wait_full_cond() { pthread_cond_wait(&__cache_full_cond, &__cache_lock); } // 等待缓存为满的同步变量 97 | bool is_empty() { return __cache.empty(); } // 判断缓存是否为空 98 | bool is_full() { return __cache.size() >= __cache_max_size; } // 判断缓存是否为满 99 | int get_fd() { return this->__fd; } // 获取该tc对象的文件描述符 100 | double get_lambda() { return this->__lambda; } // 获取该tc对象的lambda参数 101 | T get_task() { 102 | T t = __cache.front(); 103 | __cache.pop(); 104 | return t; // 拷贝返回 105 | } 106 | }; 107 | 108 | #endif -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/.DS_Store -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/3.png -------------------------------------------------------------------------------- /assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/4.png -------------------------------------------------------------------------------- /assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/5.png -------------------------------------------------------------------------------- /assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/6.png -------------------------------------------------------------------------------- /assets/structure-english.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/structure-english.png -------------------------------------------------------------------------------- /assets/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffengc/Dual-Thread-Pool-Based-Pipeline-Communication-System-Framework/202238bc180987f23c6d1b39f9f3b54a816edbea/assets/structure.png -------------------------------------------------------------------------------- /docs/diff_lambda_test-CN.md: -------------------------------------------------------------------------------- 1 | 2 | # 不同lambda参数组合实验及其分析 3 | 4 | - **[简体中文](./diff_lambda_test-CN.md)** 5 | - **[English](./diff_lambda_test.md)** 6 | 7 | 在负指数分布中,如果参数越大,则表示 C++11 生成器生成的数字越小,则表示 sleep() 的时间越 小,表示事件越频繁。 8 | 其次,我设置的逻辑是,server 端会在 client 关闭了写端描述符后,才会退出。 9 | 其三,我设置的逻辑是,client 端在发送 50 条消息后,自行退出(这个数字可以在配置文件更改,设 置的数字需要大于 20,缓冲区的大小,这样后面能看到缓冲区被打满的情况)。 10 | 11 | ## 如果 Server 的 lambda 大于或等于 Client 的 lambda 12 | 13 | 此时,消费效率在宏观来看一定是大于/等于生产效率的。此时应该会出现的现象为:server 端的缓存不会被打满,而且会一直处于一个低水平的状态,因为 client 生产的很慢,通过管道过来 server 的数据会 非常少,而消费效率非常高,所以 server 缓存中的消息数量会很少。其实通俗来说:就是供不应求,东西 会被立刻抢光,什么时候发布,什么时候卖完,所以 server 和 client 的运行时间不会差很远。 14 | 15 | ![](../assets/4.png) 16 | 17 | 如图 Server 端 lambda 为 10,Client 端为 1 18 | 19 | ![](../assets/5.png) 20 | 21 | 如图 Server 端 lambda 为 1,Client 端为 1 22 | 23 | 24 | 可以看到,server 缓存一直处于一个低水平状态,其次,client 运行时间和 server 是差不多的,因为 server 端关闭,一定是 client 端已经关闭文件描述符了,也就是已经发送完 50 条消息了,因此退出。而 与此同时,server 端缓冲区处于低水平,因此 server 的 worker 不需要花长时间处理完剩下 server 缓冲区中的内容。 25 | 26 | **因此可以得出结论:如果 Server 的 lambda 大于或等于 Client 的 lambda,server 的运行时间,会比 client 略长一个任务的处理时间(即一个任务从管道写端开始,到被 server 端的 worker 线程处理完毕这 一段时间)。** 27 | 28 | ## 如果 Server 的 lambda 小于 Client 的 lambda 29 | 30 | **此时,消费效率小于生产效率的。此时应该会出现的现象为:server 端的缓存会被打满!因为 client 生产的太快了,通过管道过来 server 的数据会非常多,而消费效率非常小,所以 server 缓存中的消息数 量会很非常多,当然 server 缓存被打满之后,connector 线程也会被阻塞(因为有互斥同步变量存在),此 时甚至连管道,都有可能被打满,造成程序崩溃,所以在这个实验里面,我把管道大小设置为一个足够的 值,方便我们观察 server 缓存被打满的情况。其实通俗来说:就是供大于求,东西会堆积,发布完之后, 很长时间,才能被消耗完!** 31 | 32 | ![](../assets/6.png) 33 | 34 | 如图 此时 Server 端 lambda 为 1,Client 端为 10 此时的情况也是符合预期,缓冲区被打满,server 跑半天跑不出来。 -------------------------------------------------------------------------------- /docs/diff_lambda_test.md: -------------------------------------------------------------------------------- 1 | 2 | # Experiments and analysis of different lambda parameter combinations 3 | 4 | - **[简体中文](./diff_lambda_test-CN.md)** 5 | - **[English](./diff_lambda_test.md)** 6 | 7 | In the negative exponential distribution, if the parameter is larger, it means that the number generated by the C++11 generator is smaller, which means that the sleep() time is shorter, which means that the events are more frequent. 8 | Secondly, the logic I set is that the server will not exit until the client closes the write-side descriptor. 9 | Third, the logic I set is that the client will exit on its own after sending 50 messages (this number can be changed in the configuration file. The set number needs to be greater than 20, the size of the buffer, so that you can see the buffer being opened later. full). 10 | 11 | ## If Server's lambda is greater than or equal to Client's lambda 12 | 13 | At this time, consumption efficiency must be greater than/equal to production efficiency from a macro perspective. The phenomenon that should occur at this time is: the cache on the server side will not be filled up, and will always be in a low-level state. Because the client produces very slowly, there will be very little data coming to the server through the pipeline, and the consumption efficiency is very high. , so the number of messages in the server cache will be very small. In fact, in layman's terms: supply exceeds demand, and things will be sold out immediately, when they are released, and when they are sold out, so the running time of the server and client will not be very different. 14 | 15 | ![](../assets/4.png) 16 | 17 | As shown in the figure, the lambda on the server side is 10 and the client side is 1. 18 | 19 | ![](../assets/5.png) 20 | 21 | As shown in the figure, the lambda on the server side is 1 and the client side is 1. 22 | 23 | 24 | It can be seen that the server cache has always been at a low level. Secondly, the client running time is almost the same as that of the server. Because the server is closed, the client must have closed the file descriptor, that is, 50 messages have been sent. So quit. At the same time, the server-side buffer is at a low level, so the server's workers do not need to take a long time to process the contents of the remaining server buffer. 25 | 26 | **Therefore, it can be concluded that if the lambda of the server is greater than or equal to the lambda of the client, the running time of the server will be slightly longer than the processing time of one task of the client (that is, a task starts from the writing end of the pipeline and is processed by the worker thread on the server side. this period of time).** 27 | 28 | ## If Server's lambda is smaller than Client's lambda 29 | 30 | **At this time, consumption efficiency is less than production efficiency. The phenomenon that should occur at this time is: the cache on the server side will be full! Because the client produces too fast, there will be a lot of data coming to the server through the pipeline, and the consumption efficiency is very small, so the number of messages in the server cache will be very large. Very much. Of course, after the server cache is full, the connector thread will also be blocked (because of the existence of mutually exclusive synchronization variables). At this time, even the pipeline may be full, causing the program to crash, so in this experiment, I set the pipe size to a sufficient value so that we can observe the situation when the server cache is full. In fact, in layman's terms: supply exceeds demand, things will accumulate, and it will take a long time for them to be consumed after they are released!** 31 | 32 | ![](../assets/6.png) 33 | 34 | As shown in the figure, the lambda on the server side is 1 and the client side is 10. The situation at this time is also in line with expectations. The buffer is full and the server cannot run out for a long time. -------------------------------------------------------------------------------- /docs/reuse-the-threadpool-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 复用线程池对象 4 | 5 | - **[简体中文](./reuse-the-threadpool-CN.md)** 6 | - **[English](./reuse-the-threadpool.md)** 7 | 8 | 代码见 threadControl.hpp。 9 | 10 | 本对象本质就是一个线程池,首先,有非常多的优化方案,这个我在readme中也有提到。 11 | 12 | ## 项目可以优化的方向 13 | 14 | 我欢迎各位对我本项目进行持续的优化,如果要对本项目进行优化,大家可以联系我的邮箱(我的主页上有)之后,fork本仓库进行优化,如果我认为这个优化是ok的,我会进行merge操作的! 15 | 16 | - connector线程目前只有一个,可以进行优化,connector线程通过参数进行设定。 17 | - 可以优化使得connector线程结合多路转接技术,具体可以见我另一个仓库,[Multiplexing-high-performance-IO-server](https://github.com/Yufccode/Multiplexing-high-performance-IO-server),同时监听多个文件描述符,这个文件描述符不止来自于管道,还可以来自于网络,来自于硬件等等,因此本项目是可以持续优化的! 18 | - 优化线程池支持的数据类型,目前只支持string类型,因为目前来说,我还没有编写序列化,反序列化的代码,因此只支持string/char*类型。如果本项目要扩展到网络服务器上,或者其他地方,都需要编写反序列化和序列化的代码,维护一个Task类,Task类可以序列化反序列化。同时Task类可以重载operator()方法,表示Task任务的执行和调用。 19 | 20 | ## 这个线程池在干什么 21 | 22 | **一句话总结:线程池维护一个缓存,缓存里面是数据/任务,worker线程和connector线程会基于这个缓存做不同的事情。** 23 | 24 | 在我的项目中,server端的worker就是生成数据,push到缓存中,connector从缓存中取数据!当然,worker和connector要做的事情是可以改的,是通过传参决定的! 25 | 26 | ## 如何复用 27 | 28 | 看这一部分之前还是先要看下[项目可以优化的方向](#项目可以优化的方向),更容易理解复用的方法和原理。 29 | 30 | 调用threadControl对象。 31 | 32 | ```cpp 33 | thread_control* tc = new thread_control(worker, connector, 3, fd, lambda_arg); 34 | tc->run(); 35 | ``` 36 | 37 | 首先可以见到我编写的代码中server.cc,会传递一个worker和connector函数指针(当然你也可以修改代码封装成C++的函数对象),这个是这个线程池必须的参数,这两个参数表示worker和connector线程分别要做的事情,至于后面的参数,这个3表示是worker线程的数量(connector线程的数量现在是写死的,就是一个,你当然可以在这基础上优化),fd表示我的这个项目中管道文件的文件描述符(不是必须参数),lambda\_arg表示我这个项目中所需要的lambda参数(不是必须参数)。 38 | 39 | 你可以复用这个threadControl.hpp文件,让worker和connector做不同的事情,这就是复用的方法! -------------------------------------------------------------------------------- /docs/reuse-the-threadpool.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Reuse thread pool objects 4 | 5 | - **[简体中文](./reuse-the-threadpool-CN.md)** 6 | - **[English](./reuse-the-threadpool.md)** 7 | 8 | See threadControl.hpp for the code. 9 | 10 | This object is essentially a thread pool. First of all, there are many optimization solutions, which I also mentioned in the readme. 11 | 12 | ## Directions in which the project can be optimized 13 | 14 | I welcome everyone to continue optimizing my project. If you want to optimize this project, you can contact my email address (available on my homepage) and then fork this warehouse for optimization. If I think this optimization is OK, I The merge operation will be performed! 15 | 16 | - There is currently only one connector thread, which can be optimized. The connector thread is set through parameters. 17 | - The connector thread can be optimized to combine with multiplexing technology. For details, please see my other warehouse, [Multiplexing-high-performance-IO-server](https://github.com/Yufccode/Multiplexing-high-performance-IO -server), monitor multiple file descriptors at the same time. This file descriptor not only comes from pipes, but also from the network, from hardware, etc., so this project can be continuously optimized! 18 | - Optimize the data types supported by the thread pool. Currently, only string types are supported. Because at present, I have not written serialization and deserialization code, so only string/char* types are supported. If this project is to be expanded to a network server or other places, it is necessary to write deserialization and serialization code and maintain a Task class, which can be serialized and deserialized. At the same time, the Task class can overload the operator() method to represent the execution and invocation of the Task task. 19 | 20 | ## What is this thread pool doing? 21 | 22 | **Summary in one sentence: The thread pool maintains a cache, which contains data/tasks. The worker thread and the connector thread will do different things based on this cache. ** 23 | 24 | In my project, the worker on the server side generates data, pushes it to the cache, and the connector fetches data from the cache! Of course, what workers and connectors want to do can be changed and is determined by passing parameters! 25 | 26 | ## 如何复用 27 | 28 | Before reading this part, you should first look at [Directions in which the project can be optimized] (#Directions in which the project can be optimized) to make it easier to understand the methods and principles of reuse. 29 | 30 | Call the threadControl object. 31 | 32 | ```cpp 33 | thread_control* tc = new thread_control(worker, connector, 3, fd, lambda_arg); 34 | tc->run(); 35 | ``` 36 | 37 | First of all, you can see that server.cc in the code I wrote will pass a worker and connector function pointer (of course you can also modify the code to encapsulate it into a C++ function object). This is a necessary parameter for this thread pool. These two parameters represent What the worker and connector threads have to do respectively. As for the following parameters, this 3 represents the number of worker threads (the number of connector threads is now hard-coded, it is just one. Of course you can optimize on this basis), fd means I The file descriptor of the pipeline file in this project (not a required parameter), lambda\_arg represents the lambda parameter required in my project (not a required parameter). 38 | 39 | You can reuse this threadControl.hpp file and let workers and connectors do different things. This is the reuse method! -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY:all 2 | all:client server 3 | 4 | client:./Client/client.cc 5 | g++ -o $@ $^ -std=c++11; 6 | mv client Client 7 | server:./Server/server.cc 8 | g++ -o $@ $^ -std=c++11 9 | mv server Server 10 | 11 | .PHONY:clean 12 | clean: 13 | rm -f ./Server/server ./Client/client --------------------------------------------------------------------------------