从0到1实现线程池(C语言版)

news2024/11/15 10:58:10

目录

🌤️1. 基础知识

⛅1.1 线程概述

⛅1.2 linux下线程相关函数

🌥️1.2.1 线程ID

🌥️1.2.2 线程创建

🌥️1.2.3 线程回收

🌥️1.2.4 线程分离

🌤️2. 线程池概述

⛅2.1 线程池的定义

⛅2.2 为什么要有线程池(线程池的作用)

⛅2.3 线程池的构成

⛅2.4 线程数量的确定

🌤️3、实现线程池

⛅3.1 接口设计

⛅3.2 结构体设计

⛅3.3 关键函数书写

🌥️3.3.1 __threads_create & __threads_terminate

🌥️3.3.2 __taskqueue_create

🌥️3.3.3 __add_task & __pop_task


🌤️1. 基础知识

⛅1.1 线程概述

要实现线程池,首先要了解什么是线程?要说线程就不得不提进程,以 Windows 下 QQ 为例,当我们双击打开 QQ,便打开了一个 QQ 进程,进程可以简单的认为是程序的一次执行过程,是操作系统分配基本运行资源的基本单位,可以通过任务管理器查看每一个进程的资源(如 CPU、内存、磁盘、网络)使用情况。在 QQ 中,我们可以同时打字聊天、语音通话、下载文件等,在同一个进程 QQ 中,这些同时进行的任务就是由不同的线程来执行的。线程是进程内的一个执行单元,它与其他线程共享该进程的资源,每个线程都拥有自己的执行序列、程序计数器、寄存器集和堆栈,线程是操作系统调度执行的最小单位。在 Linux 中,其实进程和线程不做区分,关于这些本质细节后面会出专门的专栏进行讲解,现在只需要知道线程就是来执行某个特定任务的。

⛅1.2 linux下线程相关函数

为了实现线程池,还需要掌握一些常见的线程函数。

🌥️1.2.1 线程ID

每一个线程都有一个唯一的线程ID,类型为pthread_t,一个无符号长整形数,可以调用如下函数获得:

pthread_t pthread_self(void);	// 返回当前线程的线程ID
🌥️1.2.2 线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
// thread:线程ID;attr: 线程属性, 一般写NULL;start_routine:函数指针;arg:函数实参
// 返回值:成功返回0

给大家举个例子就明白怎么创建一个线程了。

可以这样来理解线程创建函数:tid作为标记值管理线程,就像用fd来管理文件描述符一样,学号来管理学生一样,作为唯一不可重复的标识,线程是用来处理任务的,所以要传一个函数指针,这个任务函数可能有参数,所以还要传入参数。

🌥️1.2.3 线程回收
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞,直到子线程退出, 函数解除阻塞, 回收对应的子线程资源
int pthread_join(pthread_t thread, void **retval);
// thread:线程ID;retval:一般写NULL
// 返回值:成功返回0

有线程创建就必有线程回收,这个函数调用之后,主线程(一般为main函数)要阻塞等待所有的子线程完成,所以效率较低。

🌥️1.2.4 线程分离

在很多情况下,主线程有属于自己的业务处理流程,调用pthread_join()只要子线程不退出主线程就会一直被阻塞。为了解决这种效率低的问题,线程库函数为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,占用的内核资源就被系统的其他进程接管并回收了

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

🌤️2. 线程池概述

在正式实现线程池之前,先来了解一下线程池的基础概念,线程池是一个框架中的基础轮子,在几年前有些同学会把线程池作为简历中的一个项目,其实线程池不能是一个项目,因为并没有实现任何业务功能,线程池是用来优化一个框架或者项目的。

⛅2.1 线程池的定义

线程池就是维持管理固定数量线程的池式结构,除了线程池之外,还有大家比较熟悉的内存池、数据库连接池等池式结构,他们都有一个共同点,就是复用资源,为什么要复用,因为这些资源的创建和销毁都是比较耗时且繁琐的;什么叫维持?就是反复的去利用线程池里的线程,没有任务就休眠(就绪态),有任务就去调度执行。

⛅2.2 为什么要有线程池(线程池的作用)

