c++简单定时器实现

一.红黑树实现

利用红黑树的有序性组织定时事件。由于需要高频的插入和删除,维护红黑树的绝对有序性会带来性能瓶颈,所以该定时器的效率比较一般。(Nginx中的定时器利用红黑树实现)。用小根堆来组织的话,会明显提高插入和删除的的效率,因为它不严格要求有序,维护成本不是太高。比如libevent/libev和golang中的定时器都是用小根堆实现的。


#ifndef TTIMERS_H
#define TTIMERS_H
#include <stdint.h>
#include <stdio.h>
#ifndef _WIN32
#include <unistd.h>
#else
#include <windows.h>
#endif
#include <functional>
#include <set>
#include <atomic>
#include <mutex>

//typedef void(*Callback)(const struct _TimerNodeBase& node);
struct _TimerNodeBase;
using Callback=std::function<void(const _TimerNodeBase& node)>;
struct _TimerNodeBase
{
    uint64_t expire;
    uint64_t offset_expire;
    int64_t id;
    _TimerNodeBase(int64_t _id, uint64_t _expire) :id(_id), expire(_expire), offset_expire(0){}
    bool operator<(const _TimerNodeBase& node) const
    {
        if (expire<node.expire)
            return true;
        else if (expire>node.expire)
            return false;
        else
            return id<node.id;
    }

};

struct _TimerNode :public _TimerNodeBase
{
    Callback func;
    _TimerNode(int64_t _id, uint64_t _expire, Callback _func) :_TimerNodeBase(_id, _expire)
    {
        func = _func;
    }
};

class Timer
{
public:
    const inline _TimerNodeBase& AddTimer(uint64_t msec, Callback func)
    {
        uint64_t now = GetCurrentTimeMs();
        uint64_t expire = now + msec;
        int64_t id = GenID();
        printf("[%lld] AddTimer: id=%lld, expire=%lld.\r\n", now, id, now + msec);
        std::unique_lock<std::mutex> locker(time_mx);
        if (timeouts.empty() || expire <= timeouts.crbegin()->expire)
        {
            auto pairs = timeouts.emplace(id, expire, func);
            auto iter = pairs.first;
            if (pairs.second && iter != timeouts.begin())
            {
                iter--;
                const uint64_t* offset_expire = &pairs.first->offset_expire;
                uint64_t* timeoffset = const_cast<uint64_t*>(offset_expire);
                *timeoffset = iter->offset_expire;
            }
            return static_cast<const _TimerNodeBase&>(*pairs.first);
        }
        auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), id, expire, func);
        auto iter = ele;
        iter--;
        const uint64_t* offset_expire = &ele->offset_expire;
        uint64_t* timeoffset = const_cast<uint64_t*>(offset_expire);
        *timeoffset = iter->offset_expire;
        return static_cast<const _TimerNodeBase&>(*ele);
    }
    inline bool DelTimer(const _TimerNodeBase& node)
    {
        struct _TimerNode n(node.id, node.expire, Callback());
        time_mx.lock();
        auto iter = timeouts.find(n);
        if (iter == timeouts.end())
        {
            time_mx.unlock();
            return false;
        }
        timeouts.erase(iter);
        time_mx.unlock();
        return true;
    }
    inline void HandleTimer(uint64_t now)
    {
        if (paused_markspot>0)
        {
            TimerSleep(10);
            return;
        }
        time_mx.lock();
        auto iter = timeouts.begin();
        while (iter != timeouts.end() && (iter->expire + iter->offset_expire <= now))
        {
            printf("[%lld] execute timer id=%lld.\r\n", now, iter->id);
            if (iter->func.operator bool())
            {
                //如果回调函数很耗时,执行回调期间以下操作将被阻塞:
                //AddTimer, DelTimer,StopTimer
                //time_mx.unlock();
                iter->func(*iter);
                //time_mx.lock();
            }
            iter = timeouts.erase(iter);
            continue;
        }
        time_mx.unlock();
    }
    inline int TimeToSleep()
    {
        time_mx.lock();
        auto iter = timeouts.begin();
        if (iter == timeouts.end())
        {
            time_mx.unlock();
            return -1;
        }
        int diss = iter->expire + iter->offset_expire - GetCurrentTimeMs();
        time_mx.unlock();
        return diss>0 ? diss : 0;
    }
    inline void TimerSleep(int msec)
    {
#ifdef _MSC_VER
        Sleep(msec);
#else
        usleep(1000 * msec);
#endif
    }
    inline void TimerPause()
    {
        paused_markspot = GetCurrentTimeMs();
    }
    inline void TimerResume()
    {
        if (paused_markspot <= 0)
            return;
        time_mx.lock();
        auto iter = timeouts.begin();
        while (iter != timeouts.end())
        {
            const uint64_t* offset_expire = &iter->offset_expire;
            uint64_t* timeoffset = const_cast<uint64_t*>(offset_expire);
            *timeoffset += (GetCurrentTimeMs() - paused_markspot);
            iter++;
        }
        time_mx.unlock();
        paused_markspot = 0;
    }
    inline void TimerStop()
    {
        time_mx.lock();
        auto iter = timeouts.begin();
        while (iter != timeouts.end())
        {
            iter = timeouts.erase(iter);
        }
        time_mx.unlock();
    }
    static inline uint64_t GetCurrentTimeMs()
    {
#ifndef _MSC_VER
        struct timespec ti;
        uint64_t t;
        clock_gettime(CLOCK_MONOTONIC, &ti);
        t = (uint64_t)ti.tv_sec * 1000;
        t += ti.tv_nsec / 1000000;
        return t;//milliseconds
#else
        return GetTickCount64();
#endif
    }
