并发编程(14)——内存栅栏

文章目录

  • 十四、day14
  • 1. 内存栅栏
    • 1.1 什么是栅栏
    • 1.2 栅栏和原子操作的对比
      • 1.2.1 获取操作
      • 1.2.2 释放操作
    • 1.3 线程可见顺序
    • 1.4 通过栅栏保证指令编排顺序
    • 1.5 通过栅栏令非原子操作服从内存次序
    • 1.6 同步线程间的内存访问

十四、day14

在学习完内存模型、内存序、原子类型、操作的相关内容后,并通过Acquire-Release模型和原子操作实现了无锁环形并发队列,今天学习关于内存栅栏的内容。

参考:

恋恋风辰官方博客

C++ 内存模型

Introduction | Concurrency-with-Modern-C++

C++编程:内存栅栏(Memory Barrier)详解及在多线程编程中的应用-CSDN博客


1. 内存栅栏

1.1 什么是栅栏

栅栏主要用于强制施加内存次序,却无须更改任何数据,通常于服从 memory_order_relaxed 次序的原子操作组合使用。用大白话来说,栅栏用于阻止编译器或CPU对某些内存操作进行重排,确保在它之前的操作完成后,才会执行它之后的操作,从而维护内存操作的顺序一致性。因为 memory_order_relaxed 是最宽松的内存序,它只能保证原子性,却不能保证多线程之间的先行性和顺序性,所以服从 memory_order_relaxed 次序的原子操作通常会通过编译器进行指令重排,而栅栏会限制这种重排。

我们之前学习了互斥量 mutex:拿到mutex锁的线程将拥有唯一进入临界区的资格(共享互斥除外)。其实 mutex 的加锁和解锁之间也起到了”栅栏“的作用,因为栅栏中的代码不会被编译器重排到栅栏之外(但不保证栅栏之外的内容进入栅栏之中)。

如下图的三种情况,第一种可能会被优化成第二种。但是第二种情况不会被优化成第三种:

在这里插入图片描述


通常,栅栏有三种:

  • 全栅(full fence):指定服从memory_order_seq_cst或者memory_order_acq_rel。在任意两个操作(读和写)之间使用完整的栅栏std::atomic_thread_fence(),可以避免这些操作的重新排序。不过,对于存储-加载操作来说,它们可能会被重新排序。
  • 获取栅栏(acquire fence):指定服从memory_order_acquire。避免在获取栅栏之前的读操作,被获取栅栏之后的读或写操作重新排序。
  • 释放栅栏(release fence)。指定服从memory_order_release。避免释放栅栏之后的写操作,在释放栅栏之前通过读或写操作重新排序。

获取是一个加载操作, 释放是一个存储操作。如果在加载和存储操作的四种组合之间,放一个内存屏障中会发生什么情况呢?

  • ① Load-Load:读接着读
  • ② Load-Store:先读后写
  • ③ Store-Load:先写后读
  • ④ Store-Store:写接着写

全栅可以防止①②④三种情况下的指令重排,但不能防止③被重排

那么,哪些操作可以翻过栅栏,通过下面的图例进行解释,其中红色的斜杆表示这种类型的乱序会被禁止。

  1. 全栅

在这里插入图片描述

也可以显式地调用std::atomic_thread_fence(std::memory_order_seq_cst),而不是std::atomic_thread_fence()。默认情况下,栅栏使用内存序为顺序一致性。如果对全栅使用顺序一致性,那么std::atomic_thread_fence也将遵循全局序。

  1. 获取栅栏

在这里插入图片描述

acquire fence 阻止了所有在它之前的读操作与在它之后的读写操作乱序,保证栅栏之后的读取操作会看到栅栏之前的操作的影响

  1. 释放栅栏
    在这里插入图片描述

release fence 阻止了所有在它之前的读写操作与在它之后的写操作乱序

三种类型的fence均不会禁止先写后读的乱序。

从C++11开始,提供了下面两种栅栏类型:

  • std::atomic_thread_fence : 同步线程间的内存访问。
  • std::atomic_signal_fence : 线程内信号之间的同步。

我们一般使用后者多一些。

1.2 栅栏和原子操作的对比

