Linux_实现线程池

【Linux】线程终结篇:线程池以及线程池的实现 什么是线程池呢?线程池一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。线程池的应用场景:1.需要大量的线程来完成任务,且完成任务的时间比较短。WEB服务器完成网页请求这样的务,使用线程池技术是非常合适的。 阅读详情

目录

1、线程池的实现逻辑

2、创建多线程

3、对线程池分配任务 

3.1 任务类

3.2 发送与接收任务

结语 


前言:        

        在Linux下实现一个线程池,线程池就是创建多个线程,然后对这些线程进行管理,并且可以发放任务给到线程池,让线程池完成任务。

1、线程池的实现逻辑

        首先可以把线程池封装成一个类,既然用到了线程,那么必离不开三个关键要素:1、线程id,2、锁,3、条件变量。因此该线程类中必须有这三个成员变量,为了更好的区分线程彼此,可以对每个线程加上对应的名称,比如线程1,线程2,可以用string变量记录线程的名称,因此可以把线程id和string类进行封装成一个线程信息类,该类如下:

struct ThreadInfo
{
    pthread_t tid;//线程id
    std::string name;//线程名称
};

        其次线程池里肯定存在多个线程,即多个线程id,为了管理他们,可以把这些线程id都放入一个容器中,因此可以在线程池类中定义一个vector<ThreadInfo>的成员变量,维护ThreadInfo类,实际上就是维护线程id。然后,给线程池发配任务并不是直接给线程本体发送,而是将任务发送到一块线程池能看到的区域内,线程池只需要在该区域内获取任务即可,发送方并不与线程池进行直接沟通,这就是一个简单的生产消费者模型,而这个区域可以采用容器(队列)的形式实现,即发送方往队列里push数据,线程池从队列中pop数据。

        生产消费者模型如下:


         再者,锁和条件变量是必须有的,因为多线程在访问同一块区域时需要对他们进行约束,即一次只能一个线程访问同一块区域,条件变量的目的是当区域达到极值(空或满)时,可以使用条件变量对该线程进行阻塞等待,等待区域不为满或者不为空时,就可以唤醒阻塞的线程。也就是说,当区域内为空时,线程池会阻塞在条件变量的等待队列中,当区域内为满时,发送方会阻塞在条件变量的等待队列中,可以理解为条件变量让线程访问共同资源时有了顺序性。

        有了上述的逻辑,就可以定义出线程池的类了,线程池类如下:

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

using namespace std;

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 5;//控制线程池的大小

template <class T>
class ThreadPool
{
public:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
 
private:
    vector<ThreadInfo> threads_;
    queue<T> tasks_;//共同区域

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

};

2、创建多线程

        有了线程池类的框架,下一步就是在该类里面使用容器threads_来创建多线程,因为在该类构造的时候,就已经创建了5个结构体ThreadInfo,然后这5个结构体ThreadInfo里面有各有一个tid,也就是说构造的时候就已经创建了5个tid了,我们直接使用这5个tid创建线程即可。

        完善线程池中创建线程的代码:

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

using namespace std;

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 5;//控制线程池的大小

template <class T>
class ThreadPool
{
public:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    //注意该函数要加static,因为类内函数默认有this指针,而执行函数只能有一个形参
    static void *HandlerTask(void *args)
    {
        while (true)
        {
            cout<<"新的线程准备就绪"<<endl;
            sleep(1);
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, nullptr);
        }
    }
    void join()//等待线程
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            cout<<"开始等待新线程"<<endl;
            pthread_join(threads_[i].tid,nullptr);
        }
    }
 
private:
    vector<ThreadInfo> threads_;
    queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

};


         有了上述代码,我们就可以在主函数中先进行测试,主函数代码如下:

#include "threadpool.hpp"

int main()
{
    ThreadPool<int> tp;
    tp.Start();
    tp.join();

    return 0;
}

        运行结果:

         从结果可以看到,程序是正常运行的,下面就可以对线程进行任务分配了。

3、对线程池分配任务 

        这里使用队列作为共享区域的容器,因此发送任务和接收任务对应的操作是入队和出队,即线程池读取任务用pop,给线程池分配任务用push。

3.1 任务类

        在实现pop和push前,先定义一个任务类,用于计算数据并得到结果,该任务类如下:

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

std::string opers="+-*/%";

enum{
    DivZero=1,
    ModZero,
    Unknown
};

