操作系统实验报告13

实验内容

  • 实验内容:设计实现一个线程池 (Thread Pool)

    • 使用 Pthread API
    • FIFO
    • 先不考虑互斥问题
    • 编译、运行、测试用例

实验环境

  • 架构:Intel x86_64 (虚拟机)
  • 操作系统:Ubuntu 20.04
  • 汇编器:gas (GNU Assembler) in AT&T mode
  • 编译器:gcc

技术日志

实验内容原理

  • 线程池

    • 解决问题:

      • 数量上没有限制的线程可能耗尽系统资源,如CPU时间或内存。
    • 解决方案:
      • 在进程启动时创建多个线程,并将它们放入线程池中,它们进入阻塞状态等待工作。
      • 当服务器收到一个请求时,会从这个池中唤醒一个可用的线程,并将服务请求传递给这个线程。
      • 一旦线程完成其服务,它就会返回到池并等待更多的工作。如果池中不包含可用线程,服务器将等待一个线程空闲。
    • 例子:
      • IA-32中每个进程的最大线程数。

        • 这个数字大约是300,3G地址空间中每个线程的默认堆栈大小为10M。
      • 可以创建少于255个线程的池。
    • 线程池的好处
      • 使用现有线程为请求提供服务通常比等待创建新线程要快一些。
      • 线程池限制任意时刻时存在的线程数。对于不能支持大量并发线程的系统,这一点尤为重要。
      • 将要执行的任务与创建任务的机制分离,允许我们使用不同的策略来运行任务。
    • 线程池的大小
      • 池中的线程数可以根据系统中cpu的数量、物理内存的数量和预期的并发客户机请求的数量等因素进行启发式设置。
      • 更为复杂的线程池架构(如苹果的Grand Central Dispatch)可以根据使用模式动态调整池中的线程数。

设计报告

线程池设计图

代码设计

测试代码:

//threadpools.c文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sched.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/time.h>
#include <sys/msg.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>#define gettid() syscall(__NR_gettid)
/* wrap the system call syscall(__NR_gettid), __NR_gettid = 224 */
#define gettidv2() syscall(SYS_gettid) /* a traditional wrapper */#define THREADS_NUM 10 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 12 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 50 // 要执行的任务总数// 线程池中每个线程执行的任务的结构体
typedef struct {void *(*function)(void *); // 执行函数void *arg; // 参数
} Task;// 任务循环队列的数据结构
typedef struct {Task tasks[TASK_QUEUE_MAX_SIZE]; // 任务队列数组int front; // 队首下标int rear; // 队尾下标
} TaskQueue;// 线程池数据结构
typedef struct {
pthread_t threads[THREADS_NUM]; // 线程数组
TaskQueue taskQueue; // 任务队列
int taskSum; // 剩余任务总数,结束程序用
} Threadpools;// 线程池中每个线程执行的任务
static void *executeTask(void *arg) {// 向每个线程传入的参数是线程池Threadpools *pools = (Threadpools *)arg;while (1) {// 当任务队列为空时while (pools->taskQueue.front == pools->taskQueue.rear) {// 如果已经没有剩余任务要处理,那么退出线程if (pools->taskSum == 0) {printf("Thread %ld exits.\n", gettid());pthread_exit(NULL);}// 否则等待任务队列中有任务后再取任务进行执行printf("Thread %ld is waiting for a task.\n", gettid());sleep(1);                }// 剩余任务总数减一pools->taskSum--;// 获取任务队列队首的任务Task task;int front = pools->taskQueue.front;task.function = pools->taskQueue.tasks[front].function;task.arg = pools->taskQueue.tasks[front].arg;// 循环队列队首下标加一pools->taskQueue.front = (front + 1) % TASK_QUEUE_MAX_SIZE;// 执行任务(*(task.function))(task.arg);}
}// 初始化线程池
void initThreadpools(Threadpools *pools) {int ret;// 任务队列的队首和队尾的坐标都为0pools->taskQueue.front = 0;pools->taskQueue.rear = 0;// 线程池中剩余的任务总数设置为总任务数pools->taskSum = TASK_NUM;// 创建线程池中的线程for(int i = 0; i < THREADS_NUM; ++i) {ret = pthread_create(&pools->threads[i], NULL, executeTask, (void *)pools);if(ret != 0) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(1);}}
}// 向任务队列中添加任务
void addTask(Threadpools *pools, void *(*function)(void *arg), void *arg) {// 当任务队列为满时,等待有任务被取出任务队列不为满再加入队列while ((pools->taskQueue.rear + TASK_QUEUE_MAX_SIZE + 1 - pools->taskQueue.front) % TASK_QUEUE_MAX_SIZE == 0) {printf("Task %d is waiting to be added to the task queue.\n", *(int *)arg);sleep(1);}// 向任务队列的队尾加入任务Task task;task.function = function;task.arg = arg;int rear = pools->taskQueue.rear;pools->taskQueue.tasks[rear] = task;// 任务队列队尾下标加一pools->taskQueue.rear = (rear + 1) % (TASK_QUEUE_MAX_SIZE);
}// 任务函数
void *taskFunction(void *arg) {// 获取每个任务的任务号int *numptr = (int *)arg;int taskId = *numptr;// 打印线程池中的哪个线程正在处理此任务printf("Thread tid = %ld is dealing with task %d\n", gettid(), taskId);// 每个任务休眠1s后继续执行printf("Task %d is sleeping for 1s.\n", taskId);sleep(1);// 打印任务完成信息和线程被复用printf("\t\t\t\tTask %d is finished and Thread tid = %ld is reused\n", taskId, gettid());return 0;
}int main() {int ret;// 创建并初始化线程池Threadpools pools;initThreadpools(&pools);// 休眠1s测试线程池中的线程在任务队列为空时是否会等待sleep(1);// 传入参数数组int num[TASK_NUM];for(int i = 0; i < TASK_NUM; ++i) {num[i] = i + 1;}// 向任务队列中连续添加任务for(int i = 0; i < TASK_NUM; ++i) {addTask(&pools, taskFunction, (void *)&num[i]);}// 主线程等待线程池中的线程全部结束后再继续for(int i = 0; i < THREADS_NUM; ++i) {ret = pthread_join(pools.threads[i], NULL);if(ret != 0) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(1);}}// 所有任务都执行完,线程池也退出printf("\nAll %d tasks have been finished.\n", TASK_NUM);
}

