Duilib多线程任务调度:使用线程池提升应用响应速度

Duilib多线程任务调度:使用线程池提升应用响应速度

【免费下载链接】duilib 【免费下载链接】duilib 项目地址: https://gitcode.com/gh_mirrors/du/duilib

1. 引言:界面卡顿的元凶与解决方案

你是否曾遇到过这样的情况:当你的Duilib应用程序在执行耗时操作(如下载文件、处理大数据或进行复杂计算)时,用户界面变得卡顿甚至无响应?这是因为在传统的单线程模型中,耗时操作会阻塞UI线程,导致界面无法及时响应用户输入。

本文将详细介绍如何在Duilib应用中实现高效的多线程任务调度,通过线程池(ThreadPool)技术将耗时操作从UI线程中分离出来,显著提升应用程序的响应速度和用户体验。

读完本文后,你将能够:

  • 理解Duilib应用中的线程模型和UI响应原理
  • 掌握线程池的设计与实现方法
  • 学会如何在Duilib中集成线程池处理耗时任务
  • 解决多线程环境下的UI更新问题
  • 优化线程池性能以适应不同的应用场景

2. Duilib线程模型与UI响应原理

2.1 Windows消息循环机制

在Windows系统中,所有GUI应用程序都基于消息循环(Message Loop)机制运行。Duilib应用程序也不例外,其UI线程负责处理所有的用户输入和界面绘制操作。

// 典型的Windows消息循环
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

2.2 UI线程阻塞问题

当在UI线程中执行耗时操作时,消息循环会被阻塞,导致界面无法及时响应用户输入和重绘请求,从而产生卡顿现象。

// 错误示例:在UI线程中执行耗时操作
void CMyWnd::OnButtonClick(TNotifyUI& msg)
{
    // 模拟耗时操作(5秒)
    Sleep(5000);  // 这将导致界面卡顿5秒
    
    // 更新UI
    m_pLabel->SetText(_T("操作完成"));
}

2.3 多线程解决方案

解决UI卡顿的关键是将耗时操作从UI线程中分离出来,在后台线程中执行。Duilib应用程序通常采用以下几种多线程方案:

  1. 独立线程:为每个耗时操作创建一个新线程
  2. 线程池:维护一组可重用的线程,用于执行多个任务
  3. 异步操作:使用Windows API(如BeginAsyncResult)或C++11及以上标准中的异步机制

3. 线程池设计与实现

3.1 线程池核心组件

一个典型的线程池实现包含以下核心组件:

mermaid

3.2 线程池实现代码

以下是一个基于C++11标准的线程池实现:

// ThreadPool.h
#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
    // 构造函数,创建指定数量的工作线程
    ThreadPool(size_t threads);
    
    // 析构函数,停止所有工作线程
    ~ThreadPool();
    
    // 提交任务到线程池,返回future对象用于获取结果
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;
        
    // 停止线程池
    void stop();
    
    // 获取当前等待的任务数量
    size_t getTaskCount() const;

private:
    // 工作线程集合
    std::vector< std::thread > workers;
    
    // 任务队列
    std::queue< std::function<void()> > tasks;
    
    // 同步机制
    std::mutex queue_mutex;
    std::condition_variable condition;
    
    // 线程池是否运行中
    bool stop_flag;
    
    // 最大线程数
    size_t max_threads;
};

#endif // THREAD_POOL_H
// ThreadPool.cpp
#include "ThreadPool.h"

// 构造函数,创建工作线程
ThreadPool::ThreadPool(size_t threads) 
    : stop_flag(false), max_threads(threads) {
    for(size_t i = 0; i < threads; ++i)
        workers.emplace_back(
            [this] {
                for(;;) {
                    std::function<void()> task;
                    
                    // 加锁获取任务
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this]{ return this->stop_flag || !this->tasks.empty(); });
                        
                        // 如果线程池已停止且任务队列为空,则退出
                        if(this->stop_flag && this->tasks.empty())
                            return;
                            
                        // 从队列中取出任务
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    
                    // 执行任务
                    task();
                }
            }
        );
}

