• C 实现有追求的线程池 后续


    引言  -_-  还是老套路开局

      很久以前写过一个有追求的线程池 -> C 实现有追求的线程池 探究

    讲述的是一种思路, 并且实现了. 可以一用. 最近在详细搞simplec 框架. 准备发布个正式版.

    刚好顺带优化一下这个线程池.优化的结果有如下几个方面.

      1). 更加美观合理的api

      2). pthread线程api 优化

      3). 在解决惊群的基础上, 更进一步, 精度定位.

      4). 增加了更多的安全性代码

      扯淡一点, 线程池对于历史语言C, C++中应用的场景并不多. 可以用线程解决的, 都可以用消息队列解决.

    当然线程有个不一样的属性就是可抢占性. 当你追求性能的时候, 那么这个基本满足不了.

    至少现代的游戏框架设计中抢占式任务没见有这个必要.

      以下容许我分段阐述思路, scthreads.h

    #ifndef _H_SIMPLEC_SCTHREADS
    #define _H_SIMPLEC_SCTHREADS
    
    #include <schead.h>
    
    //
    // 这是个线程池的库. 支持异步取消 也加过一些线程帮助库
    //
    
    typedef struct threads * threads_t;
    
    //
    // async_run - 开启一个自销毁的线程 运行 run
    // run        : 运行的主体
    // arg        : run的参数
    // return     : >= Success_Base 表示成功
    //
    extern int async_run(die_f run, void * arg);
    
    //
    // threads_create - 创建一个线程池处理对象
    // return    : 返回创建好的线程池对象, NULL表示失败
    //
    extern threads_t threads_create(void);
    
    //
    // threads_delete - 异步销毁一个线程池对象
    // pool        : 线程池对象
    // return      : void
    //
    extern void threads_delete(threads_t pool);
    
    //
    // threads_add - 线程池中添加要处理的任务
    // pool        : 线程池对象
    // run         : 运行的执行题
    // arg         : run的参数
    // return      : void
    //
    extern void threads_add(threads_t pool, die_f run, void * arg);
    
    #endif // !_H_SIMPLEC_SCTHREADS

    通过上面 可以说明 1). 更加美观合理的api 因为内部使用宏来确定最优线程数. 不需要玩家自己指定.当然这个数值偏小.

    前言  -_-  来点开胃点心

      有时候我们使用pthread 线程的时候, 步骤有点小繁琐. 我们其实不太需要知道有这个线程, 这个线程执行完毕之后做什么.

    只希望简单的帮我异步的执行一个方法就可以了. 这里设计了 thread_run 函数.

    typedef void    (* die_f)(void * node);
    extern int async_run(die_f run, void * arg);

    详细的设计套路. 如下

    #include <pthread.h>
    
    // 运行的主体
    struct func {
        die_f run;
        void * arg;
    };
    
    // thread_run 中 pthread 执行的实体
    static void * _run(void * arg) {
        struct func * func = arg;
        func->run(func->arg);
        free(arg);
        return NULL;
    }
    
    //
    // async - 开启一个自销毁的线程 运行 run
    // run        : 运行的主体
    // arg        : run的参数
    // return     : >= Success_Base 表示成功
    //
    int 
    async_run(die_f run, void * arg) {
        pthread_t tid;
        pthread_attr_t attr;
        struct func * func = malloc(sizeof(struct func));
        if (NULL == func)
            RETURN(Error_Alloc, "malloc sizeof(struct func) is error");
    
        func->run = run;
        func->arg = arg;
    
        // 构建pthread 线程奔跑起来
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        
        if (pthread_create(&tid, &attr, _run, func) < 0) {
            free(func);
            pthread_attr_destroy(&attr);
            RETURN(Error_Base, "pthread_create error run, arg = %p | %p.", run, arg);
        }
    
        pthread_attr_destroy(&attr);
        return Success_Base;
    }

    这里扯一点, 第一个是我常用的通用错误枚举. 

    //
    // flag_e - 全局操作基本行为返回的枚举, 用于判断返回值状态的状态码
    // >= 0 标识 Success状态, < 0 标识 Error状态
    //
    typedef enum {
        Success_Exist    = +2,            //希望存在,设置之前已经存在了.
        Success_Close    = +1,            //文件描述符读取关闭, 读取完毕也会返回这个
        Success_Base     = +0,            //结果正确的返回宏
    
        Error_Base       = -1,            //错误基类型, 所有错误都可用它, 在不清楚的情况下
        Error_Param      = -2,            //调用的参数错误
        Error_Alloc      = -3,            //内存分配错误
        Error_Fd         = -4,            //文件打开失败
        Error_TOUT       = -5,            //超时错误
    } flag_e;

    项目实战中运用的很好. 基本一个函数返回的错误就那些.

    再扯第二点. 在我们使用 pthread_attr_init的时候posix线程推荐我们立即也必须调用 pthread_attr_destroy.

    保证你自己的东西自己释放. 实际上 pthread_*_destroy 这类函数只是返回当前线程状态, 不涉及资源销毁内容.

    再扯第三点, 好用的RETURN宏, 还是挺飘的.

    // 
    // 控制台输出完整的消息提示信息, 其中fmt必须是 "" 包裹的字符串
    // CERR           -> 简单的消息打印
    // CERR_EXIT      -> 输出错误信息, 并推出当前进程
    // CERR_IF        -> if语句检查, 如果符合标准错误直接退出
    // 
    #ifndef _H_CERR
    #define _H_CERR
    
    #define CERR(fmt, ...) 
        fprintf(stderr, "[%s:%s:%d][errno %d:%s]" fmt "
    ",
            __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)
    
    #define CERR_EXIT(fmt,...) 
        CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE)
    
    #define CERR_IF(code) 
        if((code) < 0) 
            CERR_EXIT(#code)
    
    //
    // RETURN - 打印错误信息, 并return 返回指定结果
    // val        : return的东西, 当需要 return void; 时候填 ',' 就过 or NIL
    // fmt        : 双引号包裹的格式化字符串
    // ...        : fmt中对应的参数
    // return     : val
    // 
    #define NIL
    #define RETURN(val, fmt, ...) 
        do {
            CERR(fmt, ##__VA_ARGS__);
            return val;
        } while(0)
    
    
    #endif

    ##  是为了解决, 可变参数中只有一个参数的问题(... 为 empty 没有内容, GCC编译器不过).

    NIL 是为了解决 return void; 语法 被 RETURN(NIL) 这种语法糖替代.

    回到正题, 上面函数其实就体现了 2). pthread线程api 优化 . 主要体现在 我用 

        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    来替代
      pthread_detach(pthread_self());

    把线程启动运行的设置, 移动到线程外边初始化中. 意思就是创建即拥有.

    降低了线程控制代码的耦合性, 微量为线程业务代码提速了.

    启动慢, 运行快.  或者说不认识的时候难认识, 熟悉后好相处其实更好. O(∩_∩)O哈哈~

    正文  -_-  详细的设计

      首先看核心结构, 每个线程对象

    // 线程结构体, 每个线程一个信号量, 定点触发
    struct thread {
        struct thread * next;        // 下一个线程对象
        bool wait;                   // true 表示当前线程被挂起
        pthread_t tid;               // 当前线程id
        pthread_cond_t cond;         // 线程条件变量
    };

    线程启动对象是一个链表. wait表示当前线程挂起状态, 用于能够快速激活挂起的线程.

    // 找到空闲的线程, 并返回起信号量 
    static pthread_cond_t * _threads_getcont(struct threads * pool) {
        struct thread * head = pool->thrs;
        while (head) {
            if (head->wait)
                return &head->cond;
            head = head->next;
        }
        return NULL;
    }

    其中 struct threads 是所有线程对象的调度结构.

    // 定义线程池(线程集)定义
    struct threads {
        size_t size;                // 线程池大小, 最大线程结构体数量
        size_t curr;                // 当前线程池中总的线程数
        size_t idle;                // 当前线程池中空闲的线程数
        pthread_mutex_t mutx;       // 线程互斥量
        struct thread * thrs;       // 线程结构体对象集
        struct job * head;          // 线程任务链表的链头, 队列结构
        struct job * tail;          // 线程任务队列的表尾, 后插入后执行
    };

    任务job采用的是一个队列结构.  线程链表同时消耗这个生产者队列.

    上面wait 设计体现了 3). 在解决惊群的基础上, 更进一步, 精度定位.

    对于第四点 4). 增加了更多的安全性代码 我们的做法体现在pthread 线程的属性控制上.

        // 设置线程属性, 设置线程 允许退出线程
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
        pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    设置开启线程取消状态, 并且支持异步取消.

        // 构建线程, 构建完毕直接获取
        if (pool->idle > 0) {
            pthread_cond_t * cond = _threads_getcont(pool);
            // 先释放锁后发送信号激活线程, 速度快, 缺点丧失线程执行优先级
            pthread_mutex_unlock(mutx);
            // 发送给空闲的线程, 这个信号量一定存在
            pthread_cond_signal(cond);
            return;
        }

    定点发送信号, 精准的解决了惊群现象. 能够用空间换时间那就换, 但是不要浪费.

    扯一点其它惊群, 例如在多进程中epoll中.  fork 后 epoll -> accept 只有一个成功,多个失败.

    解决方案也是有的.最简单就是忽略惊群错误, 但是性能有点影响.也可以通过均衡轮询文件描述符处理.

    对于本线程池相关的详细说明, 可以看下面几个源文件和测试文件

      scthreads.h

      scthreads.c

      test_scthreads.c

    说道最后, 改动的主要原因是以前那版太丑了, 看不惯. 觉得美是好的, 美是一种愉悦的感受~ _φ( °-°)/

    为了美怎么办, 那就整呗~

    后记  -_-  多留点记忆吧, 说不定就忘了

      问题是难免的, 唯有打磨斟酌~

      北方的故事  http://music.163.com/#/song?id=37782112

      

      你羡慕我的心无旁骛, 我羡慕你的幸福生活

      

  • 相关阅读:
    内网渗透之文件下载
    从跨域与同源策略谈CSRF防御与绕过
    内网渗透之权限维持
    一次关于shiro反序列化漏洞的思考
    冰蝎&哥斯拉 流量特征分析
    第三方提权之数据库提权
    APP渗透测试之安卓APP抓包
    从零开始的信息搜集(二)
    从零开始的信息搜集(一)
    python 进程
  • 原文地址:https://www.cnblogs.com/life2refuel/p/6900547.html
Copyright © 2020-2023  润新知