多线程信号量(环形队列)下的生产者-消费者模型详解

目录

一、模型的基本概念

二、模型的核心问题

三、环形队列的实现

(一)数据结构

(二)同步机制

(三)生产者操作

(四)消费者操作

(五)信号量操作封装

(六)析构函数

四、生产者和消费者的实现

(一)任务类 Task

(二)生产者线程

(三)消费者线程

(四)主函数

五、信号量的实现

(一)信号量的初始化

(二)信号量的操作

(三)信号量的销毁

六、环形队列的实现细节

(一)生产者操作

(二)消费者操作

(三)环形队列的处理

(四)线程安全

(五)信号量的作用

七、任务类的实现细节

(一)构造函数

(二)run 方法

(三)GetTask 和 GetResult 方法

八、主函数的实现细节

九、总结


一、模型的基本概念

生产者消费者模型是一种经典的多线程同步问题模型,用于解决线程之间数据共享与同步的问题。它包含三个核心组成部分:

  1. 生产者(Producer)

    • 定义:生产者是负责生成数据的线程或进程。

    • 作用:生产者会不断地生成数据,并将这些数据放入一个共享缓冲区中。

    • 限制:如果共享缓冲区已经满了,生产者无法再添加数据,必须等待缓冲区有空闲位置。

  2. 消费者(Consumer)

    • 定义:消费者是负责从共享缓冲区中取出数据并进行处理的线程或进程。

    • 作用:消费者会不断地从共享缓冲区中取出数据,并对这些数据进行处理。

    • 限制:如果共享缓冲区为空,消费者无法取出数据,必须等待缓冲区中有数据可供消费。

  3. 共享缓冲区(Buffer)

    • 定义:共享缓冲区是一个有限容量的队列,用于存储生产者生成的数据。

    • 作用:共享缓冲区是生产者和消费者之间共享的存储空间,用于数据的传递。

    • 限制:共享缓冲区的容量是有限的,不能无限扩展。

总结一句话:
321原则:3种关系(生产者vs生产者,消费者vs消费者,生产者vs消费者),2种角色(生产者,消费者),1个场所(特定结构的内存空间)

二、模型的核心问题

生产者和消费者在运行过程中会遇到以下核心问题:

  1. 生产者不能向空缓冲区添加数据

    • 问题描述:如果共享缓冲区已经满了,生产者无法再添加数据。

    • 解决方案:生产者需要等待,直到缓冲区有空闲位置。这通常通过同步机制(如信号量或条件变量)来实现。

  2. 消费者不能从空缓冲区取数据

    • 问题描述:如果共享缓冲区为空,消费者无法取出数据。

    • 解决方案:消费者需要等待,直到缓冲区中有数据可供消费。这同样通过同步机制来实现。

  3. 多线程同步问题

    • 问题描述:生产者和消费者是并发运行的线程,需要确保对共享缓冲区的访问是线程安全的。

    • 解决方案:使用互斥锁(Mutex)或其他同步机制来保护共享缓冲区的访问,避免数据竞争和数据不一致的问题。

三、环形队列的实现

(一)数据结构

环形队列是一种先进先出(FIFO)的数据结构,非常适合实现生产者消费者模型。环形队列通过两个指针 _c_pos_p_pos 分别表示消费者和生产者的当前位置,避免了队列头部和尾部的频繁移动,提高了效率。

int _maxsize; // 队列的最大容量
int _c_pos; // 消费者下标
int _p_pos; // 生产者下标
std::vector<T> _ringqueue; // 环形队列

(二)同步机制

为了确保线程安全和高效的线程间通信,我们使用信号量和互斥锁来实现同步机制。