当主线程在执行的过程中,遇到了耗时任务,会严重耽误主线程的运行效率的时候,主线程将这些任务发布到线程池,让线程池来完成,主线程继续执行核心业务逻辑,下面来举个生活中的例子便于大家理解。

想象一下你是一家餐厅的经理(主线程)。餐厅面对的顾客(任务)是多种多样的,包括点餐、上菜、收钱等等。如果餐厅只有你一个人来做所有的事情,显然效率会非常低下,特别是在顾客高峰期,你可能会不堪重负。这时,你可以雇佣几位员工(线程池中的线程)来帮忙。你可以将任务分配给这些员工:比如一个专门接待顾客点餐,一个负责上菜,另一个负责收银。这样,每个员工都有明确的责任,可以同时处理不同的任务,大大提高了整体的运作效率。

线程池的工作方式也类似。当有多个任务需要处理时,线程池可以提供多个线程来同时执行任务,主线程则可以继续执行其他的核心业务逻辑,而不必等待每一个任务完成,从而提高了程序的性能和响应速度。这样的机制减少了线程创建和销毁的开销,并可以有效地管理线程的生命周期。

⛅2.3 线程池的构成

有了上面的讲述,我们知道线程池需要有生产者线程在发布耗时任务,有消费者线程(线程池)来负责执行任务,然后就来思考一下生产者线程和消费者线程怎么进行交互?想一下身为经理的你怎么和员工交互呢?可以使用手机、口头表述等形式,在计算机中就需要一种数据结构,并且在线程池中该数据结构还对应着一种多进程环境,这里我们采用任务队列,或者有朋友会想,可不可以用其他数据结构?几乎在所有线程池开源代码中,队列是唯一的选择,也可以用和队列类似的结构如数组。

队列是一种先进先出的顺序数据结构,生产者线程push任务,消费者线程pop任务,并且队列加锁十分方便,不会占用太长时间,同时链表插入有序。

这里再来补充一个线程三状态转移图:

⛅2.4 线程数量的确定

首先,为什么要维护固定数量的线程呢?主要是为了避免线程频繁的创建和销毁。对于不同的程序来说,所适合维持的线程数量一般是固定的,主要由以下经验公式确认,对于 CPU 密集型程序,一般设置为 CPU 核心数较为合适,对于 IO 密集型程序,一般设置为两倍的 CPU 核心数较为合适。在实际的测试过程中,是在上述线程数量附近寻找最合适的数量。

🌤️3、实现线程池

⛅3.1 接口设计

在正式写代码之前,首先要确定要给用户暴露哪些接口,一般将接口函数放在头文件中(.h),将具体的实现放在.c文件中,用户并不需要知道线程池实现的细节,所以这里就给用户暴露必要的三个接口,分别是创建线程池、销毁线程池和提交任务。

thrdpool_t *thrdpool_create(int thrd_count);
void thrdpool_terminate(thrdpool_t * pool);
typedef void (*handler_pt)(void * /* ctx */);
int thrdpool_post(thrdpool_t *pool, handler_pt func, void *arg);

⛅3.2 结构体设计

这里一共需要三个结构体,分别是线程池、任务队列和任务,定义如下:

struct thrdpool_s { // 线程池
    task_queue_t *task_queue; // 阻塞队列,让线程状态变更的操作应该封装在队列内部
    atomic_int quit; // 是否退出的标记,原子变量
    int thrd_count; // 线程数量
    pthread_t *threads; // 线程 
};

这里要特别注意职责的划分,有不少开源线程池的实现把互斥锁这些本应该放在队列的变量放在了线程池中,应该要让线程状态变更的操作封装在队列内部,线程池结构体中包含了指向任务队列的指针,队列结构体定义如下:

typedef struct task_queue_s { // 队列
    void *head;
    void **tail; 
    int block;
    spinlock_t lock; // 可以单独加锁
    pthread_mutex_t mutex; // 也可以复用互斥锁
    pthread_cond_t cond;
} task_queue_t;

任务结构体定义如下:

typedef struct task_s { // 任务
    void *next;
    handler_pt func;
    void *arg;
} task_t;

总体结构体的关系如图所示:

队列的head指针指向队头任务,tail指针指向队尾任务的next指针,要注意这里void **(指向指针的指针)的用法,不少优秀的开源代码都喜欢这样设计,可以简化后续链表操作。其中生产者从队列头部添加任务,消费者从队列尾部取出任务。

⛅3.3 关键函数书写

🌥️3.3.1 __threads_create & __threads_terminate

首先来看看创建线程的代码,像这种复杂资源创建要使用对称式的接口设计,有创建(__threads_create )就有销毁(__threads_terminate),并且建议采用回滚式的代码书写方式,先假设成功,然后回滚,代码呈现如下图的结构:

static int __threads_create(thrdpool_t *pool, size_t thrd_count) {
    pthread_attr_t attr;
	int ret;
    ret = pthread_attr_init(&attr);
    if (ret == 0) { // 1、假设pthread_attr_init成功
        pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thrd_count);
        if (pool->threads) { // 2、假设malloc成功
            int i = 0;
            for (; i < thrd_count; i++) {
                if (pthread_create(&pool->threads[i], &attr, __thrdpool_worker, pool) != 0) {
                    break;
                }
            }
            pool->thrd_count = i;
            pthread_attr_destroy(&attr);
            if (i == thrd_count) // 3、假设pthread_create成功
                return 0; // 最终成功返回,下面都是回滚代码
            __threads_terminate(pool); // 3、回滚
            free(pool->threads); // 2、回滚
        }
        ret = -1; // 1、回滚
    }
    return ret; 
}

数字对应的地方为代码书写顺序,可以看到回滚式代码书写还是非常优雅的。

销毁线程的代码如下:

static void __threads_terminate(thrdpool_t * pool) {
    atomic_store(&pool->quit, 1);
    __nonblock(pool->task_queue);
    int i;
    for (i=0; i<pool->thrd_count; i++) {
        pthread_join(pool->threads[i], NULL);
    }
}
🌥️3.3.2 __taskqueue_create

然后来看看任务队列的创建函数,同样使用回滚式代码书写方式

static task_queue_t *__taskqueue_create() {
    int ret;
    task_queue_t *queue = (task_queue_t *)malloc(sizeof(task_queue_t));
    if (queue) { // 1、假设malloc成功
        ret = pthread_mutex_init(&queue->mutex, NULL);
        if (ret == 0) { // 2、假设pthread_mutex_init成功
            ret = pthread_cond_init(&queue->cond, NULL);
            if (ret == 0) { // 3、假设pthread_cond_init成功
                spinlock_init(&queue->lock);
                queue->head = NULL;
                queue->tail = &queue->head;
                queue->block = 1;
                return queue; // 成功返回
            }
            pthread_mutex_destroy(&queue->mutex); // 2、回滚  
        }
        free(queue); // 1、回滚
    }
    return NULL;
}
🌥️3.3.3 __add_task & __pop_task

最后来看看添加任务和取任务的函数,这两个流程逻辑函数的书写不适合用回滚式书写方式,适合用防御式代码书写方式,即先把不满足条件的 return 掉,然后写满足条件的,这两个操作函数需要有较好的链表算法能力,并且在这里会体会到之前 void **tail 的好处,__add_task 函数如下:

static inline void __add_task(task_queue_t *queue, void *task) {
    // 不限定任务类型,只要该任务的结构起始内存是一个用于链接下一个节点的指针
    void **link = (void**)task;
    *link = NULL; // task->next = NULL; 不限定链接方式 

    spinlock_lock(&queue->lock);
    *queue->tail /* 等价于 queue->tail->next */ = link;
    queue->tail = link;
    spinlock_unlock(&queue->lock);
    pthread_cond_signal(&queue->cond);
}

可以看到使用了指针的指针之后,代码清爽简洁,然后没有用 void **tail,书写的代码量会变大,并且这种方式不会用到 next 指针,不限定链接方式,适用性更强。

__pop_task 函数如下:

static inline void * __pop_task(task_queue_t *queue) {
    spinlock_lock(&queue->lock);
    if (queue->head == NULL) {
        spinlock_unlock(&queue->lock);
        return NULL;
    }
    task_t *task;
    task = queue->head;

    void **link = (void**)task;
    queue->head = *link;

    if (queue->head == NULL) {
        queue->tail = &queue->head;
    }
    spinlock_unlock(&queue->lock);
    return task;
}

