C++线程(thread)、锁(mutex)以及atmoic

1. 线程(thread)

c++11加入了thread线程类,使我们在操作多线程的时候更加的简单;

线程是调度的基本单位
进程则是资源拥有的基本单位

头文件:

#include <thread>

下面通过一个例子,来说明线程的创建与使用

  • join:调用该函数会阻塞当前线程,需要等待子线程执行完毕;
  • joinable:用来判断线程是否可以join,返回值为bool类型;
  • detach:将线程分离出来,不再和主线程相关,可以单独存在,非必要,不建议使用;
  • swap:交换两个线程对象所代表的底层句柄;
  • get_id:返回线程id
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <unistd.h>

void printA()
{
    int32_t a = 1;
    while(1)
    {
        std::cout<<"thread A"<<std::endl;
        if(a++ > 10) break;
    }    
}

void printB()
{
    int32_t b = 1;
    sleep(1);
    while(1)
    {        
        std::cout<<"thread B"<<std::endl;
        if(b++ > 10) break;
    }    
}

int main(int argc, char *argv[])
{
    std::thread t1{printA};   //创建线程,并初始化
    std::thread t2(printB);   //创建线程,并初始化  
    std::cout<<"t1 is: " <<t1.get_id()<<std::endl;   //输出线程id
    std::cout<<"t2 is: " <<t2.get_id()<<std::endl;
    std::swap(t1,t2);    //交换
    std::cout<<"t1 is: " <<t1.get_id()<<std::endl;
    std::cout<<"t2 is: " <<t2.get_id()<<std::endl;
    sleep(2);

    std::thread t3;   //创建线程
    //通过lambda初始化线程
    t3 = std::thread([](){
        int32_t c = 1;
        while(1){
            std::cout<<"thread C"<<std::endl;
            if(c++ > 10) break;
        }
    });

    if(t1.joinable())
    {
        t1.join();
    }
    if(t2.joinable())
    {
        t2.join();
    }
    if(t3.joinable())
    {
        t3.join();
    }   

    return 0;
}

输出

t1 is: 140476317243136
t2 is: 140476308850432
t1 is: 140476308850432
t2 is: 140476317243136
...

g++编译方法(需要带上-pthread)

g++ -g  main.cc -o d -std=c++11 -pthread

2. 锁(mutex)

锁的出现是为了防止资源竞争,锁越复杂,程序的性能就越低;
头文件

#include <mutex>

mutex中锁的分类

std::mutex                    互斥锁
std::recursive_mutex          递归锁
std::timed_mutex              定时锁
std::recursive_timed_mutex    定时递归锁

C++还提供了类模板,方便我们使用

Lock 类
std::lock_guard    RAII机制下的锁,模板类,在创建的时候自动加锁,在销毁的时候自动解锁,优点是简单,直接加锁就可以,缺点是不灵活

std::unique_lock   RAII机制下的锁,模板类,是对lock_guard的扩展,支持自己上锁和解锁,支持以下方法
lock()
unlock()
try_lock()
try_lock_for()
try_lock_until()

unique_lock<mutex> uniqeLock(mu, std::adopt_lock);   //同lock_guard, 指示不加锁,前提是mutex已经加锁
unique_lock<mutex> uniqeLock(mu, std::try_to_lock);  //尝试加锁,用在获取不到锁时执行一些其他操作
unique_lock<mutex> uniqeLock(mu, std::defer_lock);   //不加锁,且mutex尚未锁定,配合成员函数使用
uniqeLock.try_lock();     //同参数std::try_to_lock
uniqeLock.owns_lock();    //返回bool,表明是否拥有锁
uniqeLock.release();      //返回mutex,解除unique_lock与mutex的绑定,unique_lock为空

2.1 互斥锁

互斥锁,同一时刻只允许一个线程访问,其余线程则等待

示例1
通过 std::mutex g_mutex 自动的上锁和解锁;

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <unistd.h>
#include <chrono>
#include <condition_variable>