class Task
{
public:
    Task()
    {}
    Task(int x, int y, char op) : data1_(x), 
    data2_(y), oper_(op), result_(0), exitcode_(0)
    {
    }
    void run()
    {
        switch (oper_)
        {
        case '+':
            result_ = data1_ + data2_;
            break;
        case '-':
            result_ = data1_ - data2_;
            break;
        case '*':
            result_ = data1_ * data2_;
            break;
        case '/':
            {
                if(data2_ == 0) exitcode_ = DivZero;
                else result_ = data1_ / data2_;
            }
            break;
        case '%':
           {
                if(data2_ == 0) exitcode_ = ModZero;
                else result_ = data1_ % data2_;
            }            break;
        default:
            exitcode_ = Unknown;
            break;
        }
    }
    void operator ()()
    {
        run();
    }
    std::string GetResult()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=";
        r += std::to_string(result_);
        r += "[code: ";
        r += std::to_string(exitcode_);
        r += "]";

        return r;
    }
    std::string GetTask()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=?";
        return r;
    }
    ~Task()
    {
    }

private:
    //oper_表示+-*/%操作符
    //data1_ oper_ data2_ = ?
    int data1_;
    int data2_;
    char oper_;

    int result_;
    int exitcode_;
};

3.2 发送与接收任务

        不管是发送任务还是接收任务,都需要对其进行上锁,因为生产消费者模型不允许发送方和接收方同时对共同区域访问,并且当共享区域里没有数据时线程池就不能在进行pop了,所以共享区域为空要将当前线程放入等待队列。(也可以设置一个阈值作为共享区域的最大容量,那样做的话则push数据时,若达到该阈值也会将发送方线程放入等待队列,但是这里就不设置阈值了)

        发送与接收任务的整体代码如下:

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

using namespace std;

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 5; // 控制线程池的大小

template <class T>
class ThreadPool
{
public:
    // 复用函数
    void Lock() // 申请锁
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock() // 释放锁
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup() // 唤醒线程
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep() // 将线程放入等待队列中
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty() // 判断队列是否为空
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid) // 拿到线程的名称
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    // 注意该函数要加static,因为类内函数默认有this指针,而执行函数只能有一个形参
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            T t = tp->Pop();
            t(); // 调用任务类里的仿函数
            std::cout << name << " 开始计算, "
                      << "结果: " << t.GetResult() << std::endl;
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "线程-" + std::to_string(i + 1);
            //传的是this指针
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    void join()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            cout << "开始等待新线程" << endl;
            pthread_join(threads_[i].tid, nullptr);
        }
    }
    T Pop() // 复用STL队列的pop
    {
        Lock();
        while (tasks_.empty())
        {
            ThreadSleep();
        }

        T t = tasks_.front();
        tasks_.pop();
        Unlock();
        return t;
    }
    void Push(const T &t) // push是给发送方使用的
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }

private:
    vector<ThreadInfo> threads_;
    queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
};

         将功能都封装进入线程池类中,现在就只需要在主函数中调用这些功能即可,主函数代码如下:

#include "threadpool.hpp"
#include "Task.hpp"
#include <ctime>

int main()
{
    ThreadPool<Task> tp;
    tp.Start();
    srand(time(0));

    while(true)
    {
        //1. 构建任务
        int x = rand() % 10 + 1;
        usleep(10);
        int y = rand() % 5;
        char op = opers[rand()%opers.size()];

        Task t(x, y, op);
        //2. 交给线程池处理
        tp.Push(t);
        cout << "main thread make task: " << t.GetTask() << endl;

        sleep(1);
    }

    return 0;
}

        运行结果:

        从结果可以看到,线程池里的每个线程都拿到并执行了分发的任务。

结语 

        以上就是关于线程池的实现和运用,实现线程池的核心在于对线程的基本运用以及对锁和条件变量的理解和运用。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!! 

Linux c++ 线程池及其实现 因为毕业设计里面要用到线程池,所以研究了一下,然后自己用c++实现了一个jib 阅读详情

相关推荐

[Linux] 实现简单的线程池

线程过多会带来调度开销,进而影响缓存的局部性和整体性能;而线程池维护着多个线程,等待着管理者分配可并发执行的任务,避免了在处理短时间任务时创建与销毁线程的代价;static void *Routine(void *arg)----从任务队列中拿任务,然后处理任务。void Put(Task &in)----往任务队列里面往任务,然后唤醒消费者去拿任务。void ThreadPoolInit()----初始化一堆处理任务的线程。void Get(Task &out)----从任务队列里面拿任务。

Darling_sheeps的博客 544

Linux线程池的原理及实现

01 为什么需要线程池 首要明白两个地方: 线程的创建需要内存资源 线程的创建和销毁需要时间资源 显而易见,由于以上两个原因,不得不寻找一个折衷的方式面对多任务的问题。如果我们只创建一定量的线程,且在一个线程执行完某一任务后,重复利用该线程去处理新的任务而不是直接销毁它,那么通过这两个策略我们就可以“朴素”的解决以上两个问题。 线程池的初衷就是想搭建一个有一定数量线程,且可以重复利用这些线程处...

koko_TT的博客 3130

Linux线程池的C语言实现指南

