同步与锁机制

同步

同步的概念

同步是指一个线程发出某一功能调用时,在没有得到结果之前,该调用不能返回,同时其他线程为保证数据的一致性,不能调用该功能。

同步的目的

同步的目的是为了避免数据混乱,解决时间引起的错误,事实上,线程、进程、信号等都需要同步机制。

条件变量

条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用,给多线程提供一个会和场所。

主要函数
pthread_cond_init/pthread_cond_destroy
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

DESCRIPTION
The pthread_cond_destroy() function shall destroy the given condition variable specified by cond; 
the object becomes, in effect, uninitialized. An implementation may cause pthread_cond_destroy() 
to set the object referenced by cond to an invalid value. A destroyed condition variable object 
can be reinitialized using pthread_cond_init(); the results of otherwise referencing the object 
after it has been destroyed are undefined.

It shall be safe to destroy an initialized condition variable upon which no threads are currently 
blocked. Attempting to destroy a condition variable upon which other threads are currently blocked 
results in undefined behavior.

The pthread_cond_init() function shall initialize the condition variable referenced by cond with 
attributes referenced by attr. If attr is NULL, the default condition variable attributes shall be 
used; the effect is the same as passing the address of a default condition variable attributes 
object. Upon successful initialization, the state of the condition variable shall become initialized.

Only cond itself may be used for performing synchronization. The result of referring to copies of 
cond in calls to pthread_cond_wait(), pthread_cond_timedwait(), pthread_cond_signal(), 
pthread_cond_broadcast(), and pthread_cond_destroy() is undefined.

Attempting to initialize an already initialized condition variable results in undefined behavior.

In cases where default condition variable attributes are appropriate, the macro 
PTHREAD_COND_INITIALIZER can be used to initialize condition variables. The effect shall be equivalent 
to dynamic initialization by a call to pthread_cond_init() with parameter attr specified as NULL, except 
that no error checks are performed. The behavior is undefined if the value specified by the cond argument 
to pthread_cond_destroy() does not refer to an initialized condition variable.

The behavior is undefined if the value specified by the attr argument to pthread_cond_init() does not 
refer to an initialized condition variable attributes object.

RETURN VALUE
If successful, the pthread_cond_destroy() and pthread_cond_init() functions shall return zero; otherwise, 
an error number shall be returned to indicate the error.
pthread_cond_wait/pthread_cond_timedwait

pthread_cond_wait()作用:

  1. 阻塞等待满足条件变量cond (pthread_cond_timedwait()函数则限时等待)
  2. 释放已掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)

    前两步为一个原子操作

  3. 当被唤醒时,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);

DESCRIPTION
The pthread_cond_timedwait() and pthread_cond_wait() functions shall block on a condition 
variable. The application shall ensure that these functions are called with mutex locked 
by the calling thread; otherwise, an error (for PTHREAD_MUTEX_ERRORCHECK and robust mutexes) 
or undefined behavior (for other mutexes) results.

These functions atomically release mutex and cause the calling thread to block on the 
condition variable cond; atomically here means "atomically with respect to access by another 
thread to the mutex and then the condition variable". That is, if another thread is able to 
acquire the mutex after the about-to-block thread has released it, then a subsequent call to 
pthread_cond_broadcast() or pthread_cond_signal() in that thread shall behave as if it were 
issued after the about-to-block thread has blocked.

Upon successful return, the mutex shall have been locked and shall be owned by the calling 
thread. If mutex is a robust mutex where an owner terminated while holding the lock and the 
state is recoverable, the mutex shall be acquired even though the function returns an error 
code.

...
...

RETURN VALUE
Except in the case of [ETIMEDOUT], all these error checks shall act as if they were performed 
immediately at the beginning of processing for the function and shall cause an error return, 
in effect, prior to modifying the state of the mutex specified by mutex or the condition 
variable specified by cond.

Upon successful completion, a value of zero shall be returned; otherwise, an error number shall 
be returned to indicate the error.
pthread_cond_signal/pthread_cond_broadcast
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

DESCRIPTION
These functions shall unblock threads blocked on a condition variable.

The pthread_cond_broadcast() function shall unblock all threads currently blocked on the 
specified condition variable cond.

The pthread_cond_signal() function shall unblock at least one of the threads that are 
blocked on the specified condition variable cond (if any threads are blocked on cond).