获取-释放栅栏与原子获取-释放内存序有着相似的同步方式和顺序,但二者有以下两种区别:

  • 栅栏不需要原子操作
  • 获取-释放栅栏比原子操作更重量级

获取-释放操作的主要思想是,在线程间建立同步和排序约束,这些同步和顺序约束也适用于使用宽松次序的原子操作或非原子操作。注意,获取-释放操作是成对出现的。此外,对获取-释放语义的原子变量的操作,必须作用在相同的原子变量上。不过,我们现在先将这些操作分开来看。

1.2.1 获取操作

在原子变量(内存序为std::memory_order_acquire)上进行的加载 (读取)操作是一个获取操作,确保在此之后的所有读操作只会在此读操作完成之后进行
在这里插入图片描述

std::atomic_thread_fence内存序设置为std::memory_order_acquire,这对内存访问重排添加了更严格的约束:

在这里插入图片描述

1.2.2 释放操作

对内存序为 std::memory_order_release 的原子变量,进行存储(写)操作时,这些操作属于释放操作。确保在此之前的所有写操作都在此写操作完成前对其他线程可见

在这里插入图片描述

释放栅栏为

在这里插入图片描述

1.3 线程可见顺序

在六种内存序和三种内存模型中,只有 memory_order_seq_cst 以及其实现的 Sequencial consistent 模型能够保证原子变量修改的值在其他多线程中看到的顺序是一致的。但我们可以通过同步机制保证一个线程对原子变量的修改对另一个线程可见。通过“Syncronizes With” 的方式达到先行的效果。

如果我们线程1对原子变量 A 的store操作采用release内存序,而线程2对原子变量 B 的load采用acquire内存序,并不能保证变量A 的操作一定比 变量B的操作先执行。因为两个线程并行执行无法确定先后顺序,我们指的先行不过是说如果B读取了A操作的结果,则B依赖于A,则称A先行于B。

比如:

#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x()
{
    x.store(true, std::memory_order_release); //1
}
void write_y()
{
    y.store(true, std::memory_order_release); //2
}
void read_x_then_y()
{
    while (!x.load(std::memory_order_acquire));
    if (y.load(std::memory_order_acquire))   //3
        ++z;
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_acquire));
    if (x.load(std::memory_order_acquire))   //4
        ++z;
}

void TestAR()
{
    x = false;
    y = false;
    z = 0;
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join();
    b.join();
    c.join();
    d.join();
    assert(z.load() != 0); //5
    std::cout << "z value is " << z.load() << std::endl;
}

在该段示例中,如果代码按以下逻辑执行,那么断言不会被触发,z始终不为0:

  1. 如果c线程执行函数 read_x_then_y 结束后,没有对z执行加加操作,那么说明c线程读取的x值为true, y值为false。
  2. 之后d线程读取时,如果保证执行到4处说明y为true,等d线程执行4处代码时x必然为true,那么肯定会z++。反过来也类似
  3. 如果x先被store为true,y后被store为true,c线程看到y为false时x已经为true了,那么d线程y为true时x也早就为true了,所以z一定会执行加加操作。

但是上述逻辑是错误的,因为只有先后一致次序才能保证全局一致性,而其他的几种内存序均不能保证多个线程看到的一个变量的值是一致的,更不能保证看到的多个变量的值是一致的。

线程d和线程d的载入操作3和4有可能都读取false值(与宽松次序的情况一样),因此有可能令断言触发错误。变量x和y分别由不同线程写出,所以两个释放操作都不会影响到其他线程。

从以下两个角度解释:

  1. CPU内存结构

假设a,b,c,d分别运行在不同的CPU内核上,那么 a 对x的操作如果放至cache中,而没更新至memory中,那么x会被线程c读取,而线程d是看不到的。同理,线程b对y的操作会先被线程d看见,而线程c看不到。如果线程a对x修改,那么线程c会退出循环,同时线程c看不到线程b对y的修改,那么线程c不会对z++;而线程d同理,它看到了y为true从而退出循环,但是看不到线程a对x的修改,此时z同样不会++。

