线程池的设计与实现:C++代码实现

一、引言

1、线程池的定义与作用

在多线程编程中,线程池是一种管理和复用线程的机制。它包含了一个线程集合,用于执行提交给它的任务,而不需要每次任务到来时都创建新的线程。线程池通常由以下几个关键组件组成:任务队列、线程管理模块和线程池管理模块。

2、为什么需要线程池?

  1. 资源管理优化:线程的创建和销毁是一种昂贵的操作,特别是在高并发的情况下。线程池能够预先创建一定数量的线程,并在需要时重复利用这些线程,从而节省了线程创建和销毁的开销,提高了资源的利用率。

  2. 任务调度与限制:线程池可以限制并发执行的线程数量,避免系统过载或资源竞争问题。通过控制线程池的大小和任务队列的容量,可以有效地管理系统的并发度。

  3. 性能提升:线程池能够在一定程度上提升程序的性能。通过合理调整线程池的参数,可以降低线程创建与销毁的开销,减少由于频繁创建线程而可能带来的性能损耗。

  4. 简化编程模型:使用线程池可以简化多线程编程模型。开发者只需将任务提交到线程池中,而无需关注线程的创建和管理细节,从而降低了多线程编程的复杂性和出错几率。

总之,线程池作为一种高效、可管理的并发编程工具,在多种并发场景下发挥着重要作用,特别是在服务器端和多任务处理系统中具有广泛的应用价值。


二、设计思路与目标

1、分析需求:任务分发与执行

在设计线程池之前,首先需要明确以下几点:

  • 任务类型:线程池需要处理的任务类型,可以是简单的函数调用、Lambda表达式或者更复杂的任务对象。

  • 任务管理:如何将任务提交到线程池,并且如何管理这些任务的状态和执行结果。

  • 线程管理:线程池需要能够动态管理线程的数量,并在需要时启动和关闭线程。

2、设计Thread类的实现:封装线程的创建与管理

线程的启动与关闭:

我们通过封装Thread类来管理线程

#ifndef __THREAD_HPP__
#define __THREAD_HPP__

#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>

namespace ThreadModule
{
    using func_t = std::function<void(const std::string &)>;

    class Thread
    {
    public:
        void Excute()
        {
            _func(_threadname);
        }

    public:
        Thread(func_t func, const std::string &name = "none-name")
            : _func(func), _threadname(name), _stop(true) {}
        static void *threadroutine(void *args) 
            // 类成员函数,形参是有this指针的!!
        {
            Thread *self = static_cast<Thread *>(args);
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, threadroutine, this);
            if (!n)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }
        void Detach()
        {
            if (!_stop)
            {
                pthread_detach(_tid);
            }
        }
        void Join()
        {
            if (!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        std::string name()
        {
            return _threadname;
        }
        void Stop()
        {
            _stop = true;
        }
        ~Thread() {}

    private:
        pthread_t _tid;
        std::string _threadname;
        func_t _func;
        bool _stop;
    };
} // namespace ThreadModule

#endif

3、设计Task类的实现:封装任务的执行单元

任务的类型与执行逻辑:

  • 任务接口的设计:定义一个统一的任务接口,可以是函数对象、Lambda表达式或者自定义的任务对象。
  • 任务的执行逻辑:确定任务执行时的逻辑,例如参数传递、异常处理、任务完成通知等。

下面是我定义的简单任务接口:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cmath>
#include <sstream>
#include <iomanip>

std::string formatResult(double _a, double _b, std::string opStr, double _result)
{
    std::ostringstream oss;
    oss << std::fixed << std::setprecision(2) << _a << opStr << _b << "=" << _result;
    return oss.str();
}
enum Operation
{
    ADD = 0,
    SUBTRACT,
    MULTIPLY,
    DIVIDE
};

class Task
{
public:
    Task(int a = 1, int b = 1, int op = ADD) : _a(a), _b(b), _op(op), _result(0) {}

    void Execute()
    {
        switch (_op)
        {
        case ADD:
            _result = static_cast<double>(_a + _b);
            break;
        case SUBTRACT:
            _result = static_cast<double>(_a - _b);
            break;
        case MULTIPLY:
            _result = static_cast<double>(_a * _b);
            break;
        case DIVIDE:
            if (_b != 0)
            {
                _result = static_cast<double>(_a) / _b;
            }
            else
            {
                std::cerr << "错误:除以零!" << std::endl;
            }
            break;
        default:
            std::cerr << "错误:未知操作!" << std::endl;
        }
    }