// 析构函数,停止所有工作线程
ThreadPool::~ThreadPool() {
    stop();
}

// 停止线程池
void ThreadPool::stop() {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop_flag = true;
    }
    
    // 唤醒所有等待的线程
    condition.notify_all();
    
    // 等待所有工作线程完成
    for(std::thread &worker: workers)
        worker.join();
}

// 提交任务到线程池
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type> {
    
    using return_type = typename std::result_of<F(Args...)>::type;
    
    // 创建一个packaged_task,用于获取任务执行结果
    auto task = std::make_shared< std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );
    
    // 获取future对象,用于获取任务执行结果
    std::future<return_type> res = task->get_future();
    
    // 将任务加入队列
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        
        // 如果线程池已停止,则不能添加新任务
        if(stop_flag)
            throw std::runtime_error("enqueue on stopped ThreadPool");
            
        tasks.emplace([task](){ (*task)(); });
    }
    
    // 唤醒一个等待的工作线程
    condition.notify_one();
    
    return res;
}

// 获取当前等待的任务数量
size_t ThreadPool::getTaskCount() const {
    std::unique_lock<std::mutex> lock(queue_mutex);
    return tasks.size();
}

4. 在Duilib中集成线程池

4.1 线程池单例模式

为了在整个应用程序中方便地使用线程池,我们可以将其实现为单例模式:

// ThreadPoolManager.h
#ifndef THREAD_POOL_MANAGER_H
#define THREAD_POOL_MANAGER_H

#include "ThreadPool.h"
#include <memory>

class ThreadPoolManager {
public:
    // 获取单例实例
    static ThreadPoolManager& getInstance() {
        static ThreadPoolManager instance;
        return instance;
    }
    
    // 禁止拷贝构造和赋值操作
    ThreadPoolManager(const ThreadPoolManager&) = delete;
    ThreadPoolManager& operator=(const ThreadPoolManager&) = delete;
    
    // 初始化线程池
    void init(size_t maxThreads = std::thread::hardware_concurrency()) {
        m_threadPool = std::make_unique<ThreadPool>(maxThreads);
    }
    
    // 获取线程池
    ThreadPool& getThreadPool() {
        if (!m_threadPool) {
            init(); // 如果未初始化,则使用默认参数初始化
        }
        return *m_threadPool;
    }
    
    // 停止线程池
    void stop() {
        if (m_threadPool) {
            m_threadPool->stop();
            m_threadPool.reset();
        }
    }
    
private:
    // 私有构造函数
    ThreadPoolManager() = default;
    
    // 线程池实例
    std::unique_ptr<ThreadPool> m_threadPool;
};

#endif // THREAD_POOL_MANAGER_H

4.2 任务调度与UI更新

在Duilib中使用线程池时,需要特别注意:不能在非UI线程中直接更新UI控件。正确的做法是通过消息机制,将UI更新操作投递到UI线程执行。

// TaskScheduler.h
#ifndef TASK_SCHEDULER_H
#define TASK_SCHEDULER_H

#include "ThreadPoolManager.h"
#include <functional>
#include <windows.h>

// 任务类型定义
using Task = std::function<void()>;
using UITask = std::function<void()>;

class TaskScheduler {
public:
    // 提交后台任务
    template<class F, class... Args>
    static auto submitTask(F&& f, Args&&... args) {
        return ThreadPoolManager::getInstance().getThreadPool().enqueue(
            std::forward<F>(f), std::forward<Args>(args)...
        );
    }
    
    // 提交UI任务(在UI线程执行)
    static void submitUITask(HWND hwnd, UITask task) {
        if (!hwnd || !task) return;
        
        // 创建一个包装任务,用于在UI线程执行
        auto* pTask = new UITask(std::move(task));
        
        // 通过Windows消息将任务投递到UI线程
        PostMessage(hwnd, WM_EXECUTE_UI_TASK, 0, (LPARAM)pTask);
    }
    