If more than one thread is blocked on a condition variable, the scheduling policy shall 
determine the order in which threads are unblocked. When each thread unblocked as a result 
of a pthread_cond_broadcast() or pthread_cond_signal() returns from its call to 
pthread_cond_wait() or pthread_cond_timedwait(), the thread shall own the mutex with which 
it called pthread_cond_wait() or pthread_cond_timedwait(). The thread(s) that are unblocked 
shall contend for the mutex according to the scheduling policy (if applicable), and as if 
each had called pthread_mutex_lock().

RETURN VALUE
If successful, the pthread_cond_broadcast() and pthread_cond_signal() functions shall return zero; otherwise, an error number shall be returned to indicate the error.

锁机制

线程锁

在Linux下,所有用户层面的锁都是建议锁(不具备强制性)。例如,当线程A对某个全局变量进行加锁访问,B在访问前尝试加锁,由于拿不到锁,B将进入阻塞状态。C线程不去加锁而是直接访问该全局变量,那么是可以正常访问的,但极易引起数据混乱。

mutex互斥锁(互斥量)

Linux中提供了互斥锁mutex,每个线程在对资源操作之前先尝试加锁,加锁成功后才能操作,操作结束后再解锁,这样资源仍然共享,线程间的竞争依然存在,但通过锁将资源的访问变成互斥操作,因此就不会再发生时间引起的错误。

主要函数
pthread_mutex_init
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_destroy
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

DESCRIPTION
The pthread_mutex_destroy() function shall destroy the mutex object referenced by mutex; the mutex 
object becomes, in effect, uninitialized. An implementation may cause pthread_mutex_destroy() to 
set the object referenced by mutex to an invalid value.

A destroyed mutex object can be reinitialized using pthread_mutex_init(); the results of otherwise 
referencing the object after it has been destroyed are undefined.

It shall be safe to destroy an initialized mutex that is unlocked. Attempting to destroy a locked 
mutex or a mutex that is referenced (for example, while being used in a pthread_cond_timedwait() 
or pthread_cond_wait()) by another thread results in undefined behavior.

The pthread_mutex_init() function shall initialize the mutex referenced by mutex with attributes 
specified by attr. If attr is NULL, the default mutex attributes are used; the effect shall be the 
same as passing the address of a default mutex attributes object. Upon successful initialization, 
the state of the mutex becomes initialized and unlocked.

Only mutex itself may be used for performing synchronization. The result of referring to copies of 
mutex in calls to pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_unlock(), and 
pthread_mutex_destroy() is undefined.

Attempting to initialize an already initialized mutex results in unde fined behavior.

In cases where default mutex attributes are appropriate, the macro PTHREAD_MUTEX_INITIALIZER can be 
used to initialize mutexes. The effect shall be equivalent to dynamic initialization by a call to 
pthread_mutex_init() with parameter attr specified as NULL, except that no error checks are performed.

The behavior is undefined if the value specified by the mutex  argument to pthread_mutex_destroy() 
does not refer to an initialized mutex.

The behavior is undefined if the value specified by the attr argument to pthread_mutex_init() does 
not refer to an initialized mutex attributes object.

RETURN VALUE
If successful, the pthread_mutex_destroy() and pthread_mutex_init() functions shall return zero; 
otherwise, an error number shall be returned to indicate the error.
pthread_mutex_lock/pthread_mutex_trylock/pthread_mutex_unlock
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

