运维最新Linux下线程池详解与实现:提升多任务处理效率的关键(1),收割快手,字节,百度,美团的Offer之旅

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


目录

日志

认识ctime

如何遍历可变参数列表

日志的实现

线程池

线程池的理解

线程池的实现

使用单例模式改造线程池

单例模式的理解

单例模式的实现


日志

认识ctime

ctime 是 C/C++ 中的一个库,主要用于处理日期和时间。它包含了一系列函数,用于获取系统当前时间、进行时间格式化输出、以及字符串和日期之间的转换等。下面是一些常用的 ctime 函数及其详解:

time_t

time_t 是 C/C++ 语言中用于表示时间的一个数据类型,通常是一个长整型(long int 或 long long int)。它用来存储从 1970 年 1 月 1 日 00:00:00(UTC,即协调世界时)起至今的秒数,这个起始时间点被称为 Unix 时间戳的纪元(epoch)

  1. time() 函数
    • 函数原型:time_t time(time_t *tloc)
    • 功能:返回从纪元(即格林尼治时间(GMT)1970年1月1日午夜0点)开始至今的秒数。如果 tloc 不是一个空指针,time 函数还会将返回值写入 tloc 所指向的位置。
    • 返回值:一个 time_t 类型的值,表示自纪元以来的秒数。
  1. localtime() 函数
    • 函数原型:struct tm *localtime(const time_t *timeptr)
    • 功能:将 time_t 类型的时间戳转换为本地时间的 tm 结构体。
    • 返回值:一个指向 tm 结构体的指针,包含了转换后的本地时间信息。
  1. gmtime() 函数
    • 函数原型:struct tm *gmtime(const time_t *timeptr)
    • 功能:将 time_t 类型的时间戳转换为协调世界时(UTC)的 tm 结构体。
    • 返回值:一个指向 tm 结构体的指针,包含了转换后的 UTC 时间信息。
  1. asctime() 函数
    • 函数原型:char *asctime(const struct tm *timeptr)
    • 功能:将 tm 日期时间结构体转换成日期时间字符串。
    • 返回值:指向日期时间字符串的指针。
  1. ctime() 函数
    • 函数原型:char *ctime(const time_t *timeval)
    • 功能:将 time_t 秒数日期时间格式直接转换成日期时间字符串格式输出。使用更加方便,因为不需要先转换为 tm 结构体。
    • 返回值:指向日期时间字符串的指针。如果返回的时间在1970年1月1日 0:0:0(UTC时间)之前,则返回NULL。

struct tm

struct tm 是 C/C++ 语言中的一个结构体,用于表示和处理日期和时间。它的具体结构如下:

struct tm {
    int tm_sec;   // 秒,范围从 0 到 59
    int tm_min;   // 分,范围从 0 到 59
    int tm_hour;  // 时,范围从 0 到 23
    int tm_mday;  // 一个月中的日,范围从 1 到 31
    int tm_mon;   // 月份,范围从 0 到 11(其中0表示一月,11表示十二月)
    int tm_year;  // 自1900年起的年数
    int tm_wday;  // 一周中的日,范围从 0(周日)到 6(周六)
    int tm_yday;  // 一年中的日,范围从 0 到 365
    int tm_isdst; // 夏令时标识符,小于0表示没有夏令时,等于0表示不知道,大于0表示夏令时
};

这个结构体包含了年、月、日、时、分、秒等时间信息,使得开发者能够方便地获取和处理当前时间、进行日期和时间的计算、格式化输出等操作。

在编程中,你可以使用 time() 函数获取当前时间的时间戳(以秒为单位),然后使用 localtime()gmtime() 函数将这个时间戳转换为 struct tm 结构体,从而获取详细的日期和时间信息

因此如果我们要让日志获得具体的时间并且打印出来可以写出如下函数:

    std::string TimeStampExLocalTime()
    {
        time_t currtime=time(nullptr);
        struct tm*curr=localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
        return time_buffer;
    }
如何遍历可变参数列表

在C语言中,遍历可变参数列表通常需要使用va_listva_startva_argva_end这四个宏。这些宏在stdarg.h头文件中定义,允许你在函数中处理可变数量的参数。