    // 处理UI任务消息
    static LRESULT handleUITaskMessage(WPARAM wParam, LPARAM lParam) {
        if (lParam) {
            UITask* pTask = reinterpret_cast<UITask*>(lParam);
            if (pTask) {
                (*pTask)(); // 执行UI任务
                delete pTask; // 释放任务对象
            }
        }
        return 0;
    }
};

// 自定义消息:用于执行UI任务
#define WM_EXECUTE_UI_TASK (WM_USER + 1001)

#endif // TASK_SCHEDULER_H

4.3 在Duilib窗口中集成

以下是如何在Duilib窗口类中集成线程池和任务调度器:

// MainWindow.h
#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H

#include "WinImplBase.h"
#include "TaskScheduler.h"

class MainWindow : public WindowImplBase {
public:
    MainWindow() : m_hwnd(nullptr) {}
    
    // 获取窗口类名
    virtual LPCTSTR GetWindowClassName() const override { return _T("MainWindow"); }
    
    // 获取窗口大小
    virtual CDuiRect GetWindowRect() const override { return CDuiRect(0, 0, 800, 600); }
    
    // 初始化窗口
    virtual void InitWindow() override {
        m_hwnd = GetHWND();
        
        // 初始化线程池
        ThreadPoolManager::getInstance().init(4); // 创建4个工作线程
        
        // 获取UI控件
        m_pStatusLabel = static_cast<CLabelUI*>(m_PaintManager.FindControl(_T("status_label")));
        m_pStartBtn = static_cast<CButtonUI*>(m_PaintManager.FindControl(_T("start_btn")));
        m_pProgress = static_cast<CProgressUI*>(m_PaintManager.FindControl(_T("progress")));
    }
    
    // 处理控件事件
    virtual void Notify(TNotifyUI& msg) override {
        if (msg.sType == _T("click")) {
            if (msg.pSender == m_pStartBtn) {
                OnStartButtonClick();
            }
        }
    }
    
    // 消息处理
    virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override {
        if (uMsg == WM_EXECUTE_UI_TASK) {
            return TaskScheduler::handleUITaskMessage(wParam, lParam);
        }
        return WindowImplBase::HandleMessage(uMsg, wParam, lParam);
    }
    
private:
    // 开始按钮点击事件
    void OnStartButtonClick() {
        if (!m_pStatusLabel || !m_pProgress) return;
        
        // 更新UI状态
        m_pStartBtn->SetEnabled(false);
        m_pStatusLabel->SetText(_T("任务开始执行..."));
        m_pProgress->SetValue(0);
        
        // 提交后台任务
        auto future = TaskScheduler::submitTask([this]() {
            // 模拟耗时操作(分10步)
            for (int i = 1; i <= 10; ++i) {
                // 模拟每步耗时500毫秒
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
                
                // 更新进度(通过UI任务)
                int progress = i * 10;
                TaskScheduler::submitUITask(m_hwnd, [this, progress]() {
                    if (m_pProgress) {
                        m_pProgress->SetValue(progress);
                    }
                });
            }
            
            // 任务完成后更新UI(通过UI任务)
            TaskScheduler::submitUITask(m_hwnd, [this]() {
                if (m_pStatusLabel) {
                    m_pStatusLabel->SetText(_T("任务完成!"));
                }
                if (m_pStartBtn) {
                    m_pStartBtn->SetEnabled(true);
                }
            });
        });
    }
    
private:
    HWND m_hwnd;
    CLabelUI* m_pStatusLabel = nullptr;
    CButtonUI* m_pStartBtn = nullptr;
    CProgressUI* m_pProgress = nullptr;
};

#endif // MAIN_WINDOW_H

5. 高级应用:任务优先级与取消机制

5.1 支持优先级的任务队列

在某些场景下,我们可能需要为不同任务设置不同的优先级。以下是一个支持优先级的任务队列实现:

