Linux C/C++ 开发(学习笔记五):线程池

简介: Linux C/C++ 开发(学习笔记五):线程池

一、线程池的由来和组成

如果涉及到十万个线程,根本没办法开这么多

如果一个poxis线程占8M内存,那么16G内存也只能有2048个线程,根本没法满足十万线程的需求。

线程池

1.避免线程太多,使得内存耗尽

2.避免创建与销毁线程的代价

3.任务与执行分离

所以需要 任务与执行 分离。

一个例子,对于营业厅来说,办理业务的人是任务队列,柜员 是执行队列

因此需要一个组件(也就是线程池),使得任务队列和执行队列,正常有序地工作

线程池组成

  1. 任务队列
  2. 执行队列
  3. 管理组件----》锁

线程池API

1.create/init

2.push_task

3.destroy/deinit

二、实现一个基本的线程池

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
***********链表操作的宏定义**************
//头部插入
#define LIST_INSERT(item,list) do{  \
    item->prev=NULL;             \
    item->next=list;                \
    if((list)!=NULL) (list)->prev=item; \
    (list)=item;\
}while(0)
//删除节点(如果指只剩下一个节点的时候要特殊考虑)
#define LIST_REMOVE(item,list) do{    \
    if(item->prev!=NULL) item->prev->next=item->next; \
    if(item->next!=NULL) item->next->prev=item->prev; \
    if(list==item) list=item->next; \
    item->prev=item->next=NULL; \
}while(0)
********************end******************
struct nTask{
    void (*task_func)(nTask* task);//函数指针
    void *user_data;//处理的数据(要传入到上面的函数中去)
    //用双向链表来表示任务队列
    nTask *prev; 
    nTask *next;
};
struct nWorker{
    pthread_t threadId;
    struct nManager *manager;//有利于worker可以更改线程池
    int terminate;//是否中止
    nWorker *prev;
    nWorker *next;
};
//管理组件,也就是线程池
typedef struct nManager{
    nTask *tasks;//任务队列的首节点
    nWorker *workers;//执行队列的首节点
    pthread_mutex_t mutex;//互斥锁
    pthread_cond_t cond;//条件变量 (比如没有来任务的时候,worker需要做什么呢,所以需要这个变量)
}ThreadPool;
//callback!=task     
static void* nThreadPoolCallBack(void* arg){
    //worker一直在判断任务队列里面有没有任务(等待任务到来),一旦有任务,就会从任务队列取出任务来。
    nWorker* worker=(nWorker*)arg;
    //1.判断有无等待的任务(任务队列),2.如果有任务就把任务分配给这个worker,3.执行任务
    while(true){//当任务不为空的时候,就会往下走,不然会一直等着任务来。
        pthread_mutex_lock(&worker->manager->mutex);//上锁
        while(worker->manager->tasks==NULL){
            if(worker->terminate) break;//中断(2处地方可以:1.等待的时候 2.有任务,还未分配任务的时候)
            pthread_cond_wait(&worker->manager->cond,&worker->manager->mutex);//等待
        }
        if(worker->terminate){
            pthread_mutex_unlock(&worker->manager->mutex);//中断的话记得解锁,不然会造成死锁
            break;
        } 
        //如果任务不为空,那么将任务队列,首个任务取出来
        nTask* task=worker->manager->tasks;
        if(task!=NULL){
            LIST_REMOVE(task,worker->manager->tasks);
        }
        pthread_mutex_unlock(&worker->manager->mutex);//解锁
    if(task==NULL) continue;//如果terminate,会出现task==NULL的情况
        task->task_func(task);//执行任务
    }
    delete worker;//由于存在线程销毁,可以到这一步
}
//API
int nThreadPoolCreate(ThreadPool* pool,int numWorkers){
    if(pool==NULL) return -1;
    if(numWorkers<1) numWorkers=1;//如果初始化 worker小于1,就默认给他1,员工小于1的话,这个线程池是没法工作的
    memset(pool,0,sizeof(ThreadPool));
    //初始化 条件变量
    pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
    pool->cond=blank_cond;
    //初始化 互斥锁
    pthread_mutex_init(&pool->mutex,NULL);
    //初始化work
    for(int i=0;i<numWorkers;i++){
        nWorker *worker=(struct nWorker*)malloc(sizeof(struct nWorker));
        if(worker==NULL){//如果创建失败
            perror("malloc");
            return -2;
        }
        memset(worker,0,sizeof(nWorker));//将堆上创建的数据全部置0
        worker->manager=pool;//方便后续worker去操作线程池
        int ret=pthread_create(&worker->threadId,NULL,nThreadPoolCallBack,worker);//每个worker执行的工作是一样的,但是任务是不一样的
        if(ret){//posixthread有个特点,创建失败会返回非0
            perror("pthread_create");
            delete worker;
            return -3;
        }
        LIST_INSERT(worker,pool->workers);//把worker插入到 执行队列中
    }
    //success
    return 0;
}
//API
int nThreadPoolDestory(ThreadPool* pool,int numWorkers){
    nWorker* worker=NULL;
    for(worker=pool->workers;worker!=NULL;worker=worker->next){
        worker->terminate=1;
    }
    //防止死锁
    pthread_mutex_lock(&pool->mutex); //广播的时候和等待的时候用的是同一把锁
    pool->workers=NULL;
    pool->tasks=NULL;
    pthread_cond_broadcast(&pool->cond);//唤醒所有等待这个条件的
    pthread_mutex_unlock(&pool->mutex);
    return 0;   
}
//API
int nThreadPoolPushTask(ThreadPool* pool,nTask* task){
    pthread_mutex_lock(&pool->mutex);
    LIST_INSERT(task,pool->tasks);
    pthread_cond_signal(&pool->cond);//唤醒等待队列中的一个
    pthread_mutex_unlock(&pool->mutex);
}
//
// sdk --> debug thread pool
#if 1
#define THREADPOOL_INIT_COUNT 20
#define TASK_INIT_SIZE      1000
void task_entry(struct nTask *task) { //type 
  //struct nTask *task = (struct nTask*)task;
  int idx = *(int *)task->user_data;
  printf("idx: %d\n", idx);
  free(task->user_data);
  free(task);
}
int main(void) {
  ThreadPool pool = {0};
  nThreadPoolCreate(&pool, THREADPOOL_INIT_COUNT);
  // pool --> memset();
  int i = 0;
  for (i = 0;i < TASK_INIT_SIZE;i ++) {
    struct nTask *task = (struct nTask *)malloc(sizeof(struct nTask));
    if (task == NULL) {
      perror("malloc");
      exit(1);
    }
    memset(task, 0, sizeof(struct nTask));
    task->task_func = task_entry;
    task->user_data = malloc(sizeof(int));
    *(int*)task->user_data  = i;
    nThreadPoolPushTask(&pool, task);
  }
  getchar();//让程序停留在这一步,直到它从键盘接收到消息.
}
#endif

对于条件变量的理解

当线程wait的时候,通过条件变量,去告知那个阻塞的线程,现在可以不用阻塞了。

为什么在发送激活(广播或者信号)线程的时候要额外加锁呢?看下图就明白了


相关文章
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
5644 77
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
482 67
|
Ubuntu 搜索推荐 Linux
详解Ubuntu的strings与grep命令:Linux开发的实用工具。
这就是Ubuntu中的strings和grep命令,透明且强大。我希望你喜欢这个神奇的世界,并能在你的Linux开发旅程上,通过它们找到你的方向。记住,你的电脑是你的舞台,在上面你可以做任何你想做的事,只要你敢于尝试。
592 32
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
329 26
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
347 17
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
453 16
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
977 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
578 12

热门文章

最新文章