再手写线程池以及性能分析

news2024/9/17 8:34:07

文章目录

  • 前言
  • 一、为什么要用线程池
      • 线程池的作用
      • 线程池的使用场景
  • 二、线程池的构成以及相关API的实现
      • 线程池中的相关变量类型
      • 线程池的创建
      • 任务线程实现
      • 获取任务
      • 线程池的销毁
      • 线程池的使用


前言

以前写的线程池文章请参考:线程池的简单实现
本次文章是对线程池的再次学习,也是深度学习哈哈哈。
毕竟人都有遗忘性,常回头看看挺好的哈哈。


一、为什么要用线程池

  1. 某类任务特别耗时,严重影响该线程处理其他任务
  2. 在其他线程中异步执行该任务
  3. 线程资源的开销与CPU核心之间平衡选择

线程池的作用

复用线程资源;
减少线程创建和销毁的开销;
可异步处理生产者线程的任务;
减少了多个任务(不是一个任务)的执行时间;

线程池的使用场景

以日志为例,在写日志loginfo(“xxx”),与日志落盘,是两码事,它们两之间应该是异步的。那么异步解耦就是将日志当作一个任务task,将这个任务抛给线程池去处理,由线程池去负责日志落盘。对于应用程序而言,就可以提升落盘的效率。
以nginx为例,一秒几万的请求,速度很快。如果在其中加一个日志,那么qps一下子就掉下来了,因为每请求一次就需要落盘一次,那么整个服务器的性能就下降。我们可以引入一个线程池,把日志这个任务抛给线程池,对于主循环来说,就只抛任务即可,这样就可以大大提升主线程的效率。这就是线程池异步解耦的作用
不仅仅是日志落盘,还有很多地方都可以用线程池,比较耗时的操作如数据库操作,io处理等,都可以用线程池。
线程池有必要将线程与cpu做亲和性吗? 在注重cpu处理能力的时候,可以做黏合;如果注重的是异步解耦,那么这里更加注重的是任务,没必要将线程和cpu做绑定。

二、线程池的构成以及相关API的实现

在这里插入图片描述
首先我们先来思考下,线程池应该有哪些构成。根据构成来让我们进行代码落实。

首先,我们应该理解的是。这应该是一个生产者和消费者模型。而线程池在其中充当消费者

在真正实现需求功能的时候。我们首先主程序(生产者)肯定会有比较耗时的需求(任务)需要处理,把任务放到任务队列。woker线程从任务队列中取出任务,执行任务。

接下来先来实现下线程池中相关API。然后在通过实验用例来使用它
在这里插入图片描述
首先我们先定义先线程池中的相关变量类型

线程池中的相关变量类型

//任务
typedef struct task_t {
    handler_pt func; //任务的执行函数
    void * arg;  //任务上下文
} task_t;

typedef struct task_queue_t {
    uint32_t head;
    uint32_t tail;
    uint32_t count; //队列的大小
    task_t *queue;  //数组可以一次性分配,避免频繁的malloc(任务队列)
} task_queue_t;

struct thread_pool_t {
    pthread_mutex_t mutex;
    pthread_cond_t condition;
    pthread_t *threads;
    task_queue_t task_queue;

    int closed;  //线程池退出的标记,和连接池中的那个bool类型的一样
    int started; // 当前运行的线程数

    int thrd_count;  //线程的数量
    int queue_size;  //队列的最大值
};

线程池的创建

thread_pool_t *
thread_pool_create(int thrd_count, int queue_size) {
    thread_pool_t *pool;

    if (thrd_count <= 0 || queue_size <= 0) {
        return NULL;
    }

    pool = (thread_pool_t*) malloc(sizeof(*pool));
    if (pool == NULL) {
        return NULL;
    }

    pool->thrd_count = 0; //不能确保所有的线程都启动起来,所以谁启动++就行
    pool->queue_size = queue_size;
    pool->task_queue.head = 0;
    pool->task_queue.tail = 0;
    pool->task_queue.count = 0;

    pool->started = pool->closed = 0;

    pool->task_queue.queue = (task_t*)malloc(sizeof(task_t)*queue_size);
    if (pool->task_queue.queue == NULL) {
        // TODO: free pool
        return NULL;
    }

    //数组
    pool->threads = (pthread_t*) malloc(sizeof(pthread_t) * thrd_count);
    if (pool->threads == NULL) {
        // TODO: free pool
        return NULL;
    }

    int i = 0;
    for (; i < thrd_count; i++) {
        if (pthread_create(&(pool->threads[i]), NULL, thread_worker, (void*)pool) != 0) {
            // TODO: free pool
            return NULL;
        }
        pool->thrd_count++;
        pool->started++;
    }
    return pool;
}