// PriorityTaskQueue.h
#ifndef PRIORITY_TASK_QUEUE_H
#define PRIORITY_TASK_QUEUE_H

#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <memory>

// 任务优先级枚举
enum class TaskPriority {
    LOW,    // 低优先级
    NORMAL, // 正常优先级
    HIGH,   // 高优先级
    URGENT  // 紧急优先级
};

// 带优先级的任务
struct PriorityTask {
    using TaskFunc = std::function<void()>;
    
    TaskPriority priority;
    TaskFunc task;
    std::shared_ptr<bool> cancel_flag; // 用于取消任务
    
    // 构造函数
    PriorityTask(TaskPriority p, TaskFunc f)
        : priority(p), task(std::move(f)), cancel_flag(std::make_shared<bool>(false)) {}
        
    // 比较运算符,用于优先级排序
    bool operator<(const PriorityTask& other) const {
        // 注意:priority_queue是最大堆,所以这里使用大于号,使高优先级任务排在前面
        return priority < other.priority;
    }
    
    // 检查任务是否已取消
    bool isCanceled() const {
        return *cancel_flag;
    }
    
    // 取消任务
    void cancel() {
        *cancel_flag = true;
    }
};

// 支持优先级的任务队列
class PriorityTaskQueue {
public:
    // 添加任务
    void push(PriorityTask task) {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_tasks.push(std::move(task));
        m_condition.notify_one();
    }
    
    // 获取任务
    PriorityTask pop() {
        std::unique_lock<std::mutex> lock(m_mutex);
        m_condition.wait(lock, [this] { return !m_tasks.empty(); });
        
        auto task = std::move(m_tasks.top());
        m_tasks.pop();
        return task;
    }
    
    // 检查队列是否为空
    bool empty() const {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_tasks.empty();
    }
    
    // 获取队列大小
    size_t size() const {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_tasks.size();
    }
    
private:
    std::priority_queue<PriorityTask> m_tasks; // 优先级队列
    mutable std::mutex m_mutex;                // 互斥锁
    std::condition_variable m_condition;       // 条件变量
};

#endif // PRIORITY_TASK_QUEUE_H

5.2 支持优先级和取消的线程池

基于上述优先级任务队列,我们可以实现一个支持任务优先级和取消机制的高级线程池:

// AdvancedThreadPool.h
#ifndef ADVANCED_THREAD_POOL_H
#define ADVANCED_THREAD_POOL_H

#include "PriorityTaskQueue.h"
#include <vector>
#include <thread>
#include <mutex>
#include <future>
#include <functional>

class AdvancedThreadPool {
public:
    // 构造函数
    AdvancedThreadPool(size_t maxThreads) : m_stop(false) {
        for (size_t i = 0; i < maxThreads; ++i) {
            m_workers.emplace_back([this] {
                for (;;) {
                    PriorityTask task = m_queue.pop();
                    
                    // 检查线程池是否已停止
                    if (m_stop) {
                        // 将任务放回队列,以便其他线程处理
                        m_queue.push(std::move(task));
                        return;
                    }
                    
                    // 检查任务是否已取消
                    if (!task.isCanceled()) {
                        task.task(); // 执行任务
                    }
                }
            });
        }
    }
    
    // 析构函数
    ~AdvancedThreadPool() {
        stop();
    }
    
    // 停止线程池
    void stop() {
        m_stop = true;
        m_queue.push(PriorityTask(TaskPriority::LOW, []{})); // 添加一个空任务唤醒所有线程
        
        for (std::thread& worker : m_workers) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }
    
    // 提交任务,返回可用于取消任务的对象
    std::shared_ptr<bool> enqueue(PriorityTask task) {
        if (m_stop) {
            throw std::runtime_error("enqueue on stopped AdvancedThreadPool");
        }
        
        auto cancel_flag = task.cancel_flag;
        m_queue.push(std::move(task));
        return cancel_flag;
    }
    