std::mutex g_mutex;
int32_t g_count{0};

void calFun1(int32_t n)
{
    g_mutex.lock();
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }
    g_mutex.unlock();    //一定要记得解锁
    std::cout<<"calFun1 g_count is: "<<g_count<<std::endl;
}

void calFun2(int32_t n)
{
    g_mutex.lock();
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }
    g_mutex.unlock();    //一定要记得解锁
    std::cout<<"calFun2 g_count is: "<<g_count<<std::endl;
}

int main(int argc, char *argv[])
{
    std::thread t1{calFun1, 1000000};
    std::thread t2(calFun2, 1000000);    
    
    if(t1.joinable())
    {
        t1.join();
    }
    if(t2.joinable())
    {
        t2.join();
    }    
    
    std::cout<<"g_count is: "<<g_count<<std::endl;
    return 0;
}

输出

//不加锁输出
calFun1 g_count is: 1029002
calFun2 g_count is: 1124281
g_count is: 1124281

//加锁输出
calFun1 g_count is: 1000000
calFun2 g_count is: 2000000
g_count is: 2000000

示例2
通过C++提供的模板类,std::lock_guard和std::unique_lock会自动的完成解锁,一般情况下会使用std::lock_guard,其效率更高

std::mutex g_mutex;
std::lock_guard<std::mutex> m(g_mutex);
std::unique_lock<std::mutex> m(g_mutex);

示例

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <unistd.h>

std::mutex g_mutex;
int32_t g_count(0);

void calFun1(int32_t n)
{
    std::lock_guard<std::mutex> m(g_mutex);
    //std::unique_lock<std::mutex> m(g_mutex);
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }
    std::cout<<"calFun1 g_count is: "<<g_count<<std::endl;
}

void calFun2(int32_t n)
{
    std::lock_guard<std::mutex> m(g_mutex);
    //std::unique_lock<std::mutex> m(g_mutex);
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }
    std::cout<<"calFun2 g_count is: "<<g_count<<std::endl;
}

int main(int argc, char *argv[])
{
    std::thread t1{calFun1, 1000000};
    std::thread t2(calFun2, 1000000);       
    
    
    if(t1.joinable())
    {
        t1.join();
    }
    if(t2.joinable())
    {
        t2.join();
    }    
    std::cout<<"g_count is: "<<g_count<<std::endl;

    return 0;
}

输出

calFun1 g_count is: 1000000
calFun2 g_count is: 2000000
g_count is: 2000000

示例3(定时锁)
定时锁,设置线程的等待时间,如果超时则进行其它处理;
单独使用std::timed_mutex g_mutex,需要解锁

设置calFun1的等待时间,如果超过3秒,则返回


#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <unistd.h>
#include <chrono>

std::timed_mutex g_mutex;
int32_t g_count{0};

void calFun1(int32_t n)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));    
    if(!g_mutex.try_lock_for(std::chrono::seconds(3)))    
    {
        std::cout<<"calFun1 connot get lock!"<<std::endl;
        return;
    }
    else{
        std::cout<<"false"<<std::endl;
    }    
    
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }     
    g_mutex.unlock();    //需要解锁
    std::cout<<"calFun1 g_count is: "<<g_count<<std::endl;
}

void calFun2(int32_t n)
{
    std::unique_lock<std::timed_mutex> m(g_mutex);
    std::this_thread::sleep_for(std::chrono::seconds(5));
    
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }    
    std::cout<<"calFun2 g_count is: "<<g_count<<std::endl;
}

int main(int argc, char *argv[])
{
    std::thread t1{calFun1, 1000000};
    std::thread t2(calFun2, 1000000);    
    
    if(t1.joinable())
    {
        t1.join();
    }    
    
    if(t2.joinable())
    {
        t2.join();
    }
    
    std::cout<<"g_count is: "<<g_count<<std::endl;

    return 0;
}

输出

