基于事件驱动状态机的协程框架设计

在设计协程之前,先将几个小概念

并发:

最早的计算机,每次只能执行一个程序,只有当当前执行的程序结束后才能执行其它程序,在此期间,别的程序都得等着。到后来,计算机运行速度提高了,程序员们发现,单任务运行一旦陷入IO阻塞状态,CPU就没事做了,很是浪费资源,于是就想要同一时间执行那么三五个程序,几个程序一块跑,于是就有了并发。原理就是将CPU时间分片,分别用来运行多个程序,可以看成是多个独立的逻辑流,由于能快速切换逻辑流,看起来就像是大家一块跑的。

并发解决了两个问题:

1.提高了CPU的利用率,在某个程序陷入IO或者其它等待状态时,CPU可以转而执行其它程序。

2.表面上看起来多个程序一起运行,解决了跑程序排队等待的问题。

引入的新问题:

并发执行也存在一些问题。我的程序运行到一半,别的进程突然插进来,抢占了CPU,我的中间状态怎么办,我用来存储的内存被覆盖了怎么办?所以跑在一个CPU里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来了一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立运行的程序运行、切换。因为程序的使用涉及到大量的计算机资源配置, 把这活随意的交给用户程序,容易让整个系统被搞挂,资源分配也很难做到相对的公平。所以就出现了操作系统,核心的操作需要陷入内核(kernel),切换到操作系统,让内核来做。

上下文切换:

上下文切换最早是指进程的上下文切换(context switch),它发生在内核态。内核调度器会对每个CPU上执行的进程进行调度(scheduling),以保证每个进程都能分到CPU时间片。当一个进程的时间片用完,或被中断后,内核将保存该进程的运行状态(即上下文),将其存入运行队列(run queue),同时让新的进程占用CPU。进程的上下文切换包括内存地址空间、内核态堆栈和硬件上下文(CPU寄存器)的切换,所以代价很高。

线程:

有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,内核就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现太慢了,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。由于进程切换开销大,所以设计了线程。 大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的重新加载地址空间,页表缓冲区,只要把寄存器刷新一遍就行,能比切换进程开销少点。Linux 2.6内核的clone()系统调用已经支持创建内核级线程,且发布了内核线程库pthread。在同一进程内的线程可以共享进程的地址空间,线程仅需要维护自己的寄存器、栈和线程相关的变量。不过内核线程的调度仍然需要由内核完成,这需要进行用户态和内核态的模式切换,至少包括堆栈和内存映射的切换。而且,不同进程之间的线程切换,有可能会还会导致进程切换,所以代价还是不小。

协程:

为了进一步减小内核态线程上下文切换的开销,于是又有了用户态线程设计,即纤程(Fiber)。如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。

从上面可以看到,实现一个用户态线程有两个必须要处理的问题:

1.碰着阻塞式I\O会导致整个进程被挂起;

2.由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。

如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。协程的做法很像早期操作系统的协作式多任务。

协作式多任务:

当任务得一个到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU。win3.x就是这个方式。

但是,对于操作系统来说,这种做法会让系统不稳定。因为操作系统管理者整个计算机的资源,这个做法容易让系统失去控制(比如用户程序的一个死循环),因此,现在的操作系统都是用的是抢占式多任务。而在一个程序内,使用协作式的方法是可行的,因为自己的程序可以自己控制。 可以这么理解:协程就是在用户程序中实现了协作式任务调度。 这里输入引用文本进程、线程、协程的设计,都是为了并发任务能够更好的利用CPU资源,协程可以作为进程和线程的有力补充。由于我们可以在用户态调度协程任务,所以,我们可以把一组互相依赖的任务设计成协程。这样,当一个协程任务完成之后,可以手动进行任务调度,把自己挂起(yield),切换到另外一个协程执行。这样,由于我们可以控制程序主动让出资源,很多情况下将不需要对资源加锁。

并行:

并行的概念是在多核处理器出来之后才有的。当单核CPU的主频遇到瓶颈的时候,设计师们想出了多核CPU来解决处理器性能问题。前面讲到单核的并发可以看出多个独立的逻辑流,那么多核CPU就是真正的多个独立的逻辑流了,多核CPU在一定程度上解决了CPU性能问题。由于有多个独立的中央调度器,真正实现了程序的并行运行,但是也带来一些列问题:1.临界资源的访问(目前大部分采用锁机制解决) 2.数据相关问题(大部分采用原子操作解决) 3.多进程运行资源调度问题。这里不作重点介绍,感兴趣的可以查阅相关资料。

基于事件驱动状态机(EDSM)的协程设计:

协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早,按照 Knuth 的说法“子例程是协程的特例”,一个子例程就是一次子函数调用,那么实际上协程就是类函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点,返回之后就结束了,而协程入口既可以是起始点,又可以从上一个返回点继续执行,也就是说协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系,这就叫非对称协程(asymmetric coroutines)。

我们举一个例子来看看一种对称协程调用场景,大家最熟悉的“生产者-消费者”事件驱动模型,一个协程负责生产产品并将它们加入队列,另一个负责从队列中取出产品并使用它。为了提高效率,你想一次增加或删除多个产品。伪代码可以是这样的:

# producer coroutine
loop
while queue is not full
create some new items
add the items to queue
yield to consumer

# consumer coroutine
loop
while queue is not empty
remove some items from queue
use the items
yield to producer

按照这种模型,我们编写自己的协程框架,首先,我们需要一个设计一个优先队列:

//priority_heap_queue.hpp
///基于堆的优先队列
#include <string>
template <typename T>
class priority_heap_queue
{
    T* heap;

    void siftup(unsigned int n) 
    {
        auto v = heap[n];
        for (auto n2 = n / 2; n > 0 && v <= heap[n2]; n = n2, n2 /= 2)
        {
            heap[n] = heap[n2];
        }
        heap[n] = v;
    }

    void siftdown(int n)
    {
        auto v = heap[n];
        for (auto n2 = n * 2; n2 < count; n = n2, n2 *= 2)
        {
            if (n2 + 1 < count && heap[n2 + 1] <= heap[n2]) n2++;
            if (v <= heap[n2]) break;
            heap[n] = heap[n2];
        }
        heap[n] = v;
    }

public:
    int count;
    int capacity;

    explicit priority_heap_queue(int cap)
    {
        capacity = cap;
        heap = new T[capacity];
        count = 0;
    }

    ~priority_heap_queue()
    {
        if(heap != nullptr)
        {
            delete[] heap;
        }
    }

    void push(T v)
    {
        if (count >= capacity)
        {
            auto tmp = new T[capacity * 2];
            for (auto i = 0; i < capacity; i++)
            {
                tmp[i] = heap[i];
            }
            capacity *= 2;
            delete heap;
            heap = tmp;
        }
        heap[count] = v;
        siftup(count++);
    }

    T pop()
    {
        auto v = top();
        heap[0] = heap[--count];
        if (count > 1) siftdown(0);
        return v;
    }

    T top()
    {
        return heap[0];
    }

    void clear()
    {
        count = 0;
    }

    bool empty() const
    {
        return count > 0;
    }

    void foreach(void (*callback)(T))
    {
        for(auto i = 0; i < count; i++)
        {
            callback(heap[i]);
        }
    }

    void foreach(void (*callback)(T &, unsigned long), unsigned long tid)
    {
        for (auto i = 0; i < count; i++)
        {
            callback(heap[i], tid);
        }
    }

    void foreach(void(*callback)(T &, std::string), std::string tid)
    {
        for (auto i = 0; i < count; i++)
        {
            callback(heap[i], tid);
        }
    }
};

其次,为了实现自行调度的标准,需要设计一个计时器类:

//timer.h

#pragma once
#include <chrono>

class timer
{
public:

    std::chrono::time_point<std::chrono::system_clock> time;

    timer();

    ~timer();

    void reset();

    std::chrono::milliseconds elapsed() const;

    //微秒
    std::chrono::microseconds elapsed_micro() const;

    //纳秒
    std::chrono::nanoseconds elapsed_nano() const;

    //秒
    std::chrono::seconds elapsed_seconds() const;

    //分
    std::chrono::minutes elapsed_minutes() const;

    //时
    std::chrono::hours elapsed_hours() const;

    bool operator<=(timer &t) const;

    bool operator>(timer &t) const;
};


//timer.cpp

#include "timer.h"
#include<chrono>
using namespace std;
using namespace chrono;

timer::timer()
{
}


timer::~timer()
{
}

void timer::reset()
{
    time = system_clock::now();
}

milliseconds timer::elapsed() const
{
    return duration_cast<milliseconds>(system_clock::now() - time);
}

microseconds timer::elapsed_micro() const
{
    return duration_cast<microseconds>(system_clock::now() - time);
}

nanoseconds timer::elapsed_nano() const
{
    return duration_cast<nanoseconds>(system_clock::now() - time);
}

seconds timer::elapsed_seconds() const
{
    return duration_cast<seconds>(system_clock::now() - time);
}

minutes timer::elapsed_minutes() const
{
    return duration_cast<minutes>(system_clock::now() - time);
}

hours timer::elapsed_hours() const
{
    return duration_cast<hours>(system_clock::now() - time);
}

bool timer::operator<=(timer& t) const
{
    return time <= t.time;
}

bool timer::operator>(timer& t) const
{
    return time < t.time;
}

然后,我们需要把每个任务封装成一个对象

//timer_task.h
#pragma once
#include "timer.h"
#include <memory>
#include <chrono>
#include <string>

class timer_task
{
public:
    timer_task();

    timer_task(timer timer, std::shared_ptr<void> userData, std::chrono::seconds offset, void(*callback)(std::shared_ptr<void>), std::string name);

    ~timer_task();

    void Destory();

    void DoTask() const;

    bool operator<= (timer_task &timer) const;

    bool operator> (timer_task &timer) const;

    timer_task &operator=(const timer_task &timer);


    void(*do_task)(std::shared_ptr<void>);

    unsigned long tid;

    timer timer;

    std::shared_ptr<void> user_data;

    std::chrono::seconds offset;

    std::string name;

    bool need_remove;

};


//timer_task.cpp
#include "timer_task.h"

using namespace std;

// ReSharper disable once CppPossiblyUninitializedMember
timer_task::timer_task()
{
}

// ReSharper disable once CppPossiblyUninitializedMember
timer_task::timer_task(::timer timer, shared_ptr<void> userData, chrono::seconds offset, void(*callback)(std::shared_ptr<void>), string name)
{
    this->timer = timer;
    this->offset = offset;
    this->do_task = callback;
    this->user_data = userData;
    this->need_remove = false;
    this->name = name;
}

timer_task::~timer_task()
{
}

void timer_task::Destory()
{
    need_remove = true;
}

void timer_task::DoTask() const
{
    do_task(user_data);
}

bool timer_task::operator<=(timer_task& timer) const
{
    return this->timer.time + offset <= timer.timer.time + offset;
}

bool timer_task::operator>(timer_task& timer) const
{
    return this->timer.time + offset > timer.timer.time + offset;
}

timer_task &timer_task::operator=(const timer_task& timer)
{
    this->tid = timer.tid;
    this->timer = timer.timer;
    this->offset = timer.offset;
    this->do_task = timer.do_task;
    this->user_data = timer.user_data;
    this->need_remove = false;
    this->name = timer.name;
    return *this;
}

再设计一个任务管理对象,单例模式:

//timer_task_manager.h
#pragma once
#include "timer_task.h"
#include "priority_heap_queue.hpp"