信号量(sem_t

信号量是一种同步原语,用于控制对共享资源的访问。信号量维护一个计数器,表示可用资源的数量。信号量的操作包括:

  • P 操作(sem_wait:等待信号量的值大于 0,然后将其减 1。

  • V 操作(sem_post:将信号量的值加 1。

在生产者消费者模型中,我们使用两个信号量:

  • _cdata_sem:消费者关注的数据资源,初始值为 0。

  • _pspace_sem:生产者关注的空间资源,初始值为队列的最大容量。

sem_t _cdata_sem; // 消费者关注的数据资源
sem_t _pspace_sem; // 生产者关注的空间资源

互斥锁(pthread_mutex_t

互斥锁用于保护对共享资源的访问,确保同一时间只有一个线程可以修改共享资源。在环形队列中,我们使用两个互斥锁:

  • _c_mutex:保护消费者操作的互斥锁。

  • _p_mutex:保护生产者操作的互斥锁。

pthread_mutex_t _c_mutex; // 消费者互斥锁
pthread_mutex_t _p_mutex; // 生产者互斥锁

(三)生产者操作

生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:

  1. 等待空间资源(_pspace_sem)。

  2. 加锁后将数据放入队列,并更新生产者指针。

  3. 增加数据资源(_cdata_sem)。

void push(const T &in)
{
    p(_pspace_sem); // 等待空间资源
    lock(_p_mutex); // 加锁
    _ringqueue[_p_pos] = in; // 放入数据
    _p_pos++; // 更新生产者指针
    _p_pos %= _maxsize; // 环形处理
    unlock(_p_mutex); // 解锁
    v(_cdata_sem); // 增加数据资源
}

关键点解释

  1. 等待空间资源

    • 生产者在队列满时需要等待,直到有空间可用。通过调用 p(_pspace_sem),生产者线程会阻塞,直到信号量 _pspace_sem 的值大于 0。

  2. 加锁

    • 使用 lock(_p_mutex) 锁定生产者操作,确保线程安全。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。

  3. 放入数据

    • 将数据放入队列的当前位置 _p_pos,然后更新生产者指针 _p_pos。通过取模运算 _p_pos %= _maxsize,实现环形队列。

  4. 增加数据资源

    • 调用 v(_cdata_sem) 增加数据资源,通知消费者队列中有新的数据可用。

(四)消费者操作

消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:

  1. 等待数据资源(_cdata_sem)。

  2. 加锁后从队列中取出数据,并更新消费者指针。

  3. 增加空间资源(_pspace_sem)。

void pop(T *out)
{
    p(_cdata_sem); // 等待数据资源
    lock(_c_mutex); // 加锁
    *out = _ringqueue[_c_pos]; // 取出数据
    _c_pos++; // 更新消费者指针
    _c_pos %= _maxsize; // 环形处理
    unlock(_c_mutex); // 解锁
    v(_pspace_sem); // 增加空间资源
}

关键点解释

  1. 等待数据资源

    • 消费者在队列空时需要等待,直到有数据可用。通过调用 p(_cdata_sem),消费者线程会阻塞,直到信号量 _cdata_sem 的值大于 0。

  2. 加锁

    • 使用 lock(_c_mutex) 锁定消费者操作,确保线程安全。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。

  3. 取出数据

    • 从队列的当前位置 _c_pos 取出数据,然后更新消费者指针 _c_pos。通过取模运算 _c_pos %= _maxsize,实现环形队列。

  4. 增加空间资源

    • 调用 v(_pspace_sem) 增加空间资源,通知生产者队列中有新的空间可用。

(五)信号量操作封装

为了使代码更加简洁易读,我们封装了信号量的 P 操作(p)和 V 操作(v),以及互斥锁的加锁(lock)和解锁(unlock)操作。

void p(sem_t &sem) { sem_wait(&sem); }
void v(sem_t &sem) { sem_post(&sem); }
void lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); }
void unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); }

(六)析构函数

在析构函数中,销毁信号量和互斥锁,释放资源。

~RingQueue()
{
    sem_destroy(&_cdata_sem); // 销毁消费者信号量
    sem_destroy(&_pspace_sem); // 销毁生产者信号量
    pthread_mutex_destroy(&_c_mutex); // 销毁消费者互斥锁
    pthread_mutex_destroy(&_p_mutex); // 销毁生产者互斥锁
}