以下是一个简单的步骤说明如何遍历可变参数列表:

  1. 定义函数原型:在函数原型中使用...来表示可变参数列表。
  2. 初始化**va\_list**:在函数内部,使用va_start宏来初始化一个va_list类型的变量。va_start的第一个参数是你的va_list变量,第二个参数是可变参数列表之前的最后一个固定参数。
  3. 遍历参数:使用va_arg宏来遍历参数列表。每次调用va_arg时,它都会返回列表中的下一个参数,并将va_list指针向前移动到下一个参数。你需要指定返回参数的类型。
  4. 结束遍历:在遍历完所有参数后,使用va_end宏来清理va_list变量。

下面是一个示例函数,它接受一个整数作为参数数量,然后遍历可变参数列表中的所有整数,并将它们打印出来:

#include <stdarg.h>
#include <stdio.h>

void print_numbers(int count, ...) {
    va_list args;
    int i;

    // 初始化va_list变量
    va_start(args, count);

    // 遍历参数列表
    for (i = 0; i < count; i++) {
        int number = va_arg(args, int); // 获取下一个int类型的参数
        printf("%d ", number); // 打印参数
    }

    // 清理va_list变量
    va_end(args);

    printf("\n");
}

int main() {
    print_numbers(3, 1, 2, 3); // 输出: 1 2 3
    print_numbers(5, 5, 10, 15, 20, 25); // 输出: 5 10 15 20 25
    return 0;
}

在上面的代码中,print_numbers函数首先使用va_start初始化va_list变量args。然后,它通过一个循环使用va_arg宏来遍历参数列表,并在每次迭代中打印一个整数。最后,它使用va_end来清理va_list变量。

请注意,由于C语言在编译时不会检查可变参数的类型或数量,因此在使用可变参数时应该格外小心,以避免类型不匹配或缓冲区溢出等问题。此外,确保传递给va_start的最后一个固定参数是正确的,因为va_start使用这个参数来确定可变参数列表的起始位置。

日志的实现
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

enum
{
    Screen = 10,
    OneFile,
    ClassFile
};

std::string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

class Log
{
public:
    Log() : style(defaultstyle), filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
    }
    void Enable(int sty) //
    {
        style = sty;
    }
    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm *curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
                 curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
                 curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }
    void WriteLogToOneFile(const std::string &logname, const std::string &message)
    {
        umask(0);
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, message.c_str(), message.size());
        close(fd);
        // std::ofstream out(logname);
        // if (!out.is_open())
        //     return;
        // out.write(message.c_str(), message.size());
        // out.close();
    }
    void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
    {
        std::string logname = logdir;
        logname += "/";
        logname += filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    void WriteLog(const std::string &levelstr, const std::string &message)
    {
        switch (style)
        {
        case Screen:
            std::cout << message;
            break;
        case OneFile:
            WriteLogToClassFile("all", message);
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message);
            break;
        default:
            break;
        }
    }
    void LogMessage(int level, const char *format, ...) // 类C的一个日志接口
    {
        char leftbuffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();
        std::string idstr = std::to_string(getpid());

        char rightbuffer[1024];
        va_list args; // char *, void *
        va_start(args, format);
        // args 指向了可变参数部分
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
        va_end(args); // args = nullptr;
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
                 levelstr.c_str(), currtime.c_str(), idstr.c_str());

        std::string loginfo = leftbuffer;
        loginfo += rightbuffer;
        WriteLog(levelstr, loginfo);
    }
    // void operator()(int level, const char *format, ...)
    // {
    //     LogMessage(int level, const char *format, ...)
    // }
    ~Log() {}

private:
    int style;
    std::string filename;
};

Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(Screen);
    }
    ~Conf()
    {}
};

Conf conf;

线程池

线程池的理解

在Linux环境下,线程池是一种用于管理和复用线程的技术,旨在避免频繁地创建和销毁线程,从而提高系统的性能和资源利用率。线程池预先创建并维护一定数量的线程,这些线程在空闲时处于等待状态,当任务到来时,线程池会分配一个空闲线程来执行任务,任务完成后线程再次回到空闲状态,等待下一个任务的分配。

线程池的主要组件包括任务队列和线程池管理器。任务队列用于存放待执行的任务,线程池管理器则负责线程的创建、销毁、调度和管理。线程池的大小可以根据实际需求进行调整,以达到最佳的性能和资源利用率。