DESCRIPTION
The mutex object referenced by mutex shall be locked by a call to pthread_mutex_lock() that 
returns zero or [EOWNERDEAD]. If the mutex is already locked by another thread, the calling 
thread shall block until the mutex becomes available. This operation shall return with the 
mutex object referenced by mutex in the locked state with the calling thread as its owner. 
If a thread attempts to relock a mutex that it has already locked, pthread_mutex_lock() shall 
behave as described in the Relock column of the following table. If a thread attempts to unlock 
a mutex that it has not locked or a mutex which is unlocked, pthread_mutex_unlock() shall behave 
as described in the Unlock When Not Owner column of the following table.
         ┌───────────┬────────────┬────────────────┬───────────────────────┐
         │Mutex Type │ Robustness │     Relock     │ Unlock When Not Owner │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │NORMAL     │ non-robust │ deadlock       │ undefined behavior    │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │NORMAL     │ robust     │ deadlock       │ error returned        │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │ERRORCHECK │ either     │ error returned │ error returned        │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │RECURSIVE  │ either     │ recursive      │ error returned        │
         │           │            │ (see below)    │                       │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │DEFAULT    │ non-robust │ undefined      │ undefined behavior    │
         │           │            │ behavior       │                       │
         ├───────────┼────────────┼────────────────┼───────────────────────┤
         │DEFAULT    │ robust     │ undefined      │ error returned        │
         │           │            │ behavior       │                       │
         └───────────┴────────────┴────────────────┴───────────────────────┘
        If the mutex type is PTHREAD_MUTEX_DEFAULT, the behavior of pthread_mutex_lock() may 
        correspond to one of the three other standard mutex types as described in the table 
        above. If it does not correspond to one of those three, the behavior is undefined for 
        the cases marked.
    // 注意,mutex类型为recursive时的不同
    Where the table indicates recursive behavior, the mutex shall maintain the concept of a lock 
    count. When a thread successfully acquires a mutex for the  first  time, the lock count shall 
    be set to one. Every time a thread relocks this mutex, the lock count shall be incremented by 
    one. Each time the thread unlocks the mutex, the lock count shall be decremented by one. When 
    the lock count reaches zero, the mutex shall become available for other threads to acquire.

    The pthread_mutex_trylock() function shall be equivalent to pthread_mutex_lock(), except that 
    if the mutex object referenced by mutex is currently locked  (by any thread, including the 
    current thread), the call shall return immediately. If the mutex type is PTHREAD_MUTEX_RECURSIVE 
    and the mutex is currently owned by the calling thread, the mutex lock count shall be incremented 
    by one and the pthread_mutex_trylock() function shall immediately return success.

    The pthread_mutex_unlock() function shall release the mutex object referenced by mutex. The manner 
    in which a mutex is released is dependent upon the mutex's type attribute. If there are threads 
    blocked on the mutex object referenced by mutex when pthread_mutex_unlock() is called, resulting in 
    the mutex becoming available, the scheduling policy shall determine which thread shall acquire the 
    mutex.

    (In the case of PTHREAD_MUTEX_RECURSIVE mutexes, the mutex shall become available when the count 
    reaches zero and the calling thread no longer has any locks on this mutex.) If a signal is delivered 
    to a thread waiting for a mutex, upon return from the signal handler the thread shall resume waiting 
    for the mutex as if it was not interrupted.

    If mutex is a robust mutex and the process containing the owning thread terminated while holding the 
    mutex lock, a call to pthread_mutex_lock() shall return the error value [EOWNERDEAD]. If mutex is a 
    robust mutex and the owning thread terminated while holding the mutex lock, a call to
    pthread_mutex_lock() may return the error value [EOWNERDEAD] even if the process in which the owning 
    thread resides has not terminated. In these cases, the mutex is locked by the thread but the state it 
    protects is marked as inconsistent. The application should ensure that the state is made consistent 
    for reuse and when that is complete call pthread_mutex_consistent(). If the application is unable to 
    recover the state, it should unlock the mutex without a prior call to pthread_mutex_consistent(), 
    after which the mutex is marked permanently unusable.

    If mutex does not refer to an initialized mutex object, the behavior of pthread_mutex_lock() , pthread_mutex_trylock(), and pthread_mutex_unlock() is undefined.

RETURN VALUE
If successful, the pthread_mutex_lock(), pthread_mutex_trylock(), and pthread_mutex_unlock() functions 
shall return zero; otherwise, an error number shall be returned to indicate the error.
死锁
常见的两种造成死锁的方式
  1. 线程试图对同一互斥量加锁两次。
  2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁。
避免死锁的两种方式
  1. 用完即释放
  2. 当存在多个锁时,使用pthread_mutex_trylock()加锁失败时,主动放弃已占有的锁。

读写锁

读写锁的状态

一把读写锁具备三种状态:

  1. 读模式下加锁状态(读锁)
  2. 写模式下加锁状态(写锁)
  3. 不加锁状态
读写锁的特点

与互斥锁类似,但读写锁具有更高的并发性,读写锁也叫共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的,即:写独占,读共享。读写锁适用于对数据结构读远大于写的场景。

  1. 读写锁是"写模式加锁"时,解锁前所有对该锁加锁的线程都会被阻塞
  2. 读写锁是"读模式加锁"时,其他线程以读模式对其加锁会成功,以写模式加锁会阻塞
  3. 读写锁是"读模式加锁"时,其它线程既有试图以写模式加锁,也有试图以读模式加锁时,那么读写锁会阻塞随后的读模式加锁请求,优先满足写锁。一句话总结:读锁写锁并行阻塞,写锁优先级更高。