四、生产者和消费者的实现

(一)任务类 Task

任务类 Task 用于表示生产者生成的任务。每个任务包含两个操作数、一个操作符和计算结果。通过 run 方法执行任务,并通过 GetTaskGetResult 方法分别获取任务的描述和执行结果。

(二)生产者线程

生产者线程负责生成任务,并将任务放入环形队列中。生产者线程的实现步骤如下:

  1. 随机生成两个操作数和一个操作符,创建一个 Task 对象。

  2. 调用 push 方法将任务放入队列。

  3. 打印生成的任务信息。

void* Productor(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    RingQueue<Task>* rq = td->rq;
    std::string name = td->threadname;

    while (true)
    {
        int data1 = rand() % 10 + 1;
        int data2 = rand() % 10;
        char op = opers[rand() % 5];
        Task t(data1, data2, op);

        rq->push(t);
        std::cout << "生产任务的id是: " << name << " 生产了一个任务: " << t.GetTask() << std::endl;
    }
}

(三)消费者线程

消费者线程负责从环形队列中取出任务,并执行任务。消费者线程的实现步骤如下:

  1. 调用 pop 方法从队列中取出任务。

  2. 执行任务。

  3. 打印任务的执行结果。

void* Consumer(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    RingQueue<Task>* rq = td->rq;
    std::string name = td->threadname;

    while (true)
    {
        Task t;
        rq->pop(&t);
        t.run();
        std::cout << "消费任务的id是: " << name << " 任务的结果: " << t.GetResult() << std::endl;
        sleep(1);
    }
}

(四)主函数

主函数负责初始化环形队列,创建生产者和消费者线程,并启动线程。主函数的实现步骤如下:

  1. 初始化随机数种子。

  2. 创建环形队列对象。

  3. 创建消费者线程和生产者线程。

  4. 等待线程结束(理论上不会结束)。

  5. 释放资源。

五、信号量的实现

信号量是一种同步原语,用于控制对共享资源的访问。信号量维护一个计数器,表示可用资源的数量。信号量的操作包括:

  • P 操作(sem_wait:等待信号量的值大于 0,然后将其减 1。

  • V 操作(sem_post:将信号量的值加 1。

在生产者消费者模型中,我们使用两个信号量:

  • _cdata_sem:消费者关注的数据资源,初始值为 0。

  • _pspace_sem:生产者关注的空间资源,初始值为队列的最大容量。

(一)信号量的初始化

在环形队列的构造函数中,初始化信号量和互斥锁。

RingQueue(int size = defaultnum)
    : _ringqueue(size), _maxsize(size), _c_pos(0), _p_pos(0)
{
    sem_init(&_cdata_sem, 0, 0); // 初始化消费者信号量
    sem_init(&_pspace_sem, 0, _maxsize); // 初始化生产者信号量
    pthread_mutex_init(&_c_mutex, nullptr); // 初始化消费者互斥锁
    pthread_mutex_init(&_p_mutex, nullptr); // 初始化生产者互斥锁
}

(二)信号量的操作

封装了信号量的 P 操作(p)和 V 操作(v),以及互斥锁的加锁(lock)和解锁(unlock)操作。

void p(sem_t &sem) { sem_wait(&sem); }
void v(sem_t &sem) { sem_post(&sem); }
void lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); }
void unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); }

(三)信号量的销毁

在析构函数中,销毁信号量和互斥锁,释放资源。

~RingQueue()
{
    sem_destroy(&_cdata_sem); // 销毁消费者信号量
    sem_destroy(&_pspace_sem); // 销毁生产者信号量
    pthread_mutex_destroy(&_c_mutex); // 销毁消费者互斥锁
    pthread_mutex_destroy(&_p_mutex); // 销毁生产者互斥锁
}

六、环形队列的实现细节

环形队列完整代码:

#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<pthread.h>

const static int defaultnum=5; // 默认队列大小

template<class T>
class RingQueue
{
private:
    int _maxsize; // 队列最大容量
    int _c_pos; // 消费者下标
    int _p_pos; // 生产者下标