🪐4. 结束语

本文较为简单的阐述了线程池的实现方法,后续还会完善代码,并用gtest做完整的测试,本人目前还是个在校生,还比较小白,也刚刚开始写 CSDN 博客不久,可能写的也不是很好,如果有任何疑问或者发现我有哪里写的不对的地方,欢迎大家留言告诉我!我都会一一改正的。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小占!

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

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

相关文章

多种现货黄金的简介 第三种你要特别注意

近期黄金的价格全线上涨&#xff0c;虽然作为商品&#xff0c;一般人看到其价格上涨&#xff0c;购买它的欲望就下跌了。但是作为一种投资品种&#xff0c;在近期黄金价格的上涨以及未来美联储货币政策的影响下&#xff0c;他的投资潜力是巨大的。但市场中有那么多的黄金理财产…

JavaScript编程精粹:语法、函数与对象

JavaScript编程精粹&#xff1a;语法、函数与对象 一 . JavaScript 介绍1.1 应用场景1.2 JavaScript 介绍1.3 JavaScript 组成部分 二 . JavaScript 引入方式2.1 内部脚本2.2 外部脚本 三 . JavaScript 基础语法3.1 输出语句3.2 变量3.3 数据类型3.3.1 number 类型3.3.2 string…

W11电脑无法找到音频输出设备,所有声音设备都是空的的解决方法

搜索&#xff1a;设备管理器 然后下滑查看。 在查看的时候&#xff0c;发现OED上面有一个黄色的感叹号 右键更新之后即可&#xff0c;笔者是选择的本地更新。 之后右下角的声音设备就恢复正常了

局域网设备自动发现常用方法

文章目录 需求实现方法ARP (Address Resolution Protocol)Ping ip的流程抓包如下代码实现 mDNS 对比测试Avahi 介绍Avahi 安装Avahi 使用测试代码 需求 局域网设备自动发现是软件开发中的一个常见且重要的需求&#xff0c;它简化了设备间的协作机制&#xff0c;降低了软件各模…

自动化测试之Selenium的使用

06、Selenium的使用 Selenium 是一个自动化测试工具&#xff0c;利用它可以驱动浏览器执行特定的动作&#xff0c;如点击、下拉等操作&#xff0c;同时还可以获取浏览器当前呈现的页面的源代码&#xff0c;做到可见即可爬。对于一些 JavaScript 动态渲染的页面来说&#xff0c…

CleanMyMac X2024破解版mac电脑清理工具

今天&#xff0c;我要跟大家分享一个让我彻底告别电脑卡顿的秘密武器——CleanMyMac X。这不仅仅是一款普通的清理工具&#xff0c;它是你电脑的私人健身教练&#xff0c;让电脑焕发青春活力&#xff01; CleanMyMac直装官方版下载地址&#xff1a; http://wm.makeding.com/i…

公共英语三级考试时间安排

公共英语三级考试时间安排

STM32学习笔记(二、初识stm32单片机)

一、stm32的含义是什么&#xff1f; 首先stm32是意法半导体公司&#xff08;ST&#xff09;使用ARM公司的Cortex-M为核心生产的32位的单片机。 其中&#xff0c;ST---意法半导体公司&#xff0c;即SOC厂商。 M---为Microelectronics的缩写&#xff0c;即微型处理器。 32---表示…

erp系统有哪些品牌?盘点2024年值得关注的十个爆款erp品牌!

本文将盘点主流的erp品牌&#xff0c;为企业选型提供参考&#xff01; ERP系统是企业数字化转型的基石&#xff0c;选择一款适合企业自身需求的ERP系统&#xff0c;不仅能够显著提升企业的运营效率&#xff0c;还能为企业的长期发展奠定坚实的基础。 随着市场竞争的日益激烈&a…

比较:#define,const,typedef

目录 1. #define 2. const 3. typedef #define 用于文本替换&#xff0c;没有类型检查&#xff0c;适用于定义简单的常量或宏。const 是类型安全的编译时常量&#xff0c;适合定义不可变的变量&#xff0c;有范围控制。typedef 用于定义类型的别名&#xff0c;简化代码复杂性…