private:
    static inline int64_t GenID()
    {
        return gid++;
    }
    static int64_t gid;
    std::atomic_uint64_t paused_markspot = 0;
    std::mutex time_mx;
    std::set<_TimerNode> timeouts;
};
int64_t Timer::gid = 0;


int main()
{
    Timer tim;
    tim.AddTimer(1000, [](const _TimerNodeBase& node){
        printf("timer expire, node=[id=%lld,expire=%lld, expire_offset=%lld]\r\n", node.id, node.expire, node.offset_expire);
    });
    const _TimerNodeBase& n = tim.AddTimer(2000, [](const _TimerNodeBase& node){
        const _TimerNode& n = static_cast<const _TimerNode&>(node);
        printf("timer expire, node=[id=%lld,expire=%lld, expire_offset=%lld]\r\n", n.id, n.expire, n.offset_expire);
    });
    tim.DelTimer(n);
    tim.TimerPause();
    tim.TimerSleep(2000);
    tim.TimerResume();
    while (true)
    {
        int time_sleep = tim.TimeToSleep();
        if (time_sleep>0)
            tim.TimerSleep(time_sleep);
        uint64_t now = Timer::GetCurrentTimeMs();
        tim.HandleTimer(now);
    }
    return 0;
}

#endif
二.时间轮实现

时间轮适用于海量时间密集定时任务。多线程环境下可以做到很小的加锁粒度,效率很高,而红黑树,小根堆以及跳表实现的定时器,对数据操作时都需要将整个结构加锁,效率受限。

linux内核 crontab定时器: 8层时间轮

skynet: 5层时间轮

kafka: 3层时间轮

下面是一个5层时间轮定时器的实现例子,仅供学习参考


//spinlock.h
#ifndef SPINLOCK_H
#define SPINLOCK_H
#include <atomic>
struct spinlock {
    std::atomic_bool flag;
};

void spinlock_init(struct spinlock* lock) {
    lock->flag.store(false);
}

void spinlock_lock(struct spinlock* lock) {
    bool expected = false;
    while (!lock->flag.compare_exchange_weak(expected, true)) {}
    //while (__sync_lock_test_and_set(&lock->lock, 1)) {}
}

int spinlock_trylock(struct spinlock* lock) {
    bool expected = false;
    return lock->flag.compare_exchange_weak(expected, true);
    //return __sync_lock_test_and_set(&lock->lock, 1) == 0;
}

void spinlock_unlock(struct spinlock* lock) {
    //__sync_lock_release(&lock->lock);
    lock->flag.store(false);
}