    std::string ResultToString() const
    {
        std::string opStr;
        switch (_op)
        {
        case Operation::ADD:
            opStr = "+";
            break;
        case Operation::SUBTRACT:
            opStr = "-";
            break;
        case Operation::MULTIPLY:
            opStr = "*";
            break;
        case Operation::DIVIDE:
            opStr = "/";
            break;
        default:
            opStr = "?";
            break;
        }
        return formatResult(_a, _b, opStr, _result);
    }

    std::string DebugToString() const
    {
        std::string opStr;
        switch (_op)
        {
        case Operation::ADD:
            opStr = "+";
            break;
        case Operation::SUBTRACT:
            opStr = "-";
            break;
        case Operation::MULTIPLY:
            opStr = "*";
            break;
        case Operation::DIVIDE:
            opStr = "/";
            break;
        default:
            opStr = "?";
            break;
        }
        return std::to_string(_a) + opStr + std::to_string(_b) + "=?";
    }

    // 重载 () 运算符,允许任务对象像函数对象一样调用
    void operator()() { Execute(); }

    // 获取任务操作的结果
    double GetResult() const { return _result; }

private:
    int _a;         // 操作的第一个操作数
    int _b;         // 操作的第二个操作数
    double _result; // 操作的结果
    int _op;        // 操作类型 (ADD, SUBTRACT, MULTIPLY, DIVIDE)
};

通过对Thread类和Task类的设计与实现,可以为后续的线程池设计奠定基础,确保任务能够被安全地提交到线程池中执行,并能够有效地管理线程资源和任务执行状态。

4、实现LOG宏定义

日志宏的设计与实现:

#pragma once

#include <iostream>
#include <fstream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
 
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
        // 构造加锁
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};

const std::string logname = "log.txt";

enum Level
{
    DEBUG = 0,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

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";
    }
}

std::string GetTimeString()
{
    time_t curr_time = time(nullptr);
    struct tm *format_time = localtime(&curr_time);
    if (format_time == nullptr)
        return "None";
    char time_buffer[1024];
    snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
             format_time->tm_year + 1900,
             format_time->tm_mon + 1,
             format_time->tm_mday,
             format_time->tm_hour,
             format_time->tm_min,
             format_time->tm_sec);
    return time_buffer;
}

void SaveFile(const std::string &filename, const std::string &message)
{
    std::ofstream out(filename, std::ios::app);
    if (!out.is_open())
    {
        return;
    }
    out << message;
    out.close();
}

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
    std::string levelstr = LevelToString(level);
    std::string timestr = GetTimeString();
    pid_t selfid = getpid();

    char buffer[1024];
    va_list arg;
    va_start(arg, format);
    vsnprintf(buffer, sizeof(buffer), format, arg);
    va_end(arg);
    std::string message = "[" + timestr + "]" +
                          "[" + levelstr + "]" +
                          "[" + std::to_string(selfid) + "]" +
                          "[" + filename + "]" +
                          "[" + std::to_string(line) + "] " + buffer + "\n";

    LockGuard lockguard(&lock);
    if (!issave)
    {
        std::cout << message;
    }
    else
    {
        SaveFile(logname, message);
    }
}
bool gIsSave = 0;