主要函数
pthread_rwlock_init/pthread_rwlock_destroy
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

DESCRIPTION
The pthread_rwlock_destroy() function shall destroy the read-write lock object referenced by rwlock and 
release any resources used by the lock. The effect of subsequent use of the lock is undefined until the 
lock is reinitialized by another call to pthread_rwlock_init(). An implementation may cause 
pthread_rwlock_destroy() to set the object referenced by rwlock to an invalid value. Results are undefined 
if pthread_rwlock_destroy() is called when any thread holds rwlock. Attempting to destroy an uninitialized 
read-write lock results in undefined behavior.

The pthread_rwlock_init() function shall allocate any resources required to use the read-write lock 
referenced by rwlock and initializes the lock to an unlocked state with attributes referenced by attr. 
If attr is NULL, the default read-write lock attributes shall be used; the effect is the same as passing 
the address of a default read-write lock attributes object. Once initialized, the lock can be used any 
number of times without being reinitialized. Results are undefined if pthread_rwlock_init() is called 
specifying an already initialized read-write lock. Results are undefined if a read-write lock is used 
without first being initialized.

//注意
If the pthread_rwlock_init() function fails, rwlock shall not be initialized and the contents of rwlock 
are undefined.
//与互斥锁类似
In cases where default read-write lock attributes are appropriate, the macro PTHREAD_RWLOCK_INITIALIZER can 
be used to initialize read-write locks. The effect shall be equivalent to dynamic initialization by a call 
to pthread_rwlock_init() with the attr parameter specified as NULL, except that no error checks are performed.

The behavior is undefined if the value specified by the attr argument to pthread_rwlock_init() does not refer 
to an initialized read-write lock attributes object.

RETURN VALUE
If successful, the pthread_rwlock_destroy() and pthread_rwlock_init() functions shall return zero; otherwise, 
an error number shall be returned to indicate the error.
pthread_rwlock_rdlock/pthread_rwlock_tryrdlock
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

DESCRIPTION
The pthread_rwlock_rdlock() function shall apply a read lock to the read-write lock referenced 
by rwlock. The calling thread acquires the read lock if a writer does not hold the lock and 
there are no writers blocked on the lock.

If the Thread Execution Scheduling option is supported, and the threads involved in the lock 
are executing with the scheduling policies SCHED_FIFO or SCHED_RR, the calling thread shall 
not acquire the lock if a writer holds the lock or if writers of higher or equal priority are 
blocked on the lock; otherwise, the calling thread shall acquire the lock.

If the Thread Execution Scheduling option is supported, and the threads involved in the lock 
are executing with the SCHED_SPORADIC scheduling policy, the calling thread shall not acquire 
the lock if a writer holds the lock or if writers of higher or equal priority are blocked on 
the lock; otherwise, the calling thread shall acquire the lock.

If the Thread Execution Scheduling option is not supported, it is implementation-defined 
whether the calling thread acquires the lock when a writer does not hold the lock and there are 
writers blocked on the lock. If a writer holds the lock, the calling thread shall not acquire 
the read lock. If the read lock is not acquired, the calling thread shall block until it can 
acquire the lock. The calling thread may deadlock if at the time the call is made it holds a 
write lock.

A thread may hold multiple concurrent read locks on rwlock (that is, successfully call the 
pthread_rwlock_rdlock() function n times). If so, the application shall ensure that the thread 
performs matching unlocks (that is, it calls the pthread_rwlock_unlock() function n times).

The maximum number of simultaneous read locks that an implementation guarantees can be applied 
to a read-write lock shall be implementation-defined. The pthread_rwlock_rdlock() function may 
fail if this maximum would be exceeded.

The pthread_rwlock_tryrdlock() function shall apply a read lock as in the pthread_rwlock_rdlock() 
function, with the exception that the function shall fail if the equivalent pthread_rwlock_rdlock() 
call would have blocked the calling thread. In no case shall the pthread_rwlock_tryrdlock() function 
ever block; it always either acquires the lock or fails and returns immediately.

Results are undefined if any of these functions are called with an uninitialized read-write lock.

If a signal is delivered to a thread waiting for a read-write lock for reading, upon return from 
the signal handler the thread resumes waiting for the read-write lock for reading as if it was not 
interrupted.