calFun1 connot get lock!
calFun2 g_count is: 1000000
g_count is: 1000000

示例4(定时锁)
通过模板类std::unique_lock来实现
修改calFun1

void calFun1(int32_t n)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));    
    std::unique_lock<std::timed_mutex> mt(g_mutex, std::defer_lock);   //这里需要使用参数std::defer_lock,不加锁
    if(!mt.try_lock_for(std::chrono::seconds(3)))    
    {
        std::cout<<"calFun1 connot get lock!"<<std::endl;
        return;
    }
    else{
        std::cout<<"false"<<std::endl;
    }
    
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    }
    std::cout<<"calFun1 g_count is: "<<g_count<<std::endl;
}

2.2 条件锁

条件锁即为条件变量+互斥锁,当线程未满足运行条件时,则阻塞;只有满足运行条件,才会运行;
使用条件变量的头文件

#include <condition_variable>
  • notify_one:唤醒一个线程,如果线程有多个,则唤醒哪一个线程是随机的
  • notify_all:唤醒所有线程
  • wait:等待直到条件满足
  • wait_for:等待直到条件满足 或 达到超时时间(时间长度)
  • wait_until:等待直到条件满足 或 达到设定的超时时间(时间点)

示例
其中有两个给线程,一个线程来来自增全局变量g_count,当除以10余数为0的时候,通知另外一个线程来打印全局变量g_count。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <unistd.h>
#include <condition_variable>

bool flag = false;
bool exit_flag = false;

std::mutex g_mutex;
int32_t g_count(0);
std::condition_variable cv;    //条件变量

void calFun1(int32_t n)
{    
    std::unique_lock<std::mutex> lck(g_mutex);
    for(int32_t i = 0; i < n ;i++)
    {        
        if(g_count%10 == 0)
        {
            flag = true;         
            cv.notify_one();        
            cv.wait(lck,[]{return !flag;});   //等待,后面跟lambda表达式
        }
        g_count++;
    }
    exit_flag = true;
    flag = true;
    cv.notify_one();
}

void calFun2(int32_t n)
{    
    while(1)
    {
        std::unique_lock<std::mutex> lck(g_mutex);
        cv.wait(lck, []{return flag;});
        std::cout<<"g_count value is: "<<g_count<<std::endl;
        flag = false;
        cv.notify_one();        
        if(exit_flag) break;
    }    
    std::cout<<"calFun2 exit"<<std::endl;
}

int main(int argc, char *argv[])
{
    std::thread t1{calFun1, 100};
    std::thread t2(calFun2, 100);    
    
    if(t1.joinable())
    {
        t1.join();
    }
    if(t2.joinable())
    {
        t2.join();
    }        

    std::cout<<"g_count is: "<<g_count<<std::endl;

    return 0;
}

输出

g_count value is: 0
g_count value is: 10
g_count value is: 20
g_count value is: 30
g_count value is: 40
g_count value is: 50
g_count value is: 60
g_count value is: 70
g_count value is: 80
g_count value is: 90
g_count value is: 100
calFun2 exit
g_count is: 100

2.3 自旋锁

互斥锁在访问资源的时候,如果有锁则会等待,这个时候cpu的资源会释放;自旋锁在获取资源的时候,如果有锁,则会一直尝试,直到访问到资源,这个时候cpu的资源是不会被释放的;
自旋锁需要自己实现
示例:

class spinlock_mutex
{
public:
	spinlock_mutex() {};
	~spinlock_mutex() {};

	void lock()
	{
		while (flag.test_and_set(std::memory_order_acquire));
	}
	void unlock()
	{
		flag.clear(std::memory_order_release);
	}
private:
	std::atomic_flag flag = ATOMIC_FLAG_INIT;
};

使用

spinlock_mutex sp_mutex;    //外部定义

std::lock_guard<spinlock_mutex> m(sp_mutex);   //在使用的时候定义

2.4 读写锁