如果 核1 先将y放入memory,那么核3就会读取y的值为true。那么t2就会运行至3处从while循环退出,进而运行至4处,此时核1还未将x的值写入memory。t2读取的x值为false,进而线程t2运行结束,然后核1将x写入true, t1结束运行,最后主线程运行至5处,因为z为0,所以触发断言。

在这里插入图片描述

  1. 从内存序来看

下图展示了两个线程间操作序列的执行关系,可以看到线程a核线程c之间存在先行关系,线程d和线程b之间也从在先行关系,但除此之外没有其他先行关系。内存序memory_order_acquire和memory_order_release只能保证 write_xwrite_y 先于相应的读取操作发生,但不能保证两个读之间也有先行关系,也不能保证线程a对线程d或线程b对线程c存在先行关系。

在这里插入图片描述

AR模型只能保证每个线程内部的顺序而不能保证线程之间的顺序完全正确

1.4 通过栅栏保证指令编排顺序

我们使用之前宽松次序的例子来理解栅栏的作用:

std::atomic<bool> x, y;
std::atomic<int> z;

void write_x_then_y() {
    x.store(true, std::memory_order_relaxed);  // 1
    y.store(true, std::memory_order_relaxed);  // 2
}

void read_y_then_x() {
    while (!y.load(std::memory_order_relaxed)) { // 3
        std::cout << "y load false" << std::endl;
    }
    if (x.load(std::memory_order_relaxed)) { //4
        ++z;
    }
}

int main(){
    x=false;
    y=false;
    z=0;
    std::thread t1(write_x_then_y);
    std::thread t2(read_y_then_x);
    t1.join();
    t2.join();
    assert(z.load() != 0); // 5
}

在文章并发编程(12)——内存次序与内存模型 | 爱吃土豆的个人博客中我们知道,因为宽松次序不保证线程的先行性与顺序性,所以断言5可能会触发。

我们之前是通过获取-释放模型来解决该问题:

void write_x_then_y3()
{
    x.store(true, std::memory_order_relaxed); // 1
    y.store(true, std::memory_order_release);   // 2
}
void read_y_then_x3()
{
    while (!y.load(std::memory_order_acquire));  // 3
    if (x.load(std::memory_order_relaxed))  // 4
        ++z;
}

在宽松次序中,上面所有的内存序均为std::memory_order_relaxed,导致 2 和 3 不构成同步关系, 2 “ not synchronizes with “ 3。而这里通过使用Acquire-Release模型,2 和 3 可构成同步关系,即 2 “ synchronizes with “ 3。

当线程t2执行到4处时,说明ry以及被线程t1置为true,而1顺序先行2,所以在4处时也能看到1被修改了,进而可以推断断言不会被触发。

而除了使用AR模型外,我们也可以使用栅栏保证指令的写入顺序。

void write_x_then_y_fence()
{
    x.store(true, std::memory_order_relaxed);  //1
    std::atomic_thread_fence(std::memory_order_release);  //2
    y.store(true, std::memory_order_relaxed);  //3
}
void read_y_then_x_fence()
{
    while (!y.load(std::memory_order_relaxed));  //4
    std::atomic_thread_fence(std::memory_order_acquire); //5
    if (x.load(std::memory_order_relaxed))  //6
        ++z;
}

尽管4和3我们采用的是std::memory_order_relaxed顺序,但是通过逻辑关系保证了3的结果同步给4,进而”3 happens-before 4”

因为我们采用了获取栅栏std::atomic_fence所以,5处能保证6不会先于5写入内存。2处的释放栅栏能保证1处的指令先于2写入内存,进而”1 happens-before 6”, 1的结果会同步给 6

在这里插入图片描述

该栅栏会保证两个store写操作不会被重排。

在这里插入图片描述

该栅栏会保证两个load写操作不会被重排。

所以 ”atomic_thread_fence”其实和”release-acquire”相似,都是保证memory_order_release之前的指令不会排到其后,memory_order_acquire之后的指令不会排到其之前。

1.5 通过栅栏令非原子操作服从内存次序

如果将x从原子类型改为普通的布尔类型,程序的行为同样相同:

bool x;
std::atomic<bool> y;
std::atomic<int> z;

void write_x_then_y() {
    x = true;  // 1
    std::atomic_thread_fence(std::memory_order_release);  //2
    y.store(true, std::memory_order_relaxed);  // 3
}