class timer_task_manager
{
    priority_heap_queue<timer_task> task_queue;

    static unsigned long number_of_timer_task;

    static timer_task_manager *instance;

public:

    static timer_task_manager* get_instance();

    void do_timer_task();

    timer_task_manager();

    ~timer_task_manager();

    unsigned long add_timer_task(timer_task t);

    void remove_timer_task(unsigned long tid);

    void remove_timer_task(std::string name);
};



//timer_task_manager.cpp

#include "timer_task_manager.h"
#include <ostream>
#include <iostream>

timer_task_manager* timer_task_manager::instance = nullptr;
unsigned long timer_task_manager::number_of_timer_task = 0;

timer_task_manager* timer_task_manager::get_instance()
{
    if(instance == nullptr)
    {
        instance = new timer_task_manager();
    }
    return instance;
}

void timer_task_manager::do_timer_task()
{
    while(task_queue.count > 0)
    {
        auto task = task_queue.top();
        if(task.timer.elapsed_seconds() >= task.offset)
        {
            if(!task.need_remove)
            {
                task.DoTask();
            }
            task_queue.pop();
        }
    }
}

timer_task_manager::timer_task_manager():task_queue(10)
{
}


timer_task_manager::~timer_task_manager()
{
}

unsigned long timer_task_manager::add_timer_task(timer_task t)
{
    t.tid = number_of_timer_task++;
    task_queue.push(t);
    return t.tid;
}

void timer_task_manager::remove_timer_task(unsigned long tid)
{
    auto remove = [](timer_task &t, unsigned long Tid) { if (t.tid == Tid) t.need_remove = true; };
    task_queue.foreach(remove, tid);
}

void timer_task_manager::remove_timer_task(std::string name)
{
    auto remove = [](timer_task &t, std::string Name) { if (t.name.compare(Name) == 0) t.need_remove = true; };
    task_queue.foreach(remove, name);
}

最后,任务调度器:

//tick_processer.h
#pragma once
#include <chrono>

class tick_processer
{
    std::chrono::time_point<std::chrono::system_clock> last_tick_time;

    std::chrono::time_point<std::chrono::system_clock> begin;

public:

    tick_processer();

    virtual ~tick_processer();
    
    void run(std::chrono::milliseconds tick_millisecond);

    virtual void init();

    std::chrono::milliseconds one_loop();

    virtual void one_tick(std::chrono::milliseconds delts, std::chrono::milliseconds ms);

};


//tick_processer.cpp
#include <thread>
#include <iostream>
#include "tick_processer.h"
#include "timer_task_manager.h"
using namespace std;
using namespace chrono;


// ReSharper disable once CppPossiblyUninitializedMember
tick_processer::tick_processer()
{
}


tick_processer::~tick_processer()
{
}

void tick_processer::run(milliseconds tick_millisecond)
{
    init();
    while(true)
    {
        auto process_time = one_loop();
        if(process_time < tick_millisecond)
        {
            this_thread::sleep_for(milliseconds(tick_millisecond - process_time));
        }
    }
}

void tick_processer::init()
{
    last_tick_time = system_clock::now();
    begin = last_tick_time;
}

milliseconds tick_processer::one_loop()
{
    auto tick_start_time = system_clock::now();
    auto delts = duration_cast<milliseconds>(tick_start_time - last_tick_time);
    auto ms = duration_cast<milliseconds>(tick_start_time - begin);
    one_tick(delts, ms);
    last_tick_time = tick_start_time;
    auto tick_end_time = system_clock::now();
    return duration_cast<milliseconds>(tick_end_time - tick_start_time);
}

void tick_processer::one_tick(milliseconds delts, milliseconds ms)
{
    timer_task_manager::get_instance()->do_timer_task();
    //所有的逻辑可以从这个地方重写实现

}

一个简单的协程框架就好了,接下来为了配合协程调度,我们设计一个简单的有限状态机

//base_fsm.h
#pragma once
#include <chrono>