线程池是由一个或多个可重用的线程组成的池,这些线程在池中等待分配任务。当一个新任务提交给线程池时,它不会立即创建一个新的线程来处理该任务。相反,它会将任务分配给池中已存在的线程,或者如果当前没有空闲线程,则将任务加入队列等待。当一个线程完成它的任务后,它会返回到线程池中并准备接受新的任务。在软件架构中,线程池的工作原理如下:初始化:线程池创建一定数量的线程,这些线程处于等待任务的状态。任务提交:当有新任务提交时,线程池首先检查是否有空闲线程。如果有,则将任务直接分配给空闲线程执行。

weixin_28721743的博客 918

linux 线程池原理以及实现

线程池实现

819

机器学习第四个算法ANN(人工神经网络)

我是在14年9月刚读研的时候听到一个说法叫“类人脑算法”很火热,之后搞了很长一段时间相关的蓄水池算法等,但大都没有摸到门路而中途放弃了。现今看来,还是缺少一些知识背景,也不善于在网络上学习,随时间推移,也逐渐明白这是机器学习的一小部分,并且逐渐发展的名称叫做人工神经网络的算法,如果网络层较多,又会有个别名,叫做深度学习。其中个用到的最著名的算法是背向传输算法(backpropagation),而这个

legalhighhigh的博客 1万+

CA(载波聚合)

文章目录CA基本概念1. CA分类2. Pcell、Scell以及Serving CellPcell(Primary Cell)Scell(Secondary Cell)3. Pcell、Scell部署场景场景1场景2场景3场景44. CA的协议控制4.1 Camping/Connetion Establishment:4.2 CC Management/SCell Addition/Release:4.3 Handover/Measurements:4.4 SCell激活 CA基本概念 CA也就是我们中文

你又来看我了,一起学习吧 4万+

Java多线程之线程池(合理分配资源)

一、故事讲解 1.故事 有一家月饼店开业了,店里面有3个核心员工,这些人负责月饼的制作,3人同时工作制作100个月饼,每个人做完自己的月饼就会忙着下一个月饼的制作。因为总不可能要100个员工来做这些月饼吧,那也太浪费了。这些月饼的制作顺序是按照先后排队等待被制作的。 中秋佳节来临了,这些月饼的单子一下子赶了起来。忙不过来那就只能请一些临时员工来帮忙,但是核心员工忙得过来就用不上临时员工了。同时根据这些核心员工和临时员工的工作能力来判断接单情况,超过工作情况就不接单了,等先忙完这些再说。忙完这些月饼的制做

快乐学习 2139

2.数字芯片时序正确的前提—Max-transition

芯片物理设计中最基本的Max-transition检查

weixin_43868998的博客 5426

Linux--实现线程池(万字详解)

IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式。

2301_76618602的博客 1364

EPICS pythonSoftIOC教程

EPICS pythonSoftIOC教程

yuyuyuliang00的博客 1522

FPGA新手必看:XC7Z020最小系统搭建全攻略(附原理图解析)

本文为FPGA新手提供了XC7Z020最小系统搭建的完整实战指南。文章详细解析了电源架构、时钟复位、配置调试等关键硬件电路设计,并重点剖析了外围电路与端口引脚的连接要点及常见陷阱,帮助读者从零开始构建稳定可靠的开发平台。

weixin_29189377的博客 360

【Linux】线程池详解及基本实现

上文中,笔者带领大家实现了日志类,现在我们已经将线程锁条件日志这些线程池中都会用到的基本类都模拟实现过一遍了。线程池是 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。现在我们将这些结合起来,深度了解一下线程池,并模拟实现一下。

2301_77954967的博客 1422

基于LTspice的共模电感差模/共模插入损耗仿真指南

在电源与EMC设计领域,插入损耗是衡量滤波器件抑制噪声能力的核心指标。共模电感作为EMI滤波器中最常见的元器件之一,其差模与共模插入损耗特性直接影响整体滤波方案的合规性。然而,仅依赖规格书曲线往往难以覆盖真实应用中的阻抗条件,借助SPICE仿真工具进行电路级验证已成为工程师常用的手段。LTspice凭借其轻量、高效的交流分析能力,能够帮助设计者快速搭建共模电感等效模型,并通过调整耦合系数、寄生电容等参数,还原漏感与谐振行为。本文以Wurth Elektronik的WE 744821201型号为例,从基本测试

weixin_27252001的博客 293

linux c++线程池的实现

线程池的原理大家都知道,直接上代码了^_^ Thread.h [cpp] view plaincopyprint? #ifndef __THREAD_H   #define __THREAD_H     #include    #include    #include      using namespace std;    /**  * 执行任务的类,设置任务数据并

zhoubl668的专栏:远帆,梦之帆! 7830

XDMA流传输模式