#define LOG(level, format, ...)                                                \
    do                                                                         \
    {                                                                          \
        LogMessage(__FILE__, __LINE__, gIsSave, level, format, ##__VA_ARGS__); \
    } while (0)

#define EnableFile()    \
    do                  \
    {                   \
        gIsSave = true; \
    } while (0)

#define EnableScreen()   \
    do                   \
    {                    \
        gIsSave = false; \
    } while (0)

日志级别与输出格式控制:

日志级别通过枚举类型 Level 定义,用于标识不同严重性的日志。日志格式包括时间、日志级别、进程ID、文件名、行号和具体日志信息,以便于追溯和调试。

日志的异步输出与性能考虑:

在日志的实现中,通过互斥锁 LockGuard 来保护对共享资源的访问,避免多线程环境下的竞争条件。同时,提供了异步输出到文件的能力,通过 SaveFile() 函数将日志信息追加到文件末尾,确保性能和线程安全性的平衡。

这段代码实现了一个简单的多线程安全的日志系统,通过宏定义和函数封装提供了方便的接口来记录和管理日志。它考虑了多线程环境下的数据竞争问题,并提供了动态切换日志输出目标的能力,方便在不同场景下使用和配置。


三、线程池的核心实现

线程池的基本结构与成员变量:

线程池的基本结构由以下成员变量组成:

template <class T>
class ThreadPool
{
    // 禁止赋值运算符和拷贝构造函数,确保线程池实例不能被复制或赋值
    void LockQueue();                                     // 加锁任务队列的互斥锁
    void UnlockQueue();                                   // 解锁任务队列的互斥锁
    void ThreadSleep();                                   // 线程进入睡眠等待状态
    void ThreadWakeup();                                  // 唤醒一个处于睡眠状态的线程
    void ThreadWakeupAll();                               // 唤醒所有处于睡眠状态的线程
    ThreadPool operator=(const ThreadPool<T> &) = delete; // 禁止赋值运算符
    ThreadPool(const ThreadPool<T> &) = delete;           // 禁止拷贝构造函数

    // 构造函数,初始化线程池对象
    ThreadPool(int threadnum = gdefaultthreadnum);

    // 处理任务的函数,线程池中的工作线程会调用此函数执行任务
    void HandlerTask(const std::string &name);

    // 初始化线程池,创建指定数量的工作线程并启动
    void InitThreadPool();

public:
    // 获取线程池单例实例,如果不存在则创建新实例并返回
    static std::unique_ptr<ThreadPool<T>> &GetInstance(int num = gdefaultthreadnum);

    // 将任务加入任务队列,等待线程池中的线程执行
    bool Enqueue(const T &t);

    // 启动线程池,使得线程池中的线程可以开始执行任务
    void Start();

    // 停止线程池,不再接收新任务,并等待已有任务执行完成
    void Stop();

    // 等待线程池中的所有线程执行完成
    void Wait();

    // 析构函数,释放线程池中的资源,包括互斥锁、条件变量等
    ~ThreadPool();

private:
    int _threadnum;               // 线程池中线程的数量
    std::vector<Thread> _threads; // 线程池中的线程对象数组
    std::queue<T> _task_queue;    // 任务队列,存放需要执行的任务
    pthread_mutex_t _mutex;       // 互斥锁,保护任务队列的访问
    pthread_cond_t _cond;         // 条件变量,用于线程间的同步操作
    int _waitnum = 0;             // 等待执行任务的线程数目
    bool _isrunning = false;      // 线程池的运行状态标志

    static std::unique_ptr<ThreadPool<T>> _instance; // 线程池的单例实例
    static pthread_mutex_t _lock;                    // 控制单例实例访问的互斥锁
};
template <class T>
std::unique_ptr<ThreadPool<T>> ThreadPool<T>::_instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
// 静态成员,用于实现线程池的单例模式,保证全局只有一个线程池实例,并且线程安全。

线程池的初始化与资源管理

线程池的初始化和资源管理包括以下几个关键点:

ThreadPool(int threadnum = gdefaultthreadnum)
    : _threadnum(threadnum)
{
    pthread_mutex_init(&_mutex, nullptr);     // 初始化互斥锁
    pthread_cond_init(&_cond, nullptr);       // 初始化条件变量
    LOG(INFO, "ThreadPool Construct()");
    // 构造函数中进行线程池的初始化工作,包括初始化线程数量和日志记录
}

void InitThreadPool()
{
    for (int num = 0; num < _threadnum; num++)
    {
        std::string name = "thread-" + std::to_string(num + 1);
        std::function<void(const std::string &)> fun = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        _threads.emplace_back(fun, name);     // 创建线程对象,并添加到线程池中的线程数组
        LOG(INFO, "init thread %s done", name.c_str());
    }
    // 初始化线程池,创建指定数量的工作线程,并初始化线程名和任务处理函数
}
  • 在构造函数中,会根据传入的 threadnum 初始化线程池的基本参数,并进行互斥锁和条件变量的初始化。
  • InitThreadPool 函数负责创建 _threadnum 个工作线程,并将它们添加到线程池中的 _threads 数组中,每个线程都有一个名字和一个处理任务的函数。

任务队列的管理与调度

任务队列的管理和调度是线程池中的核心功能之一,确保任务能够被有效地提交、调度和执行。

void HandlerTask(const std::string &name)
{
    LOG(INFO, "%s is running...", name.c_str());
    while (true)
    {
        LockQueue();
        while (_task_queue.empty() && _isrunning)
        {
            _waitnum++;
            ThreadSleep();
            _waitnum--;
        }

        if (_task_queue.empty() && !_isrunning)
        {
            UnlockQueue();
            sleep(1);
            LOG(INFO, "%s break while ...", name.c_str());
            break;
        }
        T task = _task_queue.front();
        _task_queue.pop();
        UnlockQueue();
        LOG(DEBUG, "%s get a task", name.c_str());

        task();
        LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), task.ResultToString().c_str());
    }
}