首先进行宏定义:

#define THREADS_NUM 10 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 12 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 50 // 要执行的任务总数

为了方便测试,这里线程个数和任务队列长度设置的较小,要执行的任务总数相对线程个数较多。

然后定义使用到的数据结构:

任务:

// 线程池中每个线程执行的任务的结构体
typedef struct {void *(*function)(void *); // 执行函数void *arg; // 参数
} Task;

任务队列和线程池:

// 任务循环队列的数据结构
typedef struct {Task tasks[TASK_QUEUE_MAX_SIZE]; // 任务队列数组int front; // 队首下标int rear; // 队尾下标
} TaskQueue;// 线程池数据结构
typedef struct {
pthread_t threads[THREADS_NUM]; // 线程数组
TaskQueue taskQueue; // 任务队列
int taskSum; // 剩余任务总数,结束程序用
} Threadpools;

线程池初始化函数:

// 初始化线程池
void initThreadpools(Threadpools *pools) {int ret;// 任务队列的队首和队尾的坐标都为0pools->taskQueue.front = 0;pools->taskQueue.rear = 0;// 线程池中剩余的任务总数设置为总任务数pools->taskSum = TASK_NUM;// 创建线程池中的线程for(int i = 0; i < THREADS_NUM; ++i) {ret = pthread_create(&pools->threads[i], NULL, executeTask, (void *)pools);if(ret != 0) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(1);}}
}

创建线程池中的线程时,可以看到每个线程执行的函数都为executeTask()任务执行函数。

对应设计图中的初始化线程池部分:

接着实现函数部分:

线程执行函数:

// 线程池中每个线程执行的任务
static void *executeTask(void *arg) {// 向每个线程传入的参数是线程池Threadpools *pools = (Threadpools *)arg;while (1) {// 当任务队列为空时while (pools->taskQueue.front == pools->taskQueue.rear) {// 如果已经没有剩余任务要处理,那么退出线程if (pools->taskSum == 0) {printf("Thread %ld exits.\n", gettid());pthread_exit(NULL);}// 否则等待任务队列中有任务后再取任务进行执行printf("Thread %ld is waiting for a task.\n", gettid());sleep(1);                }// 剩余任务总数减一pools->taskSum--;// 获取任务队列队首的任务Task task;int front = pools->taskQueue.front;task.function = pools->taskQueue.tasks[front].function;task.arg = pools->taskQueue.tasks[front].arg;// 循环队列队首下标加一pools->taskQueue.front = (front + 1) % TASK_QUEUE_MAX_SIZE;// 执行任务(*(task.function))(task.arg);}
}

可以看到,每个线程执行完任务后,若还有剩余任务且任务队列不为空,线程会自动从任务队列中获取任务,继续执行任务,而不用手动为每一个任务指定一个空闲线程进行执行,任务队列为循环队列,每次从任务队列的队首获取任务,保证了FIFO。