XDMA 流传输模式(Streaming DMA)​ 是 Xilinx FPGA 通过 PCIe 与主机进行 ​实时流式数据传输 的核心机制,

浩瀚之水的专栏 3435

B3640 T3 句子反转

要求将这些单词倒序输出。而且对于每个单词,如果是小写词,应当转为大写;如果是大写词,应当转为小写;如果是自然数,应该倒转输出。给定一行句子,每个词之间用空格隔开,要么是全小写英文单词,要么是全大写英文单词,要么是自然数。第一次做的时候0分,两个小时没做出来。仅一行,表示程序对句子的处理结果。仅一行,即需要反转的句子。

小前日记 366

linux线程池c的实现原理,Linux-C实现线程池

文章目录池​池是一组资源的集合,这组资源在服务器启动之初就被创建和初始化,这称为静态资源分配。它避免了服务器对内核的频繁访问,提高了效率。​常见的池有进程池,线程池,内存池内存池​先申请一定数量,大小相等的内存块备用。有新的内存需求时,就从内存池中分出一部分内存块,若内存不足则继续申请新的内存。内存池使内存分配效率得到提升进程池​进程池线先由服务器创建一组子进程,子进程数量应和CPU数量差不多。所...

weixin_42659252的博客 290

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Linux C实现线程池

Linux C语言 线程池的实现 链表操作,锁 条件变量使用

C++ 开发者的博客 1752

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

一个Linux下C线程池的实现

一个Linux下C线程池的实现       什么时候需要创建线程池呢?简单的说,如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时也是线程池该出场的机会了。如果线程创建和销毁时间相比任务执行时间可以忽略不计,则没有必要使用线程池了。    下面是Linux系统下用C语言创建的一个线程池。线程池会维护一个任务链表(每个CTh

YANGSENG1987的专栏 821

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

Linux下c语言线程池的实现

1.线程池基本原理   在传统服务器结构中, 常是 有一个总的 监听线程监听有没有新的用户连接服务器, 每当有一个新的用户进入, 服务器就开启一个新的线程用户处理这 个用户的数据包。这个线程只服务于这个用户 , 当 用户与服务器端关闭连接以后,服务器端销毁这个线程。然而频繁地开辟与销毁线程极大地占用了系统的资源。而且在大量用户的情况下,系统为了开辟和销毁线程将浪费大量的时间和资源。线程池提供

whatever 1355

Linux实现线程池

线程池是一种并发编程模式,它维护一组预先创建的线程,用于执行多个任务,避免了每次有新任务就去创建线程和销毁线程的开销。线程池就是负责任务与线程的匹配,线程池的核心思路是通过预先创建并管理一组线程,以高效执行大量短期任务,避免频繁创建和销毁线程的开销,提前创建固定数量的线程并保持活跃,任务到来时直接分配,当你有新的任务产生,并需要线程执行的时候,线程池将负责自动将传入的任务与线程进行匹配。同时线程池将任务提交与任务执行分离,提交者只需关注任务内容,线程池负责调度和执行,实现解耦。

Chihiro_wyj的博客 473

Linux——线程池的模拟实现

线程池技术是一种高效管理多线程任务的机制,通过预先创建一组线程并复用它们来执行短期任务,避免了频繁创建和销毁线程的开销。线程池适用于高并发场景(如Web服务器),能提高系统响应速度并防止资源耗尽。文章介绍了线程池的优势、适用场景,并提供了C++实现的线程封装类(Thread)和线程池模板类(ThreadPool),包含线程创建、任务队列管理、线程唤醒等功能。线程池通过控制线程数量(通常不超过CPU核心数)来平衡并行效率与系统稳定性,特别适合处理大量短时任务的场景。

二哈的博客 931

【Linux线程】线程池(线程池的概念以及实现)

了解过了线程、那最应该学习的就是线程池,线程池作为一种池化技术,可以避免了在处理短时间任务时创建与销毁线程的代价,并且还能够保证内核的充分利用,还能防止过分调度,可以极大的提高程序运行的效率。

2202_75605090的博客 1034

Linux学习:线程池的简单实现

日志信息与格式[年-月-日 时:分:秒(日志产生的时间)][日志等级][进程pid][产生日志所在的文件][日志打印所在的行数]具体的日志信息日志等级标识日志信息的等级,共分为Info(普通信息)Debug(调试信息)Warning(警告)Error(错误)Fatal(致命)Unknown(未知)日志的输出方向定义一个bool类型全局变量gIsSave,用于标识日志信息的输出方向。当值为false时,直接向显示器上输出打印,当值为true时,向指定记录文件中输出。

qq_71806743的博客 1155
上一篇: Linux_生产消费者模型
下一篇: C++_单例模式
安权_code
博客等级 码龄5年 1558粉丝 114原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

安权_code

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

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

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

打赏作者

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

抵扣说明:

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

余额充值