enum state_set
{
    state0 = 0,
    state1 = 1,
    state2 = 2,
    state3 = 3,
    state4 = 4
};


class base_fsm
{
public:
    std::chrono::microseconds time_in_current_state;
    state_set current_state;

    base_fsm();
    virtual ~base_fsm();

    virtual void begin_state(state_set state);
    virtual void update_state(state_set state);
    virtual void end_state(state_set state);

    void set_state(state_set state);
    void update_fsm(std::chrono::milliseconds delts);

};


//base_fsm.cpp
#include "base_fsm.h"

base_fsm::base_fsm():time_in_current_state(0),current_state(state1)
{
}

base_fsm::~base_fsm()
{
}

void base_fsm::begin_state(state_set state)
{
}

void base_fsm::update_state(state_set state)
{
}

void base_fsm::end_state(state_set state)
{
}

void base_fsm::set_state(state_set state)
{
    end_state(current_state);
    current_state = state;
    time_in_current_state = std::chrono::microseconds(0);
    begin_state(current_state);
}

void base_fsm::update_fsm(std::chrono::milliseconds delts)
{
    if(current_state != state0)
    {
        time_in_current_state += delts;
        update_state(current_state);
    }
}

下面是一个简单调用示例:

//main.cpp
#pragma once
#include <iostream>
#include "timer_task.h"
#include "base_fsm.h"
#include <string>
#include "tick_processer.h"
#include "timer_task_manager.h"
using namespace std;

class my_fsm : public base_fsm
{
public:

    void begin_state(state_set state) override 
    {
    }

    void end_state(state_set state) override 
    {
    }
    
    void update_state(state_set state) override 
    {
        auto callback = [](shared_ptr<void> data)
        {
            cout << *static_pointer_cast<string>(data) << endl;
        };
        auto t = timer_task(timer(), shared_ptr<void>(new string(to_string(state))), chrono::seconds(0), callback, "auto");
        auto tid = timer_task_manager::get_instance()->add_timer_task(t);

        switch (state)
        {
        case state1:
            set_state(state2);
            timer_task_manager::get_instance()->remove_timer_task(tid);
            break;
        case state2:
            set_state(state3);
            timer_task_manager::get_instance()->remove_timer_task("auto");
            break;
        case state3:
            set_state(state4);
            break;
        case state4:
            set_state(state1);
            break;
        case state0: 
            break;
        default: 
            break;
        }
    }
};

class my_processer : public tick_processer
{
public:

    my_fsm fsm;

    void init() override 
    {
        tick_processer::init();
    }

    void one_tick(chrono::milliseconds delts, chrono::milliseconds ms) override 
    {
        tick_processer::one_tick(delts, ms);
        fsm.update_fsm(delts);
    }
};

int main()
{
    my_processer processer;
    processer.run(std::chrono::milliseconds(200));
    return 0;
}

这里,以200ms作为心跳时间,状态机从1到4轮转切换,每个切换的大循环中,状态1和状态2的任务被移除,所以程序会循环输出3和4。 在一些更高级的语言中,譬如C#、java等,在编译器层支持了yield语义,借此实现Coroutine框架会更方便快捷。然而这个yield语义是如何实现的呢? 我们知道 python 的 yield 语义功能类似于一种迭代生成器,函数会保留上次的调用状态,并> 这里输入引用文本在下次调用时会从上个返回点继续执行。用 C 语言来写就像这样:

int function(void) {
    int i;
    for (i = 0; i < 10; i++)
         return i; /* won't work, but wouldn't it be nice */
}

连续对它调用 10 次,它能分别返回 0 到 9。该怎样实现呢?可以利用 goto 语句,如果我们在函数中加入一个状态变量,就可以这样实现:

int function(void) {
    static int i, state = 0;
    switch (state) {
         case 0: goto LABEL0;
               case 1: goto LABEL1;
          }
LABEL0:/* start of function */
          for (i = 0; i < 10; i++) {
               state = 1;/* so we will come back to LABEL1 */
               return i;
LABEL1:;/* resume control straight after the return */
     }
}