读写锁,在读取资源的时候,不独占资源,可以共享;但是在进行写操作的时候,需要独占;
以下代码来自网络读写锁
读写锁在C++17增加了shared_mutex,使用起来更加方便

class readWriteLock {
private:
    std::mutex readMtx;
    std::mutex writeMtx;
    int readCnt; // 已加读锁个数
public:
    readWriteLock() : readCnt(0) {}
    void readLock()
    {
        readMtx.lock();
        if (++readCnt == 1) {
            writeMtx.lock();  // 存在线程读操作时,写加锁(只加一次)
        }
        readMtx.unlock();
    }
    void readUnlock()
    {
        readMtx.lock();
        if (--readCnt == 0) { // 没有线程读操作时,释放写锁
            writeMtx.unlock();
        }
        readMtx.unlock();
    }
    void writeLock()
    {
        writeMtx.lock();
    }
    void writeUnlock()
    {
        writeMtx.unlock();
    }
};

2.5 递归锁

假设定了锁std::mutex m_mutes,函数func1和func2中都加了锁,同时func1中调用了func2,这种情况下普通的锁会出现死锁,而递归锁可以解决该问题;

递归锁

std::recursive_mutex r_mutes;

示例

#include <iostream>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#include <vector>
#include <shared_mutex>
#include <unistd.h>

std::atomic<int32_t> g_count{0};

std::recursive_mutex r_mutes;
std::mutex m_mutes;

void func2()
{
    //std::lock_guard<std::mutex> m(m_mutes);   //该锁会形成死锁
    std::lock_guard<std::recursive_mutex> m(r_mutes);
    g_count++;
    std::cout<<"(fucn2) g_count value is: "<<g_count<<std::endl;
}

void func1(int32_t n)
{   
    for(int i = 0;i<n;i++)
    {
        //std::lock_guard<std::mutex> m(m_mutes);     //该锁会形成死锁
        std::lock_guard<std::recursive_mutex> m(r_mutes);
        g_count++;        
        std::cout<<"(fucn1) g_count value is: "<<g_count<<std::endl;
        func2();
    }
}

int main(int argc, char *argv[])
{    
    std::thread t1{func1,3};
    if(t1.joinable())
    {
        t1.join();
    }       

    return 0;
}

输出

(fucn1) g_count value is: 1
(fucn2) g_count value is: 2
(fucn1) g_count value is: 3
(fucn2) g_count value is: 4
(fucn1) g_count value is: 5
(fucn2) g_count value is: 6