HandlerTask 函数是线程池中线程实际执行任务的函数。每个工作线程执行该函数,不断从任务队列 _task_queue 中取出任务并执行,直到线程池停止运行。在函数中使用了互斥锁保护 _task_queue 的访问,确保多个线程同时操作任务队列时不会发生竞态条件。使用条件变量 _cond 来实现线程的等待和唤醒机制,当任务队列为空且线程池正在运行时,线程进入等待状态;当有新任务加入时,唤醒一个等待的线程进行任务处理。

线程池的状态控制

线程池的状态控制保证了线程池的稳定运行和异常处理能力。

void Start()
{
    _isrunning = true;
    for (auto &thread : _threads)
    {
        thread.Start();     // 启动所有工作线程
    }
}

void Stop()
{
    LockQueue();
    _isrunning = false;    // 停止线程池运行
    ThreadWakeupAll();     // 唤醒所有等待中的线程
    UnlockQueue();
}

void Wait()
{
    for (auto &thread : _threads)
    {
        thread.Join();      // 等待所有工作线程结束
        LOG(INFO, "%s is quit...", thread.name().c_str());
    }
}

Start 函数用于启动线程池,将 _isrunning 设置为 true,并依次启动所有的工作线程。Stop 函数停止线程池的运行,将 _isrunning 设置为false,唤醒所有等待中的线程,确保线程池能够安全退出。Wait 函数用于等待所有工作线程结束,并记录线程退出的信息。

实现任务提交的接口

bool Enqueue(const T &task)
{
    bool ret = false;
    LockQueue();
    if (_isrunning)
    {
        _task_queue.push(task);    // 将任务加入任务队列
        if (_waitnum > 0)
        {
            ThreadWakeup();     // 唤醒等待中的线程
        }
        LOG(DEBUG, "enqueue task success");
        ret = true;
    }
    UnlockQueue();
    return ret;
}

Enqueue 函数用于向线程池提交任务。首先获取互斥锁保护任务队列的操作,将任务 task 加入 _task_queue 中。如果线程池正在运行且任务队列非空,唤醒一个等待中的线程处理任务。返回 true 表示任务成功加入任务队列,否则返回 false

获得线程池单例的函数:

我们在第一次获得单例的时候,完成对工作线程的初始化,并且启动了线程池。

static std::unique_ptr<ThreadPool<T>> &GetInstance(int num = gdefaultthreadnum)
{
    if (!_instance)
    {
        LockGuard lockguard(&_lock);
        if (!_instance)
        {
            LOG(DEBUG, "creating a ThreadPool instance.");
            _instance = std::unique_ptr<ThreadPool<T>>(new ThreadPool<T>(num));
            _instance->InitThreadPool();
            _instance->Start();
        }
    }
    else
    {
        LOG(DEBUG, "returning the existing instance of the ThreadPool.");
    }
    return _instance;
}

线程池类实现了单例模式,确保在整个程序生命周期内只存在一个线程池实例。这通过 GetInstance 函数实现,使用了双重检查锁定机制来保证线程安全地创建和获取单例实例。第一次检查 _instance 是否为空,如果为空,才会进入互斥锁的保护区域。

  • 双重检查锁定,这是一种常见的单例模式实现方式。其目的是在多线程环境下安全地创建和获取单例实例,同时尽可能减少互斥锁的竞争,提高性能。

单例模式的实现中,使用了 pthread_mutex_t 来保护静态成员 _instance 的创建过程,确保在多线程环境中也能正确地返回单例实例。函数最后返回 _instance,这是一个 std::unique_ptr<ThreadPool<T>>& 类型的引用。这样做的好处是,外部调用 GetInstance 函数可以直接操作和使用线程池的唯一实例。

完整代码

#pragma once

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

using namespace ThreadModule;

const static int gdefaultthreadnum = 3;