这个方法是可行的。我们在所有需要 yield 的位置都加上标签:起始位置加一个,还有所有 return 语句之后都加一个。每个标签用数字编号,我们在状态变量中保存这个编号,这样就能在我们下次调用时告诉我们应该跳到哪个标签上。每次返回前,更新状态变量,指向到正确的标签;不论调用多少次,针对状态变量的 switch 语句都能找到我们要跳转到的位置。

但这还是难看得很。最糟糕的部分是所有的标签都需要手工维护,还必须保证函数中的标签和开头 switch 语句中的一致。每次新增一个 return 语句,就必须想一个新的标签名并将其加到 switch 语句中;每次删除 return 语句时,同样也必须删除对应的标签。这使得维护代码的工作量增加了一倍。

仔细想想,其实我们可以不用 switch 语句来决定要跳转到哪里去执行,而是直接利用 switch 语句本身来实现跳转:

int function(void) {
     static int i, state = 0;
     switch (state) {
          case 0:/* start of function */
          for (i = 0; i < 10; i++) {
               state = 1;/* so we will come back to "case 1" */
               return i;
               case 1:;/* resume control straight after the return */
          }
     }
}

没想到 switch-case 语句可以这样用,其实说白了 C 语言就是脱胎于汇编语言的,switch-case 跟 if-else 一样,无非就是汇编的条件跳转指令的另类实现而已。我们还可以用 LINE 宏使其更加一般化:

int function(void) {
     static int i, state = 0;
     switch (state) {
          case 0:/* start of function */
               for (i = 0; i < 10; i++) {
                    state = __LINE__ + 2;/* so we will come back to "case __LINE__" */
                    return i;
                    case __LINE__:;/* resume control straight after the return */
               }
     }
}

这样一来我们可以用宏提炼出一种范式,封装成组件:

#define Begin() static int state=0; switch(state) { case 0:
#define Yield(x) do { state=__LINE__; return x; case __LINE__:; } while (0)
#define End() }
int function(void) {
     static int i;
     Begin();
     for (i = 0; i < 10; i++)
          Yield(i);
     End();
}

怎么样,看起来像不像发明了一种全新的语言?实际上我们利用了 switch-case 的分支跳转特性,以及预编译的 LINE 宏,实现了一种隐式状态机,最终实现了“yield 语义”。

值得一提的是,这种协程实现方法有个使用上的局限,就是协程调度状态的保存依赖于 static 变量,而不是堆栈上的局部变量,实际上也无法用局部变量(堆栈)来保存状态,这就使得代码不具备可重入性和多线程应用。后来作者补充了一种技巧,就是将局部变量包装成函数参数传入的一个虚构的上下文结构体指针,然后用动态分配的堆来“模拟”堆栈,解决了线程可重入问题。但这样一来反而有损代码清晰,比如所有局部变量都要写成对象成员的引用方式,特别是局部变量很多的时候很麻烦,再比如宏定义 malloc/free 的玩法过于托大,不易控制。

我们用ILSpy去观察C#中关于yield return的实现,就会发现,其实C#中的yield语义实现跟上面的方式原理上几乎是一样的,只是它单独把每个带有yield return的函数封转成了一个类型实例,用实例里面的一个成员变量代替上面的static状态变量。上面实现方法,其实代码挺猥琐的,而这些实现了yield return的语言,只是把这些猥琐的事交给编译器干了。

参考文献:

1.http://bbs.linuxtone.org/archiver/?tid-21967.html

2.https://yq.aliyun.com/articles/53673

3.https://segmentfault.com/a/1190000001813992

4.http://www.kuqin.com/shuoit/20140128/337902.html

5.http://www.kuqin.com/shuoit/20141013/342602.html

转载于:https://my.oschina.net/seanx/blog/710092

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值