void read_y_then_x() {
    while (!y.load(std::memory_order_relaxed)) { // 4
        std::cout << "y load false" << std::endl;
    }
    std::atomic_thread_fence(std::memory_order_acquire); //5
    if (x) { // 6
        ++z;
    }
}

int main(){
    x=false;
    y=false;
    z=0;
    std::thread t1(write_x_then_y);
    std::thread t2(read_y_then_x);
    t1.join();
    t2.join();
    assert(z.load() != 0); // 7
}

即使1是非原子变量,3是relaxed次序,但只要加上栅栏2,那么就会形成1先行于3的关系(如果没有栅栏,那么服从relaxed次序的操作不会保证1先行2,因为relaxed次序中,同一线程中只有同一变量的操作服从先行,而同一线程的不同变量没有先行关系)

用更简洁的形式进行解释:

  1. 获取-释放栅栏阻止了原子和非原子操作跨栅栏的重排序。
  2. 释放栅栏与获取栅栏同步。
  3. 自由操作或非原子操作的所有结果(在释放栅栏之前),在获得栅栏之后都是可见的。

释放栅栏和获取栅栏之间的同步

这两个定义来自于N4659: Working Draft, Standard for Programming Language C++ ,并且标准文档的文字比较难懂:“如果操作X和操作Y对原子对象M的操作存在有原子操作,释放栅栏A同步于获取栅栏B;那么A的操作顺序位于X之前,X对M进行修改,Y位于B之前,并且Y读取X写入的值,或在进行释放操作时,释放序列X中的任何操作所写的值将被读取。”

让我借上面的代码段解释一下这段话:

  • atomic_thread_fence(memory_order_release) 是一个释放栅栏A。2处
  • atomic_thread_fence(memory_order_acquire) 是一个获取栅栏B。5处
  • std::atomic<bool> y是一个原子对象M。
  • y.store(true, std::memory_order_relaxed)是一个原子存储操作X。3处
  • while (!y.load(std::memory_order_relaxed)) )是一个原子加载操作Y。4处

能令非原子操作服从内存次序的不只有栅栏,我们亦可以通过memory_order_release和memory_order_consume来保证非原子操作服从内存次序,参考文章并发编程(12)——内存次序与内存模型 | 爱吃土豆的个人博客中关于Release-Consume的介绍。

1.6 同步线程间的内存访问

我们在说栅栏的时候,提到了从C++11开始,提供了下面两种栅栏类型:

  • std::atomic_thread_fence : 同步线程间的内存访问。
  • std::atomic_signal_fence : 线程内信号之间的同步。

我们只用了第一种方式,那么第二种如何使用?

std::atomic_signal_fence在线程和信号句柄间,建立了非原子和自由原子访问的内存同步序。通过一个例子进行说明:

#include <atomic>
#include <cassert>
#include <csignal>

std::atomic<bool> a{false};
std::atomic<bool> b{false};

extern "C" void handler(int){
  if (a.load(std::memory_order_relaxed)){
    std::atomic_signal_fence(std::memory_order_acquire);
    assert(b.load(std::memory_order_relaxed));
  }
}

int main(){

  std::signal(SIGTERM, handler);

  b.store(true, std::memory_order_relaxed);
  std::atomic_signal_fence(std::memory_order_release);
  a.store(true, std::memory_order_relaxed);

}