ubuntu20.04 Qt6引用dcmtk库实现dicom文件读取和字符集转换

1 环境问题 安装完Qt6&#xff0c;新建Qt/QtQuick CMake工程编译出现如下错误: Found package configuration file: Qt6Config.cmake but it set Qt6 FOUND to FALSE so package "Qt6" is considered to be NOT FOUND. 原因&#xff1a; 这是因为系统中缺少OpenG…

IOS Siri和快捷指令打开app

使用场景 需要Siri打开应用或者在自定义快捷指令打开应用&#xff0c;并携带内容进入应用。 1.创建Intents文件 1.1 依次点开File->New->File 1.2 搜索intent关键字找到 SiriKit Intent Definition File文件 1.3 找到刚才创建的Intent文件&#xff0c;点击然后New Inte…

Vue2+JS项目升级为Vue3+TS之jquery的maphilight引入项目(附使用)

本人由于想提升自己的项目开发能力&#xff0c;所以将就项目的vue2JavaScriptwebpack的旧技术栈升级为vue3typescriptvite的技术栈&#xff0c;所以遇到很多坑&#xff0c;以下是maphilight的解决方法。 众所周知jquery是基于JavaScript进行开发&#xff0c;但是已有typescript…

钉钉虚拟位置打卡

我用蓝奏浏览器分享了[base_r_sign], 下载链接:https://wwp.lanzoup.com/i5NK526t7u9e 提取码 : 7wib, 你可以不限速下载哦\n\n通过百度网盘分享的文件&#xff1a;彤彤240724…\n链接:https://pan.baidu.com/s/1x_xhRQDopvQBAg-nWUNf4Q?pwd6666\n提取码:6666 下载好以后先配置…

超全!进销存系统排名前列的厂商有哪些?

本文将为大家盘点10款主流的进销存系统&#xff0c;为企业选型提供参考&#xff01; 进销存系统&#xff08;Inventory Management System&#xff09;&#xff0c;也称为物料管理系统或存货管理系统&#xff0c;是指企业为有效管理和控制进出货物的流动&#xff0c;准确记录库…

css——网格布局

名词解释 div{$}*9tab键&#xff0c;快捷生成 记首字母gtc 网格布局&#xff1a;display: grid; grid-template-columns: 100px 100px 100px; grid-template-rows: 100px 100px 100px; &#xff08;父元素&#xff09; <!DOCTYPE html> <html lang&q…

虚幻引擎Gameplay探索 Actor 之间的高效通信与交互技巧二

Actor通信介绍 在虚幻引擎中&#xff0c;Actor 是游戏世界中的基本构建块&#xff0c;类似于 Unity 中的 GameObject。Actor 通信是指不同 Actor 之间如何相互交互和传递信息&#xff0c;这在构建复杂的游戏逻辑时至关重要。以下是对 Actor 通信的详细介绍。 Actor通信方法表…

Redis复习笔记整理(没有人会有耐心看完包括我自己)

目录 1、Redis简介 1.1 补充数据类型Stream 1.2 Redis底层数据结构 1.3 Redis为什么快 1.4 持久化机制* 1.4.1 RDB持久化 bgsave执行流程 如何保证数据一致性 快照操作期间服务崩溃 RDB优缺点 1.4.2 AOF持久化 为什么采用写后日志 如何实现AOF 什么是AOF重写 AO…

Vue组件:动态组件、缓存组件、异步组件

1、动态组件 Vue.js 提供了对动态组件的支持。在使用动态组件时&#xff0c;多个组件使用同一挂载点&#xff0c;根据条件在不同组件之间进行动态切换。动态组件通过使用 Vue.js 中的 <component>元素&#xff0c;动态绑定到该元素的 is 属性&#xff0c;根据 is 属性的…

通过LDAP方式使用windows域认证

关于Windows的域认证, 网上大多都再介绍原理啥的, 但是对于从来没有做过.net的我来说, 和看天书一样. 我把我做的demo提供出来共大家参考. 需要参考的文章,参照如下 Windows下LDAP服务安装与使用_windows ldap-CSDN博客 OpenLDAP管理工具之LDAP Admin-腾讯云开发者社区-腾讯云…