RETURN VALUE
If successful, the pthread_rwlock_rdlock() function shall return zero; otherwise, an error number 
shall be returned to indicate the error.

The pthread_rwlock_tryrdlock() function shall return zero if the lock for reading on the read-write 
lock object referenced by rwlock is acquired. Otherwise, an error number shall be returned to 
indicate the error.
pthread_rwlock_wrlock/pthread_rwlock_trywrlock
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

DESCRIPTION
The pthread_rwlock_trywrlock() function shall apply a write lock like the pthread_rwlock_wrlock() function, with the exception that the function  shall  fail  if any thread currently holds rwlock (for reading or writing).

The pthread_rwlock_wrlock() function shall apply a write lock to the read-write lock referenced by rwlock. The calling thread acquires the write lock if no other thread (reader or writer) holds the read-write lock rwlock. Otherwise, the thread shall block until it can acquire the lock. The calling thread may deadlock if at the time the call is made it holds the read-write lock (whether a read or write lock).

Implementations may favor writers over readers to avoid writer starvation.

Results are undefined if any of these functions are called with an uninitialized read-write lock.

If a signal is delivered to a thread waiting for a read-write lock for writing, upon return from the signal handler the thread resumes waiting for the read-write lock for writing as if it was not interrupted.

RETURN VALUE
The pthread_rwlock_trywrlock() function shall return zero if the lock for writing on the read-write lock object referenced by rwlock is acquired. Otherwise, an error number shall be returned to indicate the error.

If successful, the pthread_rwlock_wrlock() function shall return zero; otherwise, an error number shall be returned to indicate the error.
pthread_rwlock_unlock
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

DESCRIPTION
The pthread_rwlock_unlock() function shall release a lock held on the read-write lock object 
referenced by rwlock. Results are undefined if the read-write lock rwlock is not held by the 
calling thread.

If this function is called to release a read lock from the read-write lock object and there 
are other read locks currently held on this read-write lock object, the read-write lock object 
remains in the read locked state. If this function releases the last read lock for this 
read-write lock object, the read-write lock object shall be put in the unlocked state with no 
owners.

If this function is called to release a write lock for this read-write lock object, the 
read-write lock object shall be put in the unlocked state.

If there are threads blocked on the lock when it becomes available, the scheduling policy shall determine which thread(s) shall 
acquire the lock. If the Thread Execution Scheduling option is supported, when threads executing with the scheduling policies 
SCHED_FIFO, SCHED_RR, or SCHED_SPORADIC are waiting on the lock, they shall acquire the lock in priority order when the lock
becomes available. For equal priority threads, write locks shall take precedence over read locks. If the Thread Execution
Scheduling option is not supported, it is implementa tion-defined whether write locks take precedence over read locks.

Results are undefined if this function is called with an uninitialized read-write lock.

RETURN VALUE
If successful, the pthread_rwlock_unlock() function shall return zero; otherwise, an error number shall be returned to indicate
the error.

进程锁

互斥锁
信号量

由于互斥锁mutex的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住,这样虽然达到了多个线程操作共享数据正确性的目的,却无意中导致了线程的并发性下降线程从并行执行变成了串行执行,信号量是比较折中的一种处理方式,既能保证同步,又能提高并发性。

信号量主要应用函数:
sem_init()
sem_destroy()
sem_wait()
sem_trywait()
sem_timewait()
sem_post()

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -pthread.

DESCRIPTION
sem_init() initializes the unnamed semaphore at the address pointed to by sem. The value 
argument specifies the initial value for the semaphore.

The pshared argument indicates whether this semaphore is to be shared between the threads 
of a process, or between processes.

If pshared has the value 0, then the semaphore is shared between the threads of a process, 
and should be located at some address that is visible to all threads (e.g., a global variable, 
or a variable allocated dynamically on the heap).

If pshared is nonzero, then the semaphore is shared between processes, and should be located 
in a region of shared memory (see shm_open(3), mmap(2), and shmget(2)). (Since a child created 
by fork(2) inherits its parent's memory mappings, it can also access the semaphore.) Any proc-
ess that can access the shared memory region can operate on the semaphore using sem_post(3), 
sem_wait(3), and so on.

Initializing a semaphore that has already been initialized results in undefined behavior.

RETURN VALUE
sem_init() returns 0 on success; on error, -1 is returned, and errno is set to indicate 
the error.
文件锁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值