首先,第19行中为特定的信号SIGTERM设置了处理句柄。SIGTERM是程序的终止请求。std::atomic_signal_handler在释放操作std:: signal_fence(std::memory_order_release)(第22行)和获取操作std:: signal_fence(std::memory_order_acquire)(第12行)之间建立一个获取-释放栅栏。释放操作不能跨越释放栅栏进行重排序(第22行),而获取操作不能跨越获取栅栏进行重排序(第11行)。因此,第13行assert(b.load(std::memory_order_relax)的断言永远不会触发,因为a.store(true, std:: memory_order_relaxed)(第23行)执行了的话, b.store(true, std::memory_order_relax)(第21行)就一定执行过。

<think>好的,用户让我介绍一下内存栅栏,特别是std::atomic_thread_fence的使用方法。我得先回忆一下自己对这方面知识的理解。内存栅栏是多线程编程中的一个重要概念,用来控制内存操作的顺序,确保不同线程之间的数据同步。不过,用户可能已经有一定的多线程基础,可能在实际开发中遇到了同步问题,所以需要更深入的了解。 首先,我需要明确内存栅栏的作用。它和原子操作有什么区别呢?原子操作保证单个操作的原子性,而内存栅栏则是控制内存操作的顺序,防止指令重排。这涉及到内存模型的知识,比如顺序一致性、松散顺序等。用户可能对C++的内存模型不太熟悉,所以需要简要解释一下。 接下来,std::atomic_thread_fence是C++11引入的,应该属于标准库中的原子操作库部分。它的用法可能分为两种:释放栅栏和获取栅栏,或者还有全栅栏?比如,memory_order_release、memory_order_acquire这些枚举值的使用场景。 然后,我需要考虑用户的实际使用场景。比如,在无锁数据结构中,如何用内存栅栏来确保数据正确同步。或者,在生产者-消费者模式中,如何避免数据竞争。这时候,给出一个具体的例子会更有帮助,比如双缓冲区的实现,或者简单的计数器同步。 不过,用户可能更关心的是如何使用这个函数,而不是背后的理论。所以需要给出具体的代码示例,说明在哪些情况下应该插入栅栏,以及不同的内存顺序参数有什么效果。例如,使用std::atomic_thread_fence(std::memory_order_release)之后,之前的所有写操作都不会被重排到栅栏之后,这对生产者线程来说很重要,确保数据写入完成后才更新标志位。 同时,需要注意栅栏和原子变量的配合使用。比如,在生产者线程中,先写入数据,然后插入释放栅栏,再更新原子标志。消费者线程中,读取原子标志后插入获取栅栏,再读取数据。这样就能保证数据同步的正确性。 可能还需要解释栅栏和原子操作自带内存顺序的区别。比如,原子操作的store和load可以指定内存顺序,而栅栏则是独立于原子操作的,可以更灵活地控制多个非原子操作的顺序。 另外,用户可能会混淆栅栏的不同内存顺序参数,比如release和acquire的作用,以及全栅栏(seq_cst)的使用场景。需要明确说明,释放栅栏防止前面的操作被重排到后面,获取栅栏防止后面的操作被重排到前面,而全栅栏则同时具有这两种效果。 还有,错误使用栅栏可能导致性能问题或者死锁,所以需要提醒用户谨慎使用,确保正确性后再优化性能。比如,在不需要严格顺序的地方使用松散内存顺序,而只在关键路径使用栅栏。 最后,总结一下使用步骤:确定需要同步的内存操作,选择合适的栅栏类型(释放或获取),在正确的位置插入栅栏,并通过原子变量进行线程间通信。同时,强调测试的重要性,尤其是在多线程环境下,问题可能难以复现,需要借助工具进行检测。 可能还需要指出,现代编译器通常会对指令进行优化重排,而CPU也有类似的机制,因此内存栅栏是必要的,确保代码在运行时按照预期顺序执行。这也是为什么在高性能编程中,正确使用内存栅栏至关重要的原因。</think>以下是对内存栅栏和`std::atomic_thread_fence`的详细说明: --- ### 内存栅栏的作用 内存栅栏(Memory Fence)是多线程编程中**强制内存操作顺序的机制**,主要解决以下问题: 1. **编译器和CPU指令重排**:编译器/处理器可能为优化性能调整指令顺序 2. **内存可见性问题**:确保某线程的写操作对其他线程可见 3. **松散内存模型的同步**:在非强一致性内存模型中建立操作顺序约束 --- ### `std::atomic_thread_fence` 核心用法 ```cpp #include <atomic> // 插入内存栅栏 std::atomic_thread_fence(std::memory_order order); ``` #### 内存顺序参数 | 内存顺序 | 作用 | |-------------------|----------------------------------------------------------------------| | `memory_order_relaxed` | **无效参数**(栅栏必须带同步语义) | | `memory_order_acquire` | **获取栅栏**:后续操作不能重排到栅栏前,与释放操作同步 | | `memory_order_release` | **释放栅栏**:前面操作不能重排到栅栏后,与获取操作同步 | | `memory_order_acq_rel` | 同时具有获取和释放语义 | | `memory_order_seq_cst` | 最强约束(默认值),包含`acq_rel`语义且全局顺序一致 | --- ### 典型使用场景 #### 场景1:生产者-消费者模式 ```cpp // 共享数据 int data[1024]; std::atomic<bool> ready(false); // 生产者线程 void producer() { // 写入数据 for(int i=0; i<1024; ++i) data[i] = i; // (1) 非原子操作 std::atomic_thread_fence(std::memory_order_release); // 释放栅栏 ready.store(true, std::memory_order_relaxed); // (2) } // 消费者线程 void consumer() { while(!ready.load(std::memory_order_relaxed)); // (3) std::atomic_thread_fence(std::memory_order_acquire); // 获取栅栏 // 读取数据 for(int i=0; i<1024; ++i) std::cout << data[i]; // (4) } ``` **保证效果**: - 生产者的(1)操作不会被重排到(2)之后 - 消费者的(4)操作不会被重排到(3)之前 - 确保当`ready==true`时,`data`数组已完全写入 --- #### 场景2:双重检查锁定(Double-Checked Locking) ```cpp class Singleton { static std::atomic<Singleton*> instance; static std::mutex mtx; public: static Singleton* get() { Singleton* tmp = instance.load(std::memory_order_acquire); if (!tmp) { std::lock_guard<std::mutex> lock(mtx); tmp = instance.load(std::memory_order_relaxed); if (!tmp) { tmp = new Singleton(); std::atomic_thread_fence(std::memory_order_release); instance.store(tmp, std::memory_order_relaxed); } } return tmp; } }; ``` **关键点**: - 第一次读取使用`memory_order_acquire` - 构造完成后插入释放栅栏,确保对象初始化完成再写入指针 --- ### 使用注意事项 1. **配对使用原则**: - 释放栅栏必须与获取栅栏配对才能建立同步关系 - 单方面使用无法保证可见性 2. **性能代价**: - 全栅栏(`seq_cst`)会严重影响性能 - 在x86架构下,获取/释放栅栏通常只需编译器屏障(无CPU指令) 3. **与原子操作的配合**: ```cpp // 错误用法:栅栏未与原子操作配合 int x; void thread1() { x = 42; std::atomic_thread_fence(std::memory_order_release); } void thread2() { std::atomic_thread_fence(std::memory_order_acquire); std::cout << x; // 无法保证看到42 } ``` 4. **正确用法示范**: ```cpp std::atomic<int> flag(0); int data; void writer() { data = 100; std::atomic_thread_fence(std::memory_order_release); flag.store(1, std::memory_order_relaxed); } void reader() { if (flag.load(std::memory_order_relaxed)) { std::atomic_thread_fence(std::memory_order_acquire); assert(data == 100); // 断言成立 } } ``` --- ### 不同架构下的实现差异 | 架构 | 典型实现方式 | 特点 | |------------|---------------------------------------|-------------------------------| | x86-64 | 编译器屏障(`asm volatile("" ::: "memory")`) | 硬件本身有较强内存模型 | | ARM | `dmb`指令 | 需要显式内存屏障指令 | | PowerPC | `lwsync`/`sync`指令 | 内存模型更松散 | --- ### 调试建议 1. 使用ThreadSanitizer检测数据竞争: ```bash g++ -fsanitize=thread -g example.cpp ``` 2. 通过`godbolt.org`观察生成的汇编指令: - 检查栅栏是否生成正确的屏障指令 - 验证编译器优化是否符合预期 --- ### 替代方案对比 | 方法 | 优点 | 缺点 | |-------------------------|-------------------------------|-------------------------------| | `atomic_thread_fence` | 细粒度控制,性能优化空间大 | 需要深入理解内存模型 | | `mutex` | 简单易用 | 有锁的性能开销 | | `atomic`变量带内存顺序 | 直接与变量关联,不易出错 | 灵活性较低 | --- 当需要同步多个非原子变量或优化高性能场景时,`std::atomic_thread_fence`是比互斥锁更轻量的选择,但必须严格遵循内存顺序规则以确保正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值