也可以自己实现递归锁,其思想为加锁和解锁的计算;
RecursiveMutex类的实现,其思路来自网上(RecursiveMutex
在使用的时候需要自己去lock()和unlock(),不能使用模板类

#include <iostream>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#include <vector>
#include <shared_mutex>
#include <unistd.h>

std::atomic<int32_t> g_count{0};

std::recursive_mutex r_mutes;
std::mutex m_mutes;

class RecursiveMutex {
public:
    RecursiveMutex() {
        num_of_locks = 0;
    } 
    ~RecursiveMutex() {}
 
    void lock() {
        if (num_of_locks == 0) {
            my_mutex.lock();            
            owner_thread_id = std::this_thread::get_id();
        }
        else if (std::this_thread::get_id() == owner_thread_id)
            num_of_locks++;
    }
 
    void unlock() {
        if (num_of_locks > 0)
            num_of_locks--;
        if (num_of_locks == 0)
            my_mutex.unlock();
    }
 
private:
    int num_of_locks;
    std::mutex my_mutex;
    std::thread::id owner_thread_id;
};

RecursiveMutex r_mutex;

void func2()
{    
    r_mutes.lock();
    g_count++;
    std::cout<<"(fucn2) g_count value is: "<<g_count<<std::endl;
    r_mutes.unlock();
}

void func1(int32_t n)
{   
    for(int i = 0;i<n;i++)
    {        
        r_mutes.lock();
        g_count++;        
        std::cout<<"(fucn1) g_count value is: "<<g_count<<std::endl;
        func2();
        r_mutes.unlock();
    }
}

int main(int argc, char *argv[])
{    
    std::thread t1{func1,3};
    if(t1.joinable())
    {
        t1.join();
    }       

    return 0;
}

2.6 共享锁

C++17才开始支持
头文件
是读写锁

#include <shared_mutex>

使用

std::shared_mutex shared_lock;
std::shared_lock<std::shared_mutex> m(shared_lock);

示例

//g++ -g  main_shared.cc -o d -std=c++17 -pthread

#include <iostream>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <unistd.h>

std::atomic<int32_t> g_count{0};

std::shared_mutex shared_lock;

void readValue(int32_t n, std::string str)
{
    //std::lock_guard<std::shared_mutex> m(shared_lock);
    std::shared_lock<std::shared_mutex> m(shared_lock);
    for(int i = 0; i < n; i++)
    {        
        std::cout<<str <<"  g_count value is: "<<g_count<<std::endl;
    }
    
}

void writeValue(int32_t n, std::string str)
{
    //std::lock_guard<std::shared_mutex> m(shared_lock);
    std::shared_lock<std::shared_mutex> m(shared_lock);
    for(int i = 0; i < n; i++)
    {
        g_count++;
        std::cout<<str <<"  g_count value is: "<<g_count<<std::endl;
    }
    
}

int main(int argc, char *argv[])
{
    std::vector<std::thread> v;
    v.emplace_back(std::thread{readValue,3,"t1"});
    v.emplace_back(std::thread{readValue,3,"t2"});
    v.emplace_back(std::thread{writeValue,3,"t3"});
    v.emplace_back(std::thread{writeValue,3,"t4"});
    v.emplace_back(std::thread{writeValue,3,"t5"});
    v.emplace_back(std::thread{writeValue,3,"t6"});
    
    
    for(int32_t i =0; i< v.size();i++)
    {
        if(v[i].joinable())
        {
            v[i].join();
        }
    }
    return 0;
}

输出

t1  g_count value is: t2  g_count value is: 00
t1  g_count value is: 0
t1  g_count value is: 0

t2  g_count value is: 0
t2  g_count value is: 0
t3  g_count value is: 1
t3  g_count value is: 2
t3  g_count value is: 3
t4  g_count value is: 4
t4  g_count value is: 5
t4  g_count value is: 6
t6  g_count value is: 7
t6  g_count value is: 8
t6  g_count value is: 9
t5  g_count value is: 10
t5  g_count value is: 11
t5  g_count value is: 12

3. 原子操作

感觉原子操作的内容比较多,本文只记录std::atomic和std::atomic_flag的常用方法;
原子操作可以实现无锁,当然也可以用于实现锁、条件变量等多线程同步机制;

头文件:

#include <atomic>

支持版本:C++11

std::atomic

std::atomic 
std::atomic<Integral>

支持类型(支持的类型较多,并且随着版本的迭代,支持的类型越来越多)

1. std::atomic<bool> 
2. std::atomic<char> 
3. std::atomic<signed char>
4. std::atomic<unsigned char> 
5. std::atomic<short>  
6. std::atomic<unsigned short> 
7. std::atomic<int> 
8. std::atomic<unsigned int> 
9. std::atomic<long>
10. std::atomic<unsigned long> 
11. std::atomic<long long>     
12. std::atomic<unsigned long long>

支持的成员函数

store()  //用非原子参数替换原子参数的数值
load()   //获得原子对象的值
exchange()  //替换原子对象的值
compare_exchange_weak()   // 原子对象和非原子对象进行比较,相等则修改原子对象的值(比较值和修改值是两个),不相等则不赋值
compare_exchange_strong()  //同上
//compare_exchange_weak比compare_exchange_strong性能更高一点

无锁示例

#include <iostream>
#include <string>
#include <atomic>
#include <thread>

std::atomic<int32_t> g_count{0};

void calFun1(int32_t n)

    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    } 
    std::cout<<"calFun1 g_count is: "<<g_count<<std::endl;
}