    // 提交任务的便捷方法
    template<class F, class... Args>
    std::shared_ptr<bool> enqueue(TaskPriority priority, F&& f, Args&&... args) {
        using return_type = typename std::result_of<F(Args...)>::type;
        
        auto task = PriorityTask(priority, [f = std::forward<F>(f), args = std::make_tuple(std::forward<Args>(args)...)]() mutable {
            std::apply(std::move(f), std::move(args));
        });
        
        return enqueue(std::move(task));
    }
    
    // 获取任务数量
    size_t getTaskCount() const {
        return m_queue.size();
    }
    
private:
    std::vector<std::thread> m_workers;
    PriorityTaskQueue m_queue;
    std::atomic<bool> m_stop;
    mutable std::mutex m_mutex;
};

#endif // ADVANCED_THREAD_POOL_H

6. 性能优化与最佳实践

6.1 线程池大小的选择

线程池的最佳大小取决于应用场景和硬件环境:

mermaid

  • CPU密集型任务:线程数 = CPU核心数 ± 1
  • IO密集型任务:线程数 = CPU核心数 × 2 或更高
  • 混合任务:根据实际情况调整,通常取CPU核心数的1.5~2倍

可以通过以下代码获取系统CPU核心数:

// 获取CPU核心数
size_t getCpuCoreCount() {
    return std::thread::hardware_concurrency();
}

6.2 任务粒度控制

任务粒度(Task Granularity)是指任务的大小和执行时间。合理的任务粒度对线程池性能有重要影响:

任务粒度优点缺点适用场景
细粒度负载均衡好,资源利用率高任务调度开销大大量小任务
粗粒度调度开销小可能导致负载不均衡少量大任务
中粒度平衡调度开销和负载均衡-大多数场景

6.3 避免线程安全问题

在多线程编程中,需要特别注意线程安全问题:

  1. 共享数据保护:使用互斥锁(mutex)保护共享数据的访问
  2. 无锁编程:对于简单数据,可使用原子操作(atomic)
  3. 线程局部存储:使用thread_local存储线程私有数据
  4. 不可变对象:设计不可变的数据结构,避免同步问题
// 线程安全的计数器示例
class ThreadSafeCounter {
private:
    std::mutex m_mutex;
    int m_count = 0;
    
public:
    // 增加计数
    void increment() {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_count++;
    }
    
    // 获取计数
    int get() const {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_count;
    }
};

// 原子操作示例
std::atomic<int> atomic_counter(0);

6.4 任务监控与错误处理

为线程池添加任务监控和错误处理机制:

// 带监控功能的任务包装器
template<class F>
auto monitored_task(const std::string& taskName, F&& f) {
    return [taskName, f = std::forward<F>(f)]() {
        auto start_time = std::chrono::high_resolution_clock::now();
        
        try {
            f(); // 执行任务
            
            auto end_time = std::chrono::high_resolution_clock::now();
            auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
            
            // 记录任务完成日志
            OutputDebugStringA(("Task " + taskName + " completed in " + std::to_string(duration.count()) + "ms\n").c_str());
        } catch (const std::exception& e) {
            // 记录任务异常日志
            OutputDebugStringA(("Task " + taskName + " failed: " + e.what() + "\n").c_str());
        } catch (...) {
            // 记录未知异常日志
            OutputDebugStringA(("Task " + taskName + " failed with unknown exception\n").c_str());
        }
    };
}

// 使用示例
TaskScheduler::submitTask(monitored_task("file_download", []() {
    // 执行文件下载任务
}));

7. 总结与展望

7.1 本文要点回顾

  • UI线程阻塞问题:耗时操作会导致界面卡顿,需要使用多线程解决
  • 线程池优势:相比独立线程,线程池减少了线程创建销毁开销,提高了资源利用率
  • Duilib多线程实践:通过自定义消息在后台线程和UI线程间通信,安全更新UI
  • 高级特性:实现支持任务优先级和取消机制的高级线程池
  • 性能优化:根据任务类型选择合适的线程池大小,控制任务粒度,避免线程安全问题