    std::vector<T>_ringqueue; // 环形队列存储数据

    sem_t _cdata_sem; // 消费者关注的数据资源信号量
    sem_t _pspace_sem; // 生产者关注的空间资源信号量

    pthread_mutex_t _c_mutex; // 消费者互斥锁,用于保护消费者操作队列的临界区
    pthread_mutex_t _p_mutex; // 生产者互斥锁,用于保护生产者操作队列的临界区
public:
    RingQueue(int size=defaultnum) // 构造函数,初始化队列大小和信号量
        :_ringqueue(_maxsize),_maxsize(size),_c_pos(0),_p_pos(0)
    {
        sem_init(&_cdata_sem,0,0); // 初始化消费者信号量,初始值为0
        sem_init(&_pspace_sem,0,_maxsize); // 初始化生产者信号量,初始值为队列大小
        pthread_mutex_init(&_c_mutex,nullptr); // 初始化消费者互斥锁
        pthread_mutex_init(&_p_mutex,nullptr); // 初始化生产者互斥锁
    }
    void push(const T& in) // 生产者向队列中添加数据
    {
        p(_pspace_sem);

        lock(_p_mutex);
        _ringqueue[_p_pos]=in;
        _p_pos++;
        _p_pos%=_maxsize;
        unlock(_p_mutex);

        v(_cdata_sem);
        
    }
    void pop(T*out) // 消费者从队列中取出数据
    {
        p(_cdata_sem);

        lock(_c_mutex);
        *out=_ringqueue[_c_pos];
        _c_pos++;
        _c_pos%=_maxsize;
        unlock(_c_mutex);

        v(_pspace_sem);
    }
    ~RingQueue() // 析构函数,销毁信号量和互斥锁
    {
        sem_destroy(&_cdata_sem);
        sem_destroy(&_pspace_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    void p(sem_t &sem) // P操作(信号量减1)
    {
        sem_wait(&sem);
    }
    void v(sem_t &sem) // V操作(信号量加1)
    {
        sem_post(&sem);
    }
    void lock(pthread_mutex_t &mutex) // 加锁
    {
        pthread_mutex_lock(&mutex);
    }
    void unlock(pthread_mutex_t &mutex) // 解锁
    {
        pthread_mutex_unlock(&mutex);
    }
};

(一)生产者操作

生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:

  1. 等待空间资源(_pspace_sem)。

  2. 加锁后将数据放入队列,并更新生产者指针。

  3. 增加数据资源(_cdata_sem)。

void push(const T &in)
{
    p(_pspace_sem); // 等待空间资源
    lock(_p_mutex); // 加锁
    _ringqueue[_p_pos] = in; // 放入数据
    _p_pos++; // 更新生产者指针
    _p_pos %= _maxsize; // 环形处理
    unlock(_p_mutex); // 解锁
    v(_cdata_sem); // 增加数据资源
}

(二)消费者操作

消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:

  1. 等待数据资源(_cdata_sem)。

  2. 加锁后从队列中取出数据,并更新消费者指针。

  3. 增加空间资源(_pspace_sem)。

void pop(T *out)
{
    p(_cdata_sem); // 等待数据资源
    lock(_c_mutex); // 加锁
    *out = _ringqueue[_c_pos]; // 取出数据
    _c_pos++; // 更新消费者指针
    _c_pos %= _maxsize; // 环形处理
    unlock(_c_mutex); // 解锁
    v(_pspace_sem); // 增加空间资源
}

(三)环形队列的处理

环形队列通过取模运算 _p_pos %= _maxsize_c_pos %= _maxsize 实现。当指针达到队列的末尾时,通过取模运算将其重置为队列的开头,从而实现环形队列。

(四)线程安全

生产者线程在将任务放入队列时,会先等待 _pspace_sem 信号量,确保队列中有可用空间。然后,它会加锁 _p_mutex,将任务放入队列,并更新生产者指针 _p_pos。最后,它会增加 _cdata_sem 信号量,通知消费者队列中有新的数据可用。
消费者线程在从队列中取出任务时,会先等待 _cdata_sem 信号量,确保队列中有可用数据。然后,它会加锁 _c_mutex,从队列中取出任务,并更新消费者指针 _c_pos。最后,它会增加 _pspace_sem 信号量,通知生产者队列中有新的空间可用。

生产者和消费者分别使用自己的互斥锁 _p_mutex_c_mutex,避免了生产者和消费者之间的直接冲突。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。

(五)信号量的作用

信号量用于同步生产者和消费者的行为:

  • _pspace_sem:表示队列中可用的空间数量。初始值为队列的最大容量 _maxsize

  • _cdata_sem:表示队列中可用的数据数量。初始值为 0。

生产者在队列满时等待 _pspace_sem,消费者在队列空时等待 _cdata_sem。当生产者将数据放入队列时,它会增加 _cdata_sem 的值,通知消费者队列中有新的数据可用。当消费者从队列中取出数据时,它会增加 _pspace_sem 的值,通知生产者队列中有新的空间可用。

七、任务类的实现细节

任务类完整代码:

#pragma once
#include <iostream>
#include <string>

std::string opers = "+-*/%"; // 定义全局变量,包含所有可能的操作符

class Task
{
private:
    int _data1;       // 第一个操作数
    int _data2;       // 第二个操作数
    int _result;      // 计算结果
    int _exitcode;    // 退出码,用于表示任务执行的状态
    char _operator;   // 操作符

public:
    Task() // 默认构造函数
    {}

    // 构造函数,初始化任务对象
    Task(int x, int y, char op)
        : _data1(x), _data2(y), _result(0), _operator(op), _exitcode(0)
    {
    }

    // 执行任务的逻辑
    void run()
    {
        switch (_operator) // 根据操作符执行不同的计算逻辑
        {
        case '+': // 加法
            _result = _data1 + _data2;
            break;
        case '-': // 减法
            _result = _data1 - _data2;
            break;
        case '*': // 乘法
            _result = _data1 * _data2;
            break;
        case '/': // 除法
            {
                if (_data2 == 0) // 检查除数是否为0
                    _exitcode = 1; // 设置退出码为1,表示除数为0的错误
                else
                    _result = _data1 / _data2;
            }
            break;
        case '%': // 取模
            {
                if (_data2 == 0) // 检查除数是否为0
                    _exitcode = 1; // 设置退出码为1,表示除数为0的错误
                else
                    _result = _data1 % _data2;
            }
            break;
        default: // 如果操作符无效
            _exitcode = 3; // 设置退出码为3,表示无效操作符
            break;
        }       
    }

    // 重载 () 运算符,方便直接调用任务对象
    void operator()()
    {
        run(); // 调用 run 方法执行任务
    }

    // 获取任务的执行结果,格式化为字符串
    std::string GetResult()
    {
        return std::to_string(_data1) + _operator + std::to_string(_data2) + "=(" + std::to_string(_result) + ") [exit code: " + std::to_string(_exitcode) + "]";
    }

    // 获取任务的描述,格式化为字符串
    std::string GetTask()
    {
        return std::to_string(_data1) + _operator + std::to_string(_data2) + "=?";
    }

    ~Task() // 析构函数
    {
    }
};

任务类 Task 用于表示生产者生成的任务。每个任务包含两个操作数、一个操作符和计算结果。任务类的实现细节如下:

(一)构造函数

任务类的构造函数初始化任务的两个操作数 _data1_data2,以及操作符 _operator

Task(int x, int y, char op)
    : _data1(x), _data2(y), _result(0), _operator(op), _exitcode(0)
{
}

(二)run 方法

run 方法根据操作符执行相应的计算,并将结果存储在 _result 中。如果操作符为除法或取模且第二个操作数为零,设置错误码 _exitcode

void run()
{
    switch (_operator)
    {
    case '+':
        _result = _data1 + _data2;
        break;
    case '-':
        _result = _data1 - _data2;
        break;
    case '*':
        _result = _data1 * _data2;
        break;
    case '/':
        if (_data2 == 0)
            _exitcode = 1; // 除数为零,设置错误码
        else
            _result = _data1 / _data2;
        break;
    case '%':
        if (_data2 == 0)
            _exitcode = 1; // 除数为零,设置错误码
        else
            _result = _data1 % _data2;
        break;
    default:
        _exitcode = 3; // 未知操作符,设置错误码
        break;
    }
}

(三)GetTask 和 GetResult 方法

GetTask 方法返回任务的字符串表示形式,例如 "3+4=?"GetResult 方法返回任务的执行结果,例如 "3+4=(7) [exit code: 0]"

std::string GetTask()
{
    return std::to_string(_data1) + _operator + std::to_string(_data2) + "=?";
}

std::string GetResult()
{
    return std::to_string(_data1) + _operator + std::to_string(_data2) + "=(" + std::to_string(_result) + ") [exit code: " + std::to_string(_exitcode) + "]";
}

八、主函数的实现细节

主函数负责初始化环形队列,创建生产者和消费者线程,并启动线程。

主函数的实现:

#include<iostream>
#include<pthread.h>
#include"RingQueue.hpp"
#include"Task.hpp"
#include<ctime>
#include<unistd.h>
#include<string>

using namespace std;

// 线程数据结构,用于传递给生产者和消费者线程
struct ThreadData
{
    RingQueue<Task>* rq;       // 指向环形队列的指针
    std::string threadname;    // 线程的名称
};

// 生产者线程函数
void* Productor(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    RingQueue<Task>* rq = td->rq;
    std::string name = td->threadname;

    while(true)
    {
        int data1 = rand() % 10 + 1;
        int data2 = rand() % 10;
        char op = opers[rand() % 5];
        Task t(data1, data2, op);
        rq->push(t);
        cout << "生产任务的id是: " << name << " 生产了一个任务: " << t.GetTask() << endl;
    }
    return nullptr;
}

// 消费者线程函数
void* Consumer(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    RingQueue<Task>* rq = td->rq;
    std::string name = td->threadname;

    while(true)
    {
        Task t;
        rq->pop(&t);
        t();
        cout << "消费任务的id是: " << name << " 任务的结果: " << t.GetResult() << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr)); // 初始化随机数种子
    RingQueue<Task>* rq = new RingQueue<Task>(); // 创建一个环形队列对象
    pthread_t c[5], p[3]; // 定义消费者线程数组和生产者线程数组

    // 创建5个消费者线程
    for(int i = 0; i < 5; i++)
    {
        ThreadData* td = new ThreadData();
        td->rq = rq;
        td->threadname = "Consumer-" + std::to_string(i);
        pthread_create(c + i, nullptr, Consumer, td);
    }

    // 创建3个生产者线程
    for(int i = 0; i < 3; i++)
    {
        ThreadData* td = new ThreadData();
        td->rq = rq;
        td->threadname = "Productor-" + std::to_string(i);
        pthread_create(p + i, nullptr, Productor, td);
    }

    // 等待所有消费者线程结束
    for(int i = 0; i < 5; i++)
    {
        pthread_join(c[i], nullptr);
    }

    // 等待所有生产者线程结束
    for(int i = 0; i < 3; i++)
    {
        pthread_join(p[i], nullptr);
    }

    delete rq; // 释放环形队列对象
    return 0;
}

九、总结

通过上述代码,我们实现了一个基于信号量的生产者消费者模型。关键点包括:

  • 环形队列:使用 std::vector 实现,通过两个指针管理生产者和消费者的位置,避免了队列头部和尾部的频繁移动,提高了效率。

  • 信号量:用于同步生产者和消费者的行为,确保队列操作的线程安全。

  • 互斥锁:保护队列操作,防止数据竞争。

  • 任务类:封装了任务的生成和执行逻辑。

这种模型适用于多线程环境中的任务调度和数据处理,通过调整队列大小和线程数量,可以优化性能以满足不同需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值