对应设计图中的每个线程获取任务的箭头部分:

将任务添加到任务队列函数:

// 向任务队列中添加任务
void addTask(Threadpools *pools, void *(*function)(void *arg), void *arg) {// 当任务队列为满时,等待有任务被取出任务队列不为满再加入队列while ((pools->taskQueue.rear + TASK_QUEUE_MAX_SIZE + 1 - pools->taskQueue.front) % TASK_QUEUE_MAX_SIZE == 0) {printf("Task %d is waiting to be added to the task queue.\n", *(int *)arg);sleep(1);}// 向任务队列的队尾加入任务Task task;task.function = function;task.arg = arg;int rear = pools->taskQueue.rear;pools->taskQueue.tasks[rear] = task;// 任务队列队尾下标加一pools->taskQueue.rear = (rear + 1) % (TASK_QUEUE_MAX_SIZE);
}

可以看到,任务队列为循环队列,每次向任务队列的队尾添加任务,保证了FIFO。

对应设计图中的将任务添加到任务队列的箭头部分:

每个任务执行的函数:

// 任务函数
void *taskFunction(void *arg) {// 获取每个任务的任务号int *numptr = (int *)arg;int taskId = *numptr;// 打印线程池中的哪个线程正在处理此任务printf("Thread tid = %ld is dealing with task %d\n", gettid(), taskId);// 每个任务休眠1s后继续执行printf("Task %d is sleeping for 1s.\n", taskId);sleep(1);// 打印任务完成信息和线程被复用printf("\t\t\t\tTask %d is finished and Thread tid = %ld is reused\n", taskId, gettid());return 0;
}

对应设计图中的每个任务执行的内容部分:

主函数中:

int main() {// 创建并初始化线程池Threadpools pools;initThreadpools(&pools);// 休眠1s测试线程池中的线程在任务队列为空时是否会等待sleep(1);// 传入参数数组int num[TASK_NUM];for(int i = 0; i < TASK_NUM; ++i) {num[i] = i + 1;}// 向任务队列中连续添加任务for(int i = 0; i < TASK_NUM; ++i) {addTask(&pools, taskFunction, (void *)&num[i]);}// 主线程等待线程池中的线程全部结束后再继续for(int i = 0; i < THREADS_NUM; ++i) {pthread_join(pools.threads[i], NULL);if(ret != 0) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(1);}}// 所有任务都执行完,线程池也退出printf("\nAll %d tasks have been finished.\n", TASK_NUM);
}

主函数中,先创建线程池,此时线程处在等待状态,然后再添加任务,线程池中的线程执行完所有的任务后,再退出程序。

执行命令:

gcc threadpools.c -pthread
./a.out

分析:

可以看到,一开始当任务队列中还没有任务时,线程池中的线程会等待任务队列中有任务后再取出任务接着执行。

可以看到,每个线程按照FIFO从任务队列中取出任务进行执行,每个任务会休眠1s,如果任务队列已满,新的任务会等待任务队列有任务被取出后再加入任务队列。

可以看到,任务执行完成之后,线程池中的线程会被复用,同一个tid的线程会自动从任务队列中获取任务,可以执行不同的任务。

可以看到,当所有的任务都被执行完后,线程池中所有线程退出,回到主线程之后继续,程序正常退出。

测试用例:

在宏定义处,改变线程池中的线程个数,任务队列的最大长度和要执行的认为总数,可以进行测试程序:

测试用例1:

线程个数为10,任务队列最大长度为12(最大任务个数为11),任务总数为50:

#define THREADS_NUM 10 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 12 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 50 // 要执行的任务总数

执行截图:

任务总数稍大于线程个数和任务队列长度时,可以看到,线程池可以正常运行。

测试用例2:

线程个数为10,任务队列最大长度为12(最大任务个数为11),任务总数为5:

#define THREADS_NUM 10 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 12 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 5 // 要执行的任务总数

执行截图:

任务总数小于线程个数和任务队列长度时,可以看到,线程池可以正常运行。

测试用例3:

线程个数为10,任务队列最大长度为12(最大任务个数为11),任务总数为1000:

#define THREADS_NUM 10 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 12 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 1000 // 要执行的任务总数

执行截图:

任务总数远远大于线程个数和任务队列长度时,可以看到,线程池可以正常运行。

测试用例4:

线程个数为500,任务队列最大长度为550(最大任务个数为549),任务总数为1000:

#define THREADS_NUM 500 // 线程池中的线程个数
#define TASK_QUEUE_MAX_SIZE 550 // 任务的等待队列的最大长度,等待队列中的最大任务个数为长度减一
#define TASK_NUM 1000 // 要执行的任务总数