创建线程的对象。并根据传来的参数来确定存储任务队列的长度以及可以工作的线程数量并进行创建。

任务线程实现

static void *
thread_worker(void *thrd_pool) {
    thread_pool_t *pool = (thread_pool_t*)thrd_pool;
    task_queue_t *que;
    task_t task;
    //不断的取出任务和执行任务
    for (;;) {
        pthread_mutex_lock(&(pool->mutex));
        que = &pool->task_queue;
        /*用while循环的原因(为什么解除锁定之后还需要循环判断下条件),原因如下:*/
        // 虚假唤醒   linux  pthread_cond_signal(以前的版本可能会唤醒两个线程),被唤醒后不一定有任务,虚假唤醒
        // linux 可能被信号唤醒
        // 业务逻辑不严谨,被其他线程抢了该任务(假设有两个线程,其中一个是活跃的,另一个是休眠的。假设有任务到来,活跃的线程取了任务,但是也唤醒了
        // 那个休眠的线程,如果此处不是while,那它就会向下执行报错)
        while (que->count == 0 && pool->closed == 0) { //任务为空,且没有退出线程池,此时就不断的休眠
            // pthread_mutex_unlock(&(pool->mutex))
            // 阻塞在 condition
            // ===================================
            // 解除阻塞
            // pthread_mutex_lock(&(pool->mutex));
            pthread_cond_wait(&(pool->condition), &(pool->mutex));
        }
        if (pool->closed == 1) break;
        task = que->queue[que->head];
        que->head = (que->head + 1) % pool->queue_size;
        que->count--;
        pthread_mutex_unlock(&(pool->mutex));
        (*(task.func))(task.arg);  //执行的任务

    pool->started--;  //执行完一个当前工作的线程数就 减一
    pthread_mutex_unlock(&(pool->mutex));
    pthread_exit(NULL);
    return NULL;
}

这里只讲说两点,别的都比较简单,不进行讲述。

第一点:
      此处的while (que->count == 0 && pool->closed == 0)
     为什么任务队列为空且线程池未退出时,进行睡眠的判定用的是while循环而不是if啊。
     即为什么解除锁定之后还需要循环判断下条件
     原因如下:
     	 1. 避免虚假唤醒   linux  pthread_cond_signal(以前的版本可能会唤醒两个线程),被唤醒后不一定有任务,虚假唤醒
     	 2. linux 可能被信号唤醒
     	 3. 业务逻辑不严谨,被其他线程抢了该任务(假设有两个线程,其中一个是活跃的,另一个是休眠的。假设有任务到来,活跃的线程取了任务,但是也唤醒了那个休眠的线程,如果此处不是while,那它就会向下执行报错)


第二点:
     执行任务的回调函数 (*(task.func))(task.arg);  //执行的任务
     请看如下代码实例便可理解:
#include <stdio.h>

typedef char (*PTR_TO_ARR)[30];
typedef int (*PTR_TO_FUNC)(int, int);

int max(int a, int b){
    return a>b ? a : b;
}

char str[3][30] = {
    "http://c.biancheng.net",
    "C-Language"
};

int main(){
    PTR_TO_ARR parr = str;
    PTR_TO_FUNC pfunc = max;
    int i;
   
    printf("max: %d\n", (*pfunc)(10, 20));
    for(i=0; i<3; i++){
        printf("str[%d]: %s\n", i, *(parr+i));
    }

    return 0;
}

获取任务

//发布任务,生产者线程调用它
int
thread_pool_post(thread_pool_t *pool, handler_pt func, void *arg) {
    if (pool == NULL || func == NULL) {
        return -1;
    }

    task_queue_t *task_queue = &(pool->task_queue);

    if (pthread_mutex_lock(&(pool->mutex)) != 0) {
        return -2;
    }

    if (pool->closed) {
        pthread_mutex_unlock(&(pool->mutex));
        return -3;
    }

    if (task_queue->count == pool->queue_size) {
        pthread_mutex_unlock(&(pool->mutex));
        return -4;
    }

    task_queue->queue[task_queue->tail].func = func;
    task_queue->queue[task_queue->tail].arg = arg;
    task_queue->tail = (task_queue->tail + 1) % pool->queue_size;
    task_queue->count++;

    if (pthread_cond_signal(&(pool->condition)) != 0) {
        pthread_mutex_unlock(&(pool->mutex));
        return -5;
    }
    pthread_mutex_unlock(&(pool->mutex));
    return 0;
}

这里的主要逻辑其实就是生产者(主线程)调用它发布任务,存储到任务队列当中去,并且唤醒一个工作线程,取任务队列中一个任务来进行执行。

线程池的销毁

static void 
thread_pool_free(thread_pool_t *pool) {
    if (pool == NULL || pool->started > 0) {
        return;
    }

    if (pool->threads) {
        free(pool->threads);
        pool->threads = NULL;

        pthread_mutex_lock(&(pool->mutex));
        pthread_mutex_destroy(&pool->mutex);
        pthread_cond_destroy(&pool->condition);
    }

    if (pool->task_queue.queue) {
        free(pool->task_queue.queue);
        pool->task_queue.queue = NULL;
    }
    free(pool);
}

int
wait_all_done(thread_pool_t *pool) {
    int i, ret=0;
    for (i=0; i < pool->thrd_count; i++) {
        if (pthread_join(pool->threads[i], NULL) != 0) {
            ret=1;
        }
    }
    return ret;
}


//清空任务
int
thread_pool_destroy(thread_pool_t *pool) {
    if (pool == NULL) {
        return -1;
    }

    if (pthread_mutex_lock(&(pool->mutex)) != 0) {
        return -2;
    }

    if (pool->closed) {
        thread_pool_free(pool);
        return -3;
    }

    pool->closed = 1;

    if (pthread_cond_broadcast(&(pool->condition)) != 0 || 
            pthread_mutex_unlock(&(pool->mutex)) != 0) {
        thread_pool_free(pool);
        return -4;
    }

    wait_all_done(pool);

    thread_pool_free(pool);
    return 0;
}

这里再来大致总结下,线程的整个实现流程;
首先,是有一个线程组,即有多个工作线程。
即在有一个队列,来存储任务(此线程池实现的比较简单,你也可以定义两个队列,一个存储任务的队列,一个存储执行任务的队列)。
当有任务到来,即调用获取任务的函数,此时把任务存储到一个队列中,然后唤醒一个线程,处理队列中的任务。
当全部程序运行结束,退出线程池时,释放分配的空间,销毁线程即可。ok到此结束,实现完成哈哈哈哈。

接下来看一个调用线程池的小程序:

线程池的使用

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

#include "thrd_pool.h"

int nums = 0;
int done = 0;

pthread_mutex_t lock;

void do_task(void *arg) {
    usleep(10000);
    pthread_mutex_lock(&lock);
    done++;
    printf("doing %d task\n", done);
    pthread_mutex_unlock(&lock);
}

int main(int argc, char **argv) {
    int threads = 8;
    int queue_size = 256;


    thread_pool_t *pool = thread_pool_create(threads, queue_size);
    if (pool == NULL) {
        printf("thread pool create error!\n");
        return 1;
    }

    while (thread_pool_post(pool, &do_task, NULL) == 0) {
        pthread_mutex_lock(&lock);
        nums++;
        pthread_mutex_unlock(&lock);
    }

    //printf("add %d tasks\n", nums);
    
    wait_all_done(pool);

    printf("did %d tasks\n", done);
    thread_pool_destroy(pool);
    return 0;
}

在这里插入图片描述
到此完结。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

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

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

相关文章

使用Egg调用mysql实现增删改查接口操作

市场上数据库可视化工具很多&#xff0c;笔者更多时间使用的是 Navicat ,但是因为它是收费的&#xff0c;一些伙伴可能没发使用&#xff0c;所以笔者在这里推荐一款 工具 名为&#xff1a; DBevaer&#xff0c;它所展现出来的能力&#xff0c;与navicat 类似&#xff0c;并且还…

玩RTOS这么久,一问原子操作,蒙了~

已剪辑自: https://mp.weixin.qq.com/s/kvxcOHT-xHtMAjQqJu7Y2g [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3f9Rrei-1668695258073)(https://res.wx.qq.com/mmbizappmsg/zh_CN/htmledition/js/images/icon/audio/icon_qqmusic_source6201b5.sv…

代码随想录——钥匙和房间(图论)

题目 有 N 个房间&#xff0c;开始时你位于 0 号房间。每个房间有不同的号码&#xff1a;0&#xff0c;1&#xff0c;2&#xff0c;…&#xff0c;N-1&#xff0c;并且房间里可能有一些钥匙能使你进入下一个房间。 在形式上&#xff0c;对于每个房间 i 都有一个钥匙列表 rooms[…

tft lcd spi 驱动

tft lcd spi 驱动参考驱动uc1701SPI设备树配置背光控制IO设备树配置SPI控制引脚配置参考驱动uc1701 linux-4.1.15-imx6ul/drivers/staging/fbtftSPI设备树配置 根据原理图修改对应引脚 linux-3.10/arch/arm/boot/dts/sun8iw11p1-pinctrl.dtsi 蔽掉引脚冲突功能 linux-3.10/ar…

如何注册公司网站?【注册公司网站】

公司网站是很多公司的必备品&#xff0c;哪怕公司并不依赖线上业务&#xff0c;也会有自己的公司网站。随着互联网的发展成熟&#xff0c;其实现在注册公司网站基本上都是比较简单的&#xff0c;那么如何注册公司网站呢&#xff1f;下面给大家说一说。 一、注册公司网站前准备…

英伟达发布526.98 WHQL 显卡驱动,支持RTX 4080,三款即将上线游戏

11月16日&#xff0c;英伟达发布了526.98 WHQL 新驱动&#xff0c;支持最新发布的RTX 4080显卡。 新驱动为《蜘蛛侠:迈尔斯莫拉莱斯》、《战锤40k:暗潮》和《魔兽世界&#xff1a;巨龙时代》提供支持。此外&#xff0c;还支持《怪物猎人崛起》的DLAA更新和WRC世代-国际汽联WRC…

【FFmpeg】ffmpeg+nginx-rtmp实现视频流转发

1.应用场景 目前的摄像头厂家能提供出来的视频流格式有限&#xff0c;且chrome已经禁止了对flash的支持&#xff0c;导致像硬盘录像机这种只能提供rtsp格式流地址的摄像头无法接入Web应用&#xff0c;所以不得不对视频的流地址进行分发&#xff0c;通过代码对流地址中的数据进…

数仓开发之DWD层(三)

&#xff08;附&#xff1a;由于篇幅原因&#xff0c;这里就不在展示代码了&#xff0c;直接告诉大家思路&#xff09; 目录 五&#xff1a;交易域订单预处理表 5.1 主要任务 5.2 思路分析 5.3 图解 六&#xff1a;交易域下单事务事实表 6.1 主要任务&#xff1a; 6.2 …

泰克AFG31152函数信号发生器Tektronix AFG31152介绍

泰克AFG31152函数信号发生器Tektronix AFG31152 AFG31152 是 Tektronix 的 50 MHz 任意函数发生器。 特征&#xff1a; 的 InstaView™ 技术使工程师能够实时查看被测设备 (DUT) 的实际波形&#xff0c;而无需示波器和探头&#xff0c;从而消除了由阻抗不匹配引起的不确定性…

linux C.UTF-8和en-US.UTF-8语言环境有什么区别?(中文乱码问题)locale命令 centos、ubuntu修改编码集(没搞定!)

文章目录问题背景查看C.UTF-8和en-US.UTF-8语言环境差异关于locale修改编码集centos&#xff08;没验证&#xff09;ubuntu问题背景 我在ubuntu16.04虚拟机和英伟达盒子ubuntu18.04上分别部署了ngrest服务 用postman请求&#xff0c;ubuntu16.04虚拟机返回的中文是乱码&#…

软件测试职场焦虑之我对35岁危机的看法

目录 前言 如何理解35岁失业&#xff1f; 本质的原因是什么&#xff1f; 应对35岁失业的策略 总结 前言 这几年关于“35岁失业”的讨论甚嚣尘上&#xff0c;特别是进入疫情时代&#xff0c;身边也越来越多的人开始讨论这个话题。 一方面是疫情带来的巨大变革&#xff0c;…

SpringCloud——微服务介绍+系统架构

目录 1. 微服务介绍 2.系统架构演变 3. 单体应用架构 3.1优点&#xff1a; * 项目架构简单&#xff0c;小型项目的话&#xff0c; 开发成本低* 项目部署在一个节点上&#xff0c; 维护方便 3.2缺点&#xff1a; * 全部功能集成在一个工程中&#xff0c;对于大型项目来讲…

Data Catalog3.0:Modern Metadata for the Modern Data Stack

从2020年开始&#xff0c;在数据领域中&#xff0c;有一个比较流行的术语&#xff1a;The Modern Data Stack(现代数据堆栈)&#xff0c;简单理解就是汇集了处理海量数据的最佳工具集。这包括在最好的工具上建立数据基础设施&#xff0c;如用于数据仓库的Snowflake&#xff0c;…

传奇外网架设常见的问题及解决办法-传奇创建人物失败/不开门/PAK显示密码错误/脚本错误

传奇外网架设常见的问题及解决办法-传奇创建人物失败/不开门/PAK显示密码错误/脚本错误 在架设传奇的时候是否有遇到无法创建人物、pak密码错误等一系列情况呢&#xff1f;咱们都知道是架设不对的问题&#xff0c;但是具体是哪部分的问题&#xff0c;很多同学都不清楚&#xff…

JDK与cglib动态代理

JDK动态代理 接口类 public interface Subject {void doSomething(); }接口实现 public class RealSubject implements Subject {Overridepublic void doSomething() {System.out.println("RealSubject do something");} }InvocationHandler类 package daili;imp…

随笔记:计算机基础及进制计数法

随笔记&#xff1a;计算机基础及进制计数法 记录一下最近接触的基本的概念&#xff0c;便于想看的时候随时来翻看一下。 基础理论还是比较重要滴&#xff0c;基础理论还是比较重要滴&#xff0c;基础理论还是比较重要滴 现代计算机是用 0 和 1 来表示信息的&#xff0c;使用的…

6、行为型模式-责任链模式

一、责任链模式描述 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;为请求创建了一个接收者对象的链。这种模式给予请求的类型&#xff0c;对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。 在这种模式中&#xff0c;通常每个接收者…

Unity AVPro 使用

AVPro 感觉我写的没多大用处&#xff0c;后面看看文档再完善一些。目前的东西是可以满足一些简单的需求的。 说明 标题名称内容Unity版本Unity 2021 .1.18f1c1AVPro 版本AVPro Video - Ultra Edition 2.5.6IDEVS2022系统版本Win 10 1909撰写日期2022 11月15日晚 需要注意的地…

防孤岛保护装置在光伏行业的应用

安科瑞 华楠 应用场景 防孤岛原理&#xff1a;防孤岛保护装置检测到并网点有逆功率、频率突变、 等异常数据时&#xff0c;即发生孤岛现象时&#xff0c;装置可配合断路器快速切除并网点&#xff0c;使本站与电网侧快速脱离&#xff0c;保证整个电站和相关维护人员的生命安全 …

Go:Signal信号量的简介与实践(优雅的退出)

文章目录简介一、kill与kill9的区别二、实践&#xff1a;优雅的退出小结简介 go中的信号量 有些信号名对应着3个信号值&#xff0c;这是因为这些信号值与平台相关&#xff0c;SIGKILL和SIGSTOP这两个信号既不能被应用程序捕获&#xff0c;也不能被操作系统阻塞或忽略。 一、…