线程池在Linux下的应用场景非常广泛,尤其适用于需要处理大量并发任务或突发请求的情况。例如,Web服务器在处理大量用户请求时,可以使用线程池来管理和复用线程,提高系统的响应速度和吞吐量。此外,线程池还可以用于异步任务执行、定时任务调度等场景。

总的来说,Linux下的线程池是一种高效、灵活的并发编程技术,能够降低系统资源消耗、提高系统性能,并方便对线程并发数进行管控。通过合理使用线程池,可以显著提升Linux系统的稳定性和可扩展性。

线程池的实现

实际上线程池的实现同生产者消费者模型之间是存在密切的关系的:

  1. 线程池为生产者消费者模型提供了线程管理和复用的机制,使得生产者和消费者能够并发地执行任务,提高了系统的并发性能。
  2. 生产者可以将生成的数据或任务提交给线程池,由线程池中的线程负责执行,实现了任务的异步处理。我们可以让多个生产者向线程池推送任务,提高推送任务的效率。
  3. 消费者可以从线程池中获取线程来处理从缓冲区中取出的数据或任务,确保了消费者的处理速度与生产者的生成速度相匹配,避免了资源的浪费。可以合理的配置线程池中的线程来提高线程池的处理效率。

线程池实现的大致图示如下:

实现如下(详细请看代码,已给出详细的注释,复用了之前文章的代码):

#pragma once

#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"

static const int defaultnum = 5;//默认的处理任务的线程数量

class ThreadData  //用于传输线程的数据,这里就先传输一个线程名
{
public:
    ThreadData(const std::string &name) : threadname(name)
    {
    }

public:
    std::string threadname;
};

template <class T>
class ThreadPool
{
    public:
    ThreadPool(int thread_num=defaultnum):_thread_num(thread_num) //构造线程池,可以指定处理任务的线程数量
    {
        pthread_mutex_init(&_mutex, nullptr); //初始化锁和条件变量
        pthread_cond_init(&_cond, nullptr);

        for(int i=0;i<_thread_num;i++) //创建线程,这里我们引用了之前写的线程接口:Thread(const std::string &threadname, func_t<T> func, T &data)
        {
            std::string threadname ="thread-"; //构建线程名
            threadname+=std::to_string(i+1);
            ThreadData td(threadname); //线程要传入的数据

            _threads.emplace_back(threadname,std::bind(&ThreadPool<T>::ThreadRun,this,std::placeholders::_1),td); //std::vector<Thread<ThreadData>> _threads;利用vector容器将所有的线程都管理起来
            // 其中需要特别注意ThreadRun这个成员函数,他是整个线程池中的线程运行函数,后续如何详见ThreadRun函数!
            lg.LogMessage(Info, "%s is created...\n", threadname.c_str()); //日志信息,lg在日志类中已定义

        }
    }
    public:
    bool Start() //启动线程
    {
        for(auto &thread : _threads) //循环将所有线程启动
        {
            thread.Start(); //调用thread类中的启动线程
            lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
        }
        return true;
    }

    void ThreadWait(const ThreadData &td) //根据条件变量线程等待的封装
    {
        lg.LogMessage(Debug, "no task, %s is sleeping...\n", td.threadname.c_str());
        pthread_cond_wait(&_cond,&_mutex);

    }
    void ThreadWakeup() //唤醒根据条件变量线程等待的封装
    {
        pthread_cond_signal(&_cond);
    }
    void ThreadRun(ThreadData &td) //真正的线程运行的函数,在前面已经被_threads容器管理起来了
    {
        while(true)
        {
            T t; //任务的创建用于给对应的任务
            { //先给锁,保证原子性
                LockGuard lockguard(&_mutex);
                while(_q.empty()) //任务队列中为空则等待
                {
                    ThreadWait(td);
                    lg.LogMessage(Debug, "thread %s is wakeup\n", td.threadname.c_str());
                }
                t=_q.front(); //拿取任务
                _q.pop();
            }
            t(); //重载了() 就是运行任务的意思
            lg.LogMessage(Debug, "%s handler task %s done, result is : %s\n",
                          td.threadname.c_str(), t.PrintTask().c_str(), t.PrintResult().c_str());


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值