void spinlock_destroy(struct spinlock* lock) {
    (void)lock;
}

#endif

//timerwheel.h
#ifndef __TIMERWHEEL_H__
#define __TIMERWHEEL_H__

#include <stdint.h>
#ifndef _MSC_VER
#include <unistd.h>
#else
#include <windows.h>
#endif
#define TIME_NEAR_SHIFT 8
#define TIME_NEAR (1 << TIME_NEAR_SHIFT)
#define TIME_NEAR_MASK (TIME_NEAR - 1)

#define TIME_LEVEL_SHIFT 6
#define TIME_LEVEL (1 << TIME_LEVEL_SHIFT)
#define TIME_LEVEL_MASK (TIME_LEVEL - 1)
#define TIME_SCALE 10 //时间精度设为10毫秒

typedef void (*handler_ptr)(void* usrdata, int datalen);

typedef struct timer_node {
    uint32_t expire;
    handler_ptr callback;
    void* data;
    int datalen;
    uint8_t cancel;
    int tid;
} timer_node_t;

timer_node_t* add_timer(int tid,int time, handler_ptr func, void* data, int len,int count=0);

void del_timer(timer_node_t* node);

void expire_timer(void);

void init_timer(void);

void clear_timer(void);

#endif

//timerwheel.cc
#include "spinlock.h"
#include "timerwheel.h"

typedef struct timer_node_internal
{
    struct timer_node node;
    int count;
    int duration;
    struct timer_node_internal* next;
}timer_node_it;

typedef struct link_list {
    timer_node_it head;
    timer_node_it* tail;
} link_list_t;

typedef struct timer {
    link_list_t nearest[TIME_NEAR];
    link_list_t t[4][TIME_LEVEL];
    struct spinlock lock;
    uint32_t time_tick;//第一层的时间刻度, 根据时间精度, 每一次加加1(即如果精度为10ms, 则每10ms加1)
    uint64_t timestamp_ref;//参考时间戳
} s_timer_t;

static s_timer_t* TI = NULL;

/**
 * @brief 移除list下面所有的节点
 * @param {link_list_t} *list
 * @return {timer_node_it} *ret 节点的链表头
 */
timer_node_it* link_clear(link_list_t* list)
{
    timer_node_it* ret = list->head.next;
    list->head.next = 0;
    list->tail = &(list->head);
    return ret;
}

void link(link_list_t* list, timer_node_it* node)
{
    list->tail->next = node;
    list->tail = node;
    node->next = 0;
}