void calFun2(int32_t n)
{    
    for(int32_t i = 0; i < n ;i++)
    {        
        g_count++;
    } 
    std::cout<<"calFun2 g_count is: "<<g_count<<std::endl;
}

int main(int argc, char *argv[])
{
    std::thread t1{calFun1, 1000000};
    std::thread t2(calFun2, 1000000);    
    
    if(t1.joinable())
    {
        t1.join();
    }
    if(t2.joinable())
    {
        t2.join();
    }    
    
    std::cout<<"g_count is: "<<g_count<<std::endl;
    return 0;
}

输出
也会出现资源竞争,但是输出结果无误

calFun1 g_count is: 1997543
calFun2 g_count is: 2000000
g_count is: 2000000

成员函数的使用示例

#include <iostream>
#include <string>
#include <atomic>
#include <thread>

std::atomic<int32_t> g_count{0};

int main(int argc, char *argv[])
{
    g_count = 10;
    g_count.store(15);    //替换其值
    std::cout<<"set value by store()\t\t"<<g_count<<std::endl;   
    std::cout<<"get value by load()\t\t"<<g_count.load()<<std::endl;   
    
    //g_count.load(std::memory_order_seq_cst);
    //g_count.exchange(100, std::memory_order_seq_cst);
    g_count.exchange(98);
    //g_count.exchange(99);
    std::cout<<"change value by exchange()\t"<<g_count<<std::endl<<std::endl;

    /*
    * 判断逻辑
    * g_count == expected_value? (return true; g_count = new_value) : (return false; expected_value = g_count)
    */
    int32_t expected_value = 99;
    int32_t new_value = 88;    
    if(g_count.compare_exchange_weak(expected_value, new_value))
    {
        std::cout<<"compare_exchange_strong true."<<std::endl;
    }
    else{
        std::cout<<"compare_exchange_strong false."<<std::endl;
    }
    std::cout<<"new_value is: \t\t"<<new_value<<std::endl;    
    std::cout<<"expected_value is: \t"<<expected_value<<std::endl;    
    std::cout<<"g_count is: \t\t"<<g_count<<std::endl<<std::endl;

    /*
    * 判断逻辑
    * g_count == expected_value? (return true; g_count = new_value) : (return false; expected_value = g_count)
    */
    //g_count.store(998);
    g_count.store(999);
    expected_value = 999;
    new_value = 888;    
    if(g_count.compare_exchange_strong(expected_value, new_value))
    {
        std::cout<<"compare_exchange_strong true."<<std::endl;
    }
    else{
        std::cout<<"compare_exchange_strong false."<<std::endl;
    }
    std::cout<<"new_value is: \t\t"<<new_value<<std::endl;    
    std::cout<<"expected_value is: \t"<<expected_value<<std::endl;    
    std::cout<<"g_count is: \t\t"<<g_count<<std::endl<<std::endl;
    
    
    return 0;
}

输出

set value by store()            15
get value by load()             15
change value by exchange()      98

compare_exchange_strong false.
new_value is:           88
expected_value is:      98
g_count is:             98

compare_exchange_strong true.
new_value is:           888
expected_value is:      999
g_count is:             888

g_count.exchange(100, std::memory_order_seq_cst);
std::memory_order_seq_cst为枚举std::memory_order中的元素,可以设置原子操作对内存访问进行排序;

typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;

std::atomic_flag
std::atomic_flag是最简单的标准原子类型,表示一个布尔标志,该类型的对象只有两种状态:成立或置零;

对象必须由宏 ATOMIC_FLAG_INIT 初始化(置零)

支持销毁、置零、读取原有的值并设置标志成立,分为为析构函数、成员函数clear()、成员函数test_and_set()

test_and_set() //获取std::atomic_flag的状态并将其置为true

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值