执行截图:

相比之前一个测试样例,这个样例的线程个数较大,线程池可以正常运行,同时因为同时运行的线程较多,所以运行速度相比之前一个样例快了很多。

操作系统实验报告13:线程池简单实现相关推荐

  1. 操作系统实验报告15:进程同步与互斥线程池

    操作系统实验报告15 实验内容 实验内容:进程同步. 内容1:编译运行课件 Lecture18 例程代码. Algorithms 18-1 ~ 18-9. 内容2:在 Lab Week 13 的基础上 ...

  2. 操作系统实验报告12:线程2

    操作系统实验报告12 实验内容 实验内容:线程(2). 编译运行课件 Lecture14 例程代码: Algorithms 14-1 ~ 14-7. 比较 pthread 和 clone() 线程实现 ...

  3. 操作系统实验报告10:线程1

    操作系统实验报告10 实验内容 实验内容:线程(1). 编译运行课件 Lecture13 例程代码: Algorithms 13-1 ~ 13-8 实验环境 架构:Intel x86_64 (虚拟机) ...

  4. 操作系统实验报告16:CPU 调度

    操作系统实验报告16 实验内容 实验内容:CPU 调度. 讨论课件 Lecture19-20 中 CPU 调度算法的例子,尝试基于 POSIX API 设计一个简单调度器(不考虑资源竞争问题): 创建 ...

  5. 操作系统实验报告5:进程的创建和终止

    操作系统实验报告5 实验内容 实验内容:进程的创建和终止. 编译运行课件 Lecture 06 例程代码:Algorithm 6-1 ~ 6-6. 实验环境 架构:Intel x86_64 (虚拟机) ...

  6. 操作系统实验报告3:Linux 下 x86 汇编语言2

    操作系统实验报告3 实验内容 验证实验 Blum's Book: Sample programs in Chapter 06, 07 (Controlling Flow and Using Numbe ...

  7. 操作系统实验报告【太原理工大学】

    操作系统实验报告 温馨提示:仅供参考! 目录 操作系统实验报告 一.进程调度程序设计 1.程序清单 2.运行结果 3.分析总结 二.页式虚拟存储管理程序设计 1.程序清单 2.运行结果 3.分析总结 ...

  8. 西工大计算机操作系统实验报告,西工大操作系统实验报告os4.doc

    西工大操作系统实验报告os4 篇一:西北工业大学-操作系统实验报告-实验四 实验四 进程与线程 一. 实验目的 (1)理解进程的独立空间: (2)理解线程的相关概念. 二. 实验内容与要求 1.查阅资 ...

  9. 计算机操作系统安装实验报告,计算机操作系统实验报告.doc

    计算机操作系统实验报告.doc (12页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 14.9 积分 计算机操作系统实验报告实验一一.实验目的 在单处理 ...

最新文章

  1. 又一个Jupyter神器,操作Excel自动生成Python代码
  2. 工作失职的处理决定_工作失误处理决定
  3. 查看“Active Directory 架构”
  4. USB供电不足怎么办
  5. equals()和==
  6. stm32 内部sram大小_让STM32的外部SRAM操作跟内部SRAM一样
  7. 信息学奥赛一本通 1096:数字统计 | 1949:【10NOIP普及组】数字统计 | OpenJudge NOI 1.5 41
  8. 03-22 H5 性能分析
  9. 在Vim中将DOS行尾转换为Linux行尾
  10. 如何在Python中将Word转换为图片?
  11. ChemDraw怎么画3D图?
  12. 外文书籍的中文翻译版本作参考文献,文献引用格式
  13. 桥梁工程智慧工地管理系统,实现工程项目的精细化管理
  14. 第十一届蓝桥杯省赛第一场原题
  15. R语言|如何进行t检验
  16. 冒泡排序【必会知识】
  17. python怎么自动生成文档_用 Python 自动生成 Word 文档
  18. CT值以及窗宽窗位(未完待续)
  19. 如何高效顺利发表sci论文
  20. 平板win10 android哪个耗电,您会为平板电脑选择win10还是Android?

热门文章

  1. IT界含金量高的认证考试
  2. 使用expdp导出时评估所需存储容量大小
  3. js/vue 高德地图绘制驾车路线图
  4. centos7下安全访问远程服务器
  5. [一天一个小知识]instanceof
  6. mysql导入导出数据库文件(转载)
  7. C/C++变量命名规则,个人习惯总结
  8. IplImage类型解释和举例
  9. 关于 mldonkey 的一些讨论和设置
  10. Google Chrome Frame