void add_node(s_timer_t* T, timer_node_it* node)
{
    uint32_t time = node->node.expire;
    uint32_t current_time = T->time_tick;
    uint32_t msec = time - current_time;

    if (msec < TIME_NEAR) { //[0, 0x100)
        link(&T->nearest[time & TIME_NEAR_MASK], node);
    }
    else if (msec < (1 << (TIME_NEAR_SHIFT + TIME_LEVEL_SHIFT))) { //[0x100, 0x4000)
        link(&T->t[0][((time >> TIME_NEAR_SHIFT) & TIME_LEVEL_MASK)], node);
    }
    else if (msec < (1 << (TIME_NEAR_SHIFT + 2 * TIME_LEVEL_SHIFT))) { //[0x4000, 0x100000)
        link(&T->t[1][((time >> (TIME_NEAR_SHIFT + TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)], node);
    }
    else if (msec < (1 << (TIME_NEAR_SHIFT + 3 * TIME_LEVEL_SHIFT))) { //[0x100000, 0x4000000)
        link(&T->t[2][((time >> (TIME_NEAR_SHIFT + 2 * TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)], node);
    }
    else { //[0x4000000, 0xffffffff]
        link(&T->t[3][((time >> (TIME_NEAR_SHIFT + 3 * TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)], node);
    }
}

/**
 * @brief 从timer子数组(level)取出idx的链表节点, 并重新添加到timer中
 * @param {s_timer_t} *T
 * @param {int} level
 * @param {int} idx
 * @return {*}
 */
void move_list(s_timer_t* T, int level, int idx)
{
    timer_node_it* current = link_clear(&T->t[level][idx]);

    while (current) {
        timer_node_it* temp = current->next;
        add_node(T, current);
        current = temp;
    }
}

/**
 * @brief 重新映射, 将该层时间节点下的链表重新映射到上一层去
 * @param {s_timer_t} *T
 * @return {*}
 */
void timer_shift(s_timer_t* T)
{
    int mask = TIME_NEAR;
    uint32_t ct = ++T->time_tick;

    if (ct == 0) {
        move_list(T, 3, 0);
    }
    else {
        // ct / 256
        uint32_t time = ct >> TIME_NEAR_SHIFT;
        int i = 0;

        // ct % 256 == 0
        while ((ct & (mask - 1)) == 0) {
            int idx = time & TIME_LEVEL_MASK;

            if (idx != 0) {
                move_list(T, i, idx);
                break;
            }
            mask <<= TIME_LEVEL_SHIFT;
            time >>= TIME_LEVEL_SHIFT;
            ++i;
        }
    }
}

/**
 * @brief 执行所有节点的回调
 * @param {timer_node_it} *current
 * @return {*}
 */
void dispatch_list(s_timer_t* T,timer_node_it* current)
{
    do {
        timer_node_it* temp = current;
        current = current->next;
        if (temp->node.cancel == 0) {
            temp->node.callback(temp->node.data,temp->node.datalen);
        }
        if (temp->node.cancel != 0) 
        {
            free(temp);
            continue;
        }
        if (temp->count < 0||--temp->count>0)
        {
            temp->next = NULL;
            temp->node.expire = temp->duration + T->time_tick;
            spinlock_lock(&T->lock);
            add_node(T, temp);
            spinlock_unlock(&T->lock);
        }
        else
        {
            free(temp);
        }
    } while (current);
}

void timer_execute(s_timer_t* T)
{
    int idx = T->time_tick & TIME_NEAR_MASK;

    while (T->nearest[idx].head.next) {
        timer_node_it* current = link_clear(&T->nearest[idx]);
        spinlock_unlock(&T->lock);
        dispatch_list(T,current);
        spinlock_lock(&T->lock);
    }
}

/**
 * @brief 更新定时器
 * @param {s_timer_t} *T
 * @return {*}
 */
void timer_update(s_timer_t* T)
{
    spinlock_lock(&T->lock);
    timer_execute(T);
    timer_shift(T);
    timer_execute(T);
    spinlock_unlock(&T->lock);
}

s_timer_t* timer_create_timer(void)
{
    s_timer_t* timer = (s_timer_t*)malloc(sizeof(s_timer_t));
    memset(timer, 0, sizeof(s_timer_t));

    int i, j;
    for (i = 0; i < TIME_NEAR; i++) {
        link_clear(&timer->nearest[i]);
    }
    for (i = 0; i < sizeof(timer->t) / sizeof(timer->t[0]); i++) {
        for (j = 0; j < TIME_LEVEL; j++) {
            link_clear(&timer->t[i][j]);
        }
    }

    spinlock_init(&timer->lock);
    return timer;
}

uint64_t get_current_time(void)
{
#ifndef _MSC_VER
    struct timespec ti;
    uint64_t t;
    clock_gettime(CLOCK_MONOTONIC, &ti);
    t = (uint64_t)ti.tv_sec * 1000;
    t += ti.tv_nsec / 1000000;//milliseconds
    return t/TIME_SCALE;
#else
    return GetTickCount64()/ TIME_SCALE;
#endif
}


/**
 * @brief 添加定时任务
 * @param {int} threadid
 * @param {int} time delayed
 * @param {handler_pt} func
 * @param {void*} user data
 * @param {int} data len
 * @param {int} task count, default 0 means oneshot executing, negiative value means endless executing.
 * @return {timer_node_t*}
 */
timer_node_t* add_timer(int tid,int time, handler_ptr func, void* data, int len,int count)
{
    timer_node_it* node = (timer_node_it*)malloc(sizeof(timer_node_it));
    spinlock_lock(&TI->lock);
    node->node.expire = time + TI->time_tick;
    node->node.callback = func;
    node->node.data = data;
    node->node.datalen = len;
    node->node.tid = tid;
    node->node.cancel = 0;
    node->count = count;
    node->duration = time;
    node->next = NULL;
    if (time <= 0) {
        node->node.callback(node->node.data,node->node.datalen);
        free(node);
        spinlock_unlock(&TI->lock);
        return NULL;
    }

    add_node(TI, node);
    spinlock_unlock(&TI->lock);

    return &node->node;
}

/**
 * @brief 取消定时任务
 * @param {timer_node_t} *node
 * @return {*}
 */
void del_timer(timer_node_t* node)
{
    node->cancel = 1;
}

/**
 * @brief 遍历定时器, 并执行过期的定时器回调
 * @param {*}
 * @return {*}
 */
void expire_timer(void)
{
    uint64_t cp = get_current_time();

    if (cp != TI->timestamp_ref) {
        uint32_t diff = (uint32_t)(cp - TI->timestamp_ref);
        TI->timestamp_ref = cp;
        int i;
        for (i = 0; i < diff; i++) {
            timer_update(TI);
        }
    }
}

void init_timer(void)
{
    TI = timer_create_timer();
    TI->timestamp_ref = get_current_time();
}

void clear_timer(void)
{
    int i, j;

    for (i = 0; i < TIME_NEAR; i++) {
        link_list_t* list = &TI->nearest[i];
        timer_node_it* current = list->head.next;

        while (current) {
            timer_node_it* temp = current;
            current = current->next;
            free(temp);
        }
        link_clear(&TI->nearest[i]);
    }

    for (i = 0; i < sizeof(TI->t) / sizeof(TI->t[0]); i++) {
        for (j = 0; j < TIME_LEVEL; j++) {
            link_list_t* list = &TI->t[i][j];
            timer_node_it* current = list->head.next;

            while (current) {
                timer_node_it* temp = current;
                current = current->next;
                free(temp);
            }
            link_clear(&TI->t[i][j]);
        }
    }
}

//main.cc
#include "timerwheel.h"
#include <thread>
#include <chrono>
#include <stdio.h>

static int temp_flag = 0;
void timer_func_cb(void* data, int len)
{
    temp_flag--;
    printf("+++ data address=%#x,data length=%d\n",data,len);
}
int main()
{
    printf("+++ func: %s, line: %d +++\n", __FUNCTION__, __LINE__);
    init_timer();
    timer_node_t* delay_task = add_timer(100,600, timer_func_cb,NULL,0,-1);
    temp_flag = 3;

    while (temp_flag) {
        expire_timer();
        std::this_thread::sleep_for(std::chrono::microseconds(200));//休眠时间要小于时间精度(10ms),因为每个定时事件的处理也需要消耗一定的时间
    }
    printf("+++ func: %s, line: %d +++\n", __FUNCTION__, __LINE__);
    clear_timer();
    return 0;
}

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在Linux C中实现定时器,我们可以使用信号处理机制来实现。具体步骤如下: 1. 引入头文件:包括<sys/time.h> 和 <signal.h>。 2. 定义信号处理函数:在信号处理函数中定义定时器到期时的操作。例如,可以在信号处理函数中输出一条定时器到期的消息。 3. 设置定时器:使用setitimer函数来设置定时器的间隔时间和初始启动时间,并指定信号处理函数。setitimer函数需要传入一个结构体itimerval作为参数,该结构体包含两个成员:it_value代表第一次到期的时间,it_interval代表后续到期的时间间隔。 4. 阻塞信号:使用sigaction函数阻塞相关信号,以免在处理定时器到期时被其他信号打断。 5. 开启定时器:使用信号处理函数来触发定时器,并在定时器到期时执行相关操作。 以下是一个简单的示例代码: ```C #include <stdio.h> #include <sys/time.h> #include <signal.h> void timer_handler(int signum) { printf("Timer expired!\n"); } int main() { struct itimerval timer; // 设置定时器间隔为2秒,并初始化定时器 timer.it_value.tv_sec = 2; timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = 2; timer.it_interval.tv_usec = 0; // 设置信号处理函数 signal(SIGALRM, timer_handler); // 设置定时器 setitimer(ITIMER_REAL, &timer, NULL); // 阻塞相关信号 sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGALRM); sigprocmask(SIG_BLOCK, &mask, NULL); // 循环等待定时器到期 while(1) { pause(); } return 0; } ``` 以上代码中,我们通过设置定时器的间隔时间和初始启动时间来控制定时器的重复触发。在信号处理函数中,我们通过输出一条消息来表示定时器到期的事件。在主函数中,我们首先设置信号处理函数,然后设置定时器并开启定时器,并最后通过循环等待定时器到期来保持程序的运行。 ### 回答2: 在Linux C编程中,实现定时器可以使用信号机制来达到目的。下面是一个简单的例子,展示了如何实现一个定时器。 首先,需要包含头文件`<unistd.h>`和`<signal.h>`,以便使用相关的函数和宏定义。 然后,定义一个用于处理定时器的信号处理函数,例如命名为`timer_handler`。在这个函数中,可以执行特定的操作作为定时器触发后的处理逻辑。在下面的例子中,我们只是简单地打印一条消息。 接下来,创建一个`timer_t`类型的变量,用于存储定时器的ID。可以使用`timer_create`函数创建一个新的定时器,并传入相关的参数,如定时器类型、信号处理函数等。 然后,使用`timer_settime`函数设置定时器的时间参数,包括初始时间和间隔时间。在下面的例子中,我们将定时器设置为3秒后启动,并且每隔5秒触发一次。 最后,使用`sleep`函数使程序暂停,以便触发定时器。在实际应用中,可以根据需要将这个定时器与其他功能集成。 下面是一个完整的例子代码: ```c #include <unistd.h> #include <signal.h> #include <stdio.h> void timer_handler(int signum) { printf("Timer expired.\n"); } int main() { // 创建一个新的定时器 timer_t timerid; struct sigevent sev; struct itimerspec its; sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGALRM; sev.sigev_value.sival_ptr = &timerid; timer_create(CLOCK_REALTIME, &sev, &timerid); // 设置定时器参数 its.it_value.tv_sec = 3; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 5; its.it_interval.tv_nsec = 0; timer_settime(timerid, 0, &its, NULL); // 暂停程序,等待定时器触发 sleep(20); return 0; } ``` 在上述的例子中,我们创建了一个3秒后启动的定时器,并且每隔5秒触发一次。程序将在主函数中的`sleep`函数处暂停20秒,期间定时器会触发三次,并打印"Timer expired."的消息。 ### 回答3: 在Linux C中,我们可以使用`timer_create()`函数来创建一个定时器。该函数接受四个参数:一个时钟ID,一个用于保存定时器 ID 的变量,一个结构体指针以指定定时器的属性,以及一个可选的回调函数。 要创建一个定时器,首先需要定义一个 `timer_t` 类型的变量来保存定时器 ID。然后,要使用 `timer_create()` 函数创建定时器,并将定时器 ID 保存到该变量中。 接下来,需要定义一个结构体变量来指定定时器的属性。可以使用 `struct sigevent` 结构体,并根据需要设置其成员变量。例如,我们可以将 `sigev_notify` 成员设置为 `SIGEV_THREAD`,以指定定时器到期时,将调用一个线程执行回调函数。 在回调函数中,可以执行想要执行的操作。可以在回调函数中做一些计算、输出等,或者执行某个函数或方法。 接下来,我们需要使用 `timer_settime()` 函数来启动定时器,并设置执行回调函数的时间间隔。此函数接受四个参数:定时器 ID、指定定时器何时到期的结构体指针、一个用于保存之前定时器设置的结构体指针,并通过第四个参数来指定相对于哪个时间来设置定时器。 综上所述,实现定时器的步骤如下: 1. 定义 `timer_t` 类型的变量来保存定时器 ID。 2. 使用 `timer_create()` 函数创建定时器。 3. 定义一个结构体变量来指定定时器的属性。 4. 在回调函数中定义要执行的操作。 5. 使用 `timer_settime()` 函数启动定时器,并设置执行回调函数的时间间隔。 需要注意的是,创建定时器的函数及相关数据结构在`<time.h>`头文件中声明。在使用定时器时,可能还需要使用信号和线程相关的函数和数据结构。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值