7.2 多线程编程挑战

  • 死锁:多个线程相互等待资源导致程序卡死
  • 竞态条件:多个线程同时访问共享数据导致数据不一致
  • 线程安全:确保多线程环境下数据访问的正确性
  • 调试困难:多线程问题难以复现和调试

7.3 未来发展方向

  • 协程:使用C++20协程(Coroutine)进一步优化异步编程
  • 任务窃取:实现工作窃取(Work Stealing)算法提高线程利用率
  • 自适应线程池:根据系统负载和任务类型自动调整线程数量
  • GPU加速:将计算密集型任务卸载到GPU执行

8. 示例应用:多线程文件下载器

为了更好地理解如何在Duilib中应用线程池技术,我们来实现一个多线程文件下载器:

// FileDownloader.h
#ifndef FILE_DOWNLOADER_H
#define FILE_DOWNLOADER_H

#include <vector>
#include <string>
#include <memory>
#include <functional>
#include "AdvancedThreadPool.h"
#include "TaskScheduler.h"

// 下载进度回调
using ProgressCallback = std::function<void(int percent, const std::string& filename)>;

// 文件下载器
class FileDownloader {
public:
    // 构造函数
    FileDownloader(HWND hwnd, size_t maxThreads = 4) 
        : m_hwnd(hwnd), m_threadPool(maxThreads) {}
        
    // 析构函数
    ~FileDownloader() {
        cancelAll();
    }
    
    // 添加下载任务
    std::shared_ptr<bool> addDownloadTask(const std::string& url, const std::string& savePath, 
                                         ProgressCallback callback = nullptr,
                                         TaskPriority priority = TaskPriority::NORMAL) {
        // 创建下载任务
        auto task = [this, url, savePath, callback]() {
            // 模拟文件下载过程
            for (int i = 0; i <= 100; i += 5) {
                // 检查任务是否已取消
                if (m_currentCancelFlag && *m_currentCancelFlag) {
                    TaskScheduler::submitUITask(m_hwnd, [callback, savePath]() {
                        if (callback) callback(-1, savePath); // -1表示取消
                    });
                    return;
                }
                
                // 模拟下载延迟
                std::this_thread::sleep_for(std::chrono::milliseconds(100));
                
                // 报告进度
                int progress = i;
                std::string filename = savePath;
                
                TaskScheduler::submitUITask(m_hwnd, [callback, progress, filename]() {
                    if (callback) callback(progress, filename);
                });
            }
            
            // 下载完成
            TaskScheduler::submitUITask(m_hwnd, [callback, savePath]() {
                if (callback) callback(100, savePath); // 100表示完成
            });
        };
        
        // 提交任务
        m_currentCancelFlag = m_threadPool.enqueue(priority, std::move(task));
        return m_currentCancelFlag;
    }
    
    // 取消当前下载
    void cancelCurrent() {
        if (m_currentCancelFlag) {
            *m_currentCancelFlag = true;
        }
    }
    
    // 取消所有下载
    void cancelAll() {
        cancelCurrent();
        m_threadPool.stop();
    }
    
private:
    HWND m_hwnd;
    AdvancedThreadPool m_threadPool;
    std::shared_ptr<bool> m_currentCancelFlag;
};

#endif // FILE_DOWNLOADER_H

通过本文介绍的线程池技术,你可以显著提升Duilib应用程序的响应速度和用户体验。多线程编程虽然复杂,但只要掌握了核心原理和最佳实践,就能轻松应对各种挑战。

希望本文对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论。别忘了点赞、收藏和关注,以便获取更多Duilib开发技巧和最佳实践!

下一篇文章预告:《Duilib皮肤系统深度剖析:从原理到实践》

【免费下载链接】duilib 【免费下载链接】duilib 项目地址: https://gitcode.com/gh_mirrors/du/duilib

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值