template <class T>
class ThreadPool
{
    void LockQueue() { pthread_mutex_lock(&_mutex); }
    void UnlockQueue() { pthread_mutex_unlock(&_mutex); }
    void ThreadSleep() { pthread_cond_wait(&_cond, &_mutex); }
    void ThreadWakeup() { pthread_cond_signal(&_cond); }
    void ThreadWakeupAll() { pthread_cond_broadcast(&_cond); }
    ThreadPool operator=(const ThreadPool<T> &) = delete;
    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool(int threadnum = gdefaultthreadnum)
        : _threadnum(threadnum)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        LOG(INFO, "ThreadPool Construct()");
    }

    void HandlerTask(const std::string &name)
    {
        LOG(INFO, "%s is running...", name.c_str());
        while (true)
        {
            LockQueue();
            while (_task_queue.empty() && _isrunning)
            {
                _waitnum++;
                ThreadSleep();
                _waitnum--;
            }

            if (_task_queue.empty() && !_isrunning)
            {
                UnlockQueue();
                sleep(1);
                LOG(INFO, "%s break while ...", name.c_str());
                break;
            }
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();
            LOG(DEBUG, "%s get a task", name.c_str());

            t();
            LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), t.ResultToString().c_str());
        }
    }
    void InitThreadPool()
    {
        for (int num = 0; num < _threadnum; num++)
        {
            std::string name = "thread-" + std::to_string(num + 1);
            std::function<void(const std::string &)> fun = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
            _threads.emplace_back(fun, name);
            LOG(INFO, "init thread %s done", name.c_str());
        }
    }

public:
    static std::unique_ptr<ThreadPool<T>> &GetInstance(int num = gdefaultthreadnum)
    {
        if (!_instance)
        {
            LockGuard lockguard(&_lock);
            if (!_instance)
            {
                LOG(DEBUG, "creating a ThreadPool instance.");
                _instance = std::unique_ptr<ThreadPool<T>>(new ThreadPool<T>(num));
                // _instance = std::make_unique<ThreadPool<T>>(num);
                _instance->InitThreadPool();
                _instance->Start();
            }
        }
        else
        {
            LOG(DEBUG, "returning the existing instance of the ThreadPool.");
        }
        return _instance;
    }

    bool Enqueue(const T &t)
    {
        bool ret = false;
        LockQueue();
        if (_isrunning)
        {
            _task_queue.push(t);
            if (_waitnum > 0)
            {
                ThreadWakeup();
            }
            LOG(DEBUG, "enqueue task success");
            ret = true;
        }
        UnlockQueue();
        return ret;
    }

    void Start()
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            thread.Start();
        }
    }
    void Stop()
    {
        LockQueue();
        _isrunning = false;
        ThreadWakeupAll();
        UnlockQueue();
    }
    void Wait()
    {
        for (auto &thread : _threads)
        {
            thread.Join();
            LOG(INFO, "%s is quit...", thread.name().c_str());
        }
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        LOG(INFO, "ThreadPool Destory()");
    }

private:
    int _threadnum;
    std::vector<Thread> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
    int _waitnum = 0;
    bool _isrunning = false;

    // 单例  hunger
    static std::unique_ptr<ThreadPool<T>> _instance;
    static pthread_mutex_t _lock;
};

template <class T>
std::unique_ptr<ThreadPool<T>> ThreadPool<T>::_instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;

我设计的的线程池中,使用互斥锁 _mutex 和条件变量 _cond 实现线程同步,确保多线程环境下任务队列的安全访问和线程的同步执行。并设计合理的任务队列 _task_queue 和工作线程数组 _threads,实现任务的提交、调度和执行,保证线程池的高效运行。


四、线程池的使用

#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Log.hpp"
#include <iostream>
#include <ctime>

int main()
{
    srand(time(nullptr) ^ getpid() ^ pthread_self());
    LOG(INFO, "beginning-------------------------------");
    EnableScreen();
    int tasknum = 10;
    while (tasknum--)
    {
        int a = rand() % 50 + 1;
        // usleep(124);
        int b = rand() % 20 + 1;
        // usleep(124);
        int c = rand() % 3 + 1;
        Task tmp(a, b, c);
        LOG(INFO, "main thread push task: %s", tmp.DebugToString().c_str());
        ThreadPool<Task>::GetInstance()->Enqueue(tmp);
        sleep(1);
    }
    ThreadPool<Task>::GetInstance()->Stop();
    sleep(1);
    ThreadPool<Task>::GetInstance()->Wait();
    sleep(1);

    LOG(INFO, "ending----------------------------------");
    return 0;
}

在这里插入图片描述

这段代码通过使用 ThreadPool 类和 Task 类,演示了如何创建多个任务并提交到线程池中执行,同时通过日志记录了任务的执行过程和程序的开始与结束。通过这种方式,可以有效地利用多线程处理任务,提高程序的并发性能和效率。

完整代码点击此处

  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无敌岩雀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值