Linux线程同步

线程同步

线程同步概念

同步即协同步调,对公共区域数据按序访问。

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。防止数据混乱,产生与时间有关的错误。

“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

数据混乱的原因

  1. 资源共享(独享资源则不会)

  2. 调度随机(意味着数据访问会出现竞争)

  3. 线程间缺乏必要的同步机制

1和2不可避免,着手解决3,使多个线程在访问共享资源的时候,出现互斥

互斥量mutex

每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。

即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

我们要尽量的减少锁的粒度,越小越好。访问共享数据前,加锁,访问结束后,要立即解锁。

主要应用函数

主要应用函数
    pthread_mutex_t mutex //创建锁
1、pthread_mutex_init函数 //初始化
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr); //动态初始化
		eg: pthread_mutex_init(&mutex,NULL);
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化
	在内核中的定义
        #define PTHREAD_MUTEX_INITIALIZER \
   			{ { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
2、pthread_mutex_destroy函数 //销毁锁
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
3、pthread_mutex_lock函数 //加锁
     int pthread_mutex_lock(pthread_mutex_t *mutex);
	 可理解为将mutex --(或 -1),操作后mutex的值为0,会阻塞
4、pthread_mutex_trylock函数 //尝试加锁
     int pthread_mutex_trylock(pthread_mutex_t *mutex);
	 pthread_mutex_trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。
5、pthread_mutex_unlock函数 //解锁
     int pthread_mutex_unlock(pthread_mutex_t *mutex);
	 可理解为将mutex ++(或 +1),操作后mutex的值为1
以上5个函数的返回值都是:成功返回0, 失败返回错误号。	
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex; 变量mutex只有两种取值10

restrict关键字

关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径.

表示指针指向的内容只能通过这个指针进行修改.

restrict 关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。

void *memcpy( void * restrict dest , const void * restrict src, size_t n) ;

这是一个很有用的内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,即 dest指针所指的区域,不能让别的指针来修改,即src的指针不能修改.

pthread_mutex_t的定义

/* Data structures for mutex handling.  The structure of the attribute
   type is not exposed on purpose.  */
typedef union
{
    struct __pthread_mutex_s
    {
        int __lock;
        unsigned int __count;
        int __owner;
#if __WORDSIZE == 64
        unsigned int __nusers;
#endif
        /* KIND must stay at this position in the structure to maintain
           binary compatibility.  */
        int __kind;
#if __WORDSIZE == 64
        int __spins;
        __pthread_list_t __list;
# define __PTHREAD_MUTEX_HAVE_PREV  1
#else
        unsigned int __nusers;
        __extension__ union
        {
            int __spins;
            __pthread_slist_t __list;
        };
#endif
    } __data;
    char __size[__SIZEOF_PTHREAD_MUTEX_T];
    long int __align;
} pthread_mutex_t;

借助互斥锁管理共享数据实现同步

  • 未加互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <errno.h>

void* tfn(void* arg){
	srand(time(NULL));
	while(1){
		printf("hello ");
		sleep(rand() % 3);
		printf("world \n");
		sleep(rand() % 3);
	}
	return (void*)(1);
}

int main(int argc,char* argv[]){

	int ret;//用于判断是否创建了线程
	int i = 0;//用于循环创建线程
	pthread_t tid;//用于create线程的传出参数

	srand(time(NULL));

	int* a;

	ret = pthread_create(&tid,NULL,tfn,(void*)(i));
	if(ret != 0){
        fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
	}

	while(1){
	
		printf("HELLO ");
		sleep(rand() % 3);
		printf("WORLD \n");
		sleep(rand() % 3);
	}

	ret = pthread_join(tid,(void**)&a);
    if(ret != 0){
        fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
	}

	printf("a==%d\n",a);
	
	pthread_exit(NULL);
}
/*执行结果
ubuntu@ubuntu:~/LearnCPP/pthread_test/mutex$ ./pthread_mutex1 
HELLO hello world 
WORLD 
hello world 
hello world 
HELLO WORLD 
hello HELLO world 
^C
*/
  • 加入互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <errno.h>

pthread_mutex_t mutex;//定义一个互斥锁

void* tfn(void* arg){
	srand(time(NULL));
	while(1){
		pthread_mutex_lock(&mutex);//加锁
		printf("hello ");
		sleep(rand() % 3);
		printf("world \n");
		pthread_mutex_unlock(&mutex);//解锁
		sleep(rand() % 3);
	}
	return (void*)(1);
}

int main(int argc,char* argv[]){

	int ret;//用于判断是否创建了线程
	int i = 0;//用于循环创建线程
	pthread_t tid;//用于create线程的传出参数

	srand(time(NULL));

	int* a;

	ret = pthread_mutex_init(&mutex,NULL);//初始化锁
	if(ret != 0){
		fprintf(stderr,"mutex_init error:%s",strerror(ret));
	}

	ret = pthread_create(&tid,NULL,tfn,(void*)(i));
	if(ret != 0){
		fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
	}

	while(1){
		pthread_mutex_lock(&mutex);//加锁
		printf("HELLO ");
		sleep(rand() % 3);
		printf("WORLD \n");
		pthread_mutex_unlock(&mutex);//解锁
		sleep(rand() % 3);
	}

	pthread_join(tid,(void**)&a);
    if(ret != 0){
        fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
	}

	pthread_mutex_destroy(&mutex);//销毁互斥锁

	//sleep(i);
	printf("a==%d\n",a);
	
	pthread_exit(NULL);
}
/*执行结果
ubuntu@ubuntu:~/LearnCPP/pthread_test/mutex$ ./pthread_mutex2
HELLO WORLD 
hello world 
HELLO WORLD 
hello world 
HELLO WORLD 
HELLO WORLD 
hello world 
HELLO WORLD 
^C
*/
//主线程和子线程在访问共享区时就没有交叉输出的情况了

死锁

死锁的情况:
	1. 线程试图对同一个互斥量A加锁两次。对一个锁反复的lock。
	2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁。一个线程不满足于拥有一把锁。

读写锁

与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享

  1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。

  2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。

  3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞, 写锁优先级高

​ 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

​ 读写锁非常适合于对数据结构读的次数远大于写的情况。

相较于互斥量而言,当读线程多的时候,提高访问效率。

总结:读共享,写独占,写优先级高

主要应用函数

1pthread_rwlock_t rwlock;//创建读写锁

2int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr);
eg:
	pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;//静态初始化
    pthread_rwlock_init(&rwlock, NULL);//动态初始化
	成功返回 0,失败返回错误号

3int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
eg:
	pthread_rwlock_destroy(&rwlock);
	成功返回 0,失败返回错误号
        
4int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//加读锁
  	int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//尝试加读锁
	pthread_rwlock_rdlock(&rwlock); 
	pthread_rwlock_tryrdlock(&rwlock);
	成功返回 0,失败返回错误号
        
5int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//加写锁
	int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//尝试加写锁
	pthread_rwlock_wrlock(&rwlock);
	pthread_rwlock_trywrlock(&rwlock); 
	成功返回 0,失败返回错误号

6int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁
    pthread_rwlock_unlock(&rwlock);
	成功返回 0,失败返回错误号

读写锁demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>

int counter;
pthread_rwlock_t rwlock;//定义一个读写锁

void* write_func(void* arg){

    int t;
    int i = (int)arg;

    while(1){
        t = counter;//保存全局变量
        usleep(1000);

        pthread_rwlock_wrlock(&rwlock);//加写锁
        printf("+++++++++%d,pthread:%lu,counter=%d,++counter:%d\n",i,pthread_self(),t,++counter);
        pthread_rwlock_unlock(&rwlock);

        usleep(9000);//给rd线程的机会
    }
    return NULL;
}

void* read_func(void* arg){
    int t = (int)arg;//用于显示第几个线程

    while(1){
        pthread_rwlock_rdlock(&rwlock);//加读锁
        printf("------------------%d,pthread:%lu,counter=%d\n",t,pthread_self(),counter);
        pthread_rwlock_unlock(&rwlock);//解锁

        usleep(2000);//给w线程机会
    }
    return NULL;
}

int main(int argc,char* argv[]){

	int i = 0;//用于循环创建线程
	pthread_t tid[8];//用于create线程的传出参数

    pthread_rwlock_init(&rwlock,NULL);//初始化rwlock

    for(i = 0;i<3;i++){
        pthread_create(&tid[i],NULL,write_func,(void *)i);//创建写线程
    }

    for(i = 0;i<5;i++){
        pthread_create(&tid[i+4],NULL,read_func,(void *)i);//创建读线程
    }

    for(i = 0;i<8;i++){
        pthread_join(tid[i],NULL);//回收子线程
    }

    pthread_rwlock_destroy(&rwlock);

	pthread_exit(NULL);
    return 0;
}

/*执行结果,程序执行得很快,只截取了一点点。
ubuntu@ubuntu:~/LearnCPP/pthread_test/rw_mutex$ ./pthread_mutex2 
------------------0,pthread:139967631570688,counter=0
------------------1,pthread:139967623177984,counter=0
------------------2,pthread:139967614785280,counter=0
------------------3,pthread:139967606392576,counter=0
------------------4,pthread:139967597999872,counter=0
+++++++++2,pthread:139967639963392,counter=0,++counter:1
+++++++++1,pthread:139967648356096,counter=0,++counter:2
+++++++++0,pthread:139967656748800,counter=0,++counter:3
......
*/

条件变量

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

主要应用函数

1pthread_cond_t cond;//创建条件变量

2、pthread_cond_init函数
	初始化一个条件变量
	int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);2:attr表条件变量属性,通常为默认值,传NULL即可
    pthread_cond_init(&cond,NULL);//动态初始化
	pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//静态初始化

3、pthread_cond_destroy函数
    销毁一个条件变量
	int pthread_cond_destroy(pthread_cond_t *cond);
	pthread_cond_destroy(&cond);

4、pthread_cond_wait函数
    阻塞等待一个条件变量
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
	pthread_cond_wait(&cond,&mutex);
	函数作用:
        1.阻塞等待条件变量cond(参1)满足	
        2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
            1.2.两步为一个原子操作,不可以在分离的操作。
        3.当被唤醒,pthread_cond_wait函数返回时,
          解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);

5、pthread_cond_timedwait函数
    限时等待一个条件变量
	int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);3:	参看man sem_timedwait函数,查看struct timespec结构体。
		struct timespec {
			time_t tv_sec;		/* seconds */long   tv_nsec;	/* nanosecondes*/ 纳秒
		}								
	形参abstime:绝对时间。										
		如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。	
			struct timespec t = {1, 0};
			pthread_cond_timedwait (&cond, &mutex, &t); 
			只能定时到 19701100:00:01(早已经过去) 
	正确用法:
		time_t cur = time(NULL); 获取当前时间。
		struct timespec t;	定义timespec 结构体变量t
		t.tv_sec = cur+1; 定时1pthread_cond_timedwait (&cond, &mutex, &t);
                
6、pthread_cond_signal函数
    唤醒至少一个阻塞在条件变量上的线程
	int pthread_cond_signal(pthread_cond_t *cond);

7、pthread_cond_broadcast函数
    唤醒全部阻塞在条件变量上的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
    
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_cond_wait原理

在这里插入图片描述

条件变量的生产者消费者模型

生产者消费者模型,借助条件变量来实现这一模型是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

在这里插入图片描述

消费者等待商品,使用pthread_cond_wait(&cond,&mutex)函数等待满足条件,生产者一有商品满足条件,通过pthread_cond_signal()函数和pthread_cond_broadcast()函数唤醒等待的线程。这个条件变量都要配合线程锁配合使用。

执行到pthread_cond_wait(&cond,&mutex)函数时,他会阻塞等待条件的满足,然后就会解锁,此时生产者就会加锁成功,然后生产者满足条件后,就会唤醒消费者线程,进行加锁。

单消费者生产者demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//商品
struct product {
    struct product* next;
    int num;
};

struct product *head;//头节点

//静态初始化一个条件变量和一个互斥量
pthread_cond_t products = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

//消费者线程
void* consumer(void* arg){
    
    struct product *p;//消费的商品
    while(1){        
        pthread_mutex_lock(&lock);//加锁		
        if(head == NULL){ //说明没有商品,要等待创建商品 
            pthread_cond_wait(&products,&lock);
            //他会阻塞等待条件的满足,
            //先会解锁,此时生产者就会加锁成功,
            //然后生产者满足条件后,就会唤if醒消费者线程,进行加锁。
        }
        
        p = head;//头删法
        head = p->next;
        pthread_mutex_unlock(&lock);//解锁
        
        printf("consumer num %d\n",p->num);//打印消费掉的数据
        free(p);  //p时消费者线程和生产者线程共享的,malloc在堆区,堆区共享  
        sleep(rand()%3);//睡眠
        
    }
}

//生产者线程
void* producter(void* arg){
    
    struct product *p;//生产的商品
    while(1){
        p = malloc(sizeof(struct product));
        p->num = rand()%100 + 1;
        printf("product num %d\n",p->num);//打印出生产的数据
        
        pthread_mutex_lock(&lock);//加锁
        p->next = head;//头插法
        head = p;
        pthread_mutex_unlock(&lock);//解锁
        
        pthread_cond_signal(&products);//满足条件,唤醒消费者线程
        
        sleep(rand()%3);
    }
}


int main(int argc,char* argv[]){
    
    pthread_t pid,cid;
    srand(time(NULL));
    
    //创建两个线程
    pthread_create(&pid,NULL,producter,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    
    //回收两个线程
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    
    pthread_exit(NULL);
    return 0;
}
//运行结果
/*
product num 73
consumer num 73
product num 87
consumer num 87
product num 89
product num 97
consumer num 97
product num 31
consumer num 31
product num 4
consumer num 4
product num 56
consumer num 56
consumer num 89
product num 64
product num 22
^C
*/

多个消费者

使用上面的demo会出现问题的,比如,两个消费者都阻塞在条件变量上,就是说没有数据可以消费。完

事儿都把锁还回去了,生产者此时生产了一个数据,会同时唤醒两个因条件变量阻塞的消费者,完事

两个消费者去抢锁。结果就是 A 消费者拿到锁,开始消费数据,B 消费者阻塞在锁上。之后 A 消费

完数据,把锁归还,B 被唤醒,然而此时已经没有数据供 B 消费了。所以这里有个逻辑错误,消费者

阻塞在条件变量那里应该使用 while 循环。这样 A 消费完数据后,B 做的第一件事不是去拿锁,而是

判定条件变量。

//消费者线程
void* consumer(void* arg){
    
    struct product *p;//消费的商品
    while(1){        
        pthread_mutex_lock(&lock);//加锁		
        while(head == NULL){ //说明没有商品,要等待创建商品 多消费者的时候这里使用while
            pthread_cond_wait(&products,&lock);
            //他会阻塞等待条件的满足,
            //先会解锁,此时生产者就会加锁成功,
            //然后生产者满足条件后,就会唤if醒消费者线程,进行加锁。
        }
        
        p = head;//头删法
        head = p->next;
        pthread_mutex_unlock(&lock);//解锁
        
        printf("consumer num %d\n",p->num);//打印消费掉的数据
        free(p);  //p时消费者线程和生产者线程共享的,malloc在堆区,堆区共享  
        sleep(rand()%3);//睡眠        
    }
}

信号量

主要操作函数

互斥量相当于初始化为1,那么信号量相当于初始化值为N的互斥量,N值代表可以同时访问共享数据的线程数。应用于线程、进程间同步。

#include <semaphore.h>
1sem_t sem;//创建信号量

2int sem_init(sem_t *sem, int pshared, unsigned int value) //初始化信号量
    参数:
    sem: 信号量
	pshared: 0: 用于线程间同步 1: 用于进程间同步
	value:N 值。(指定同时访问的线程数)
    sem_init(&sem,0,3);
3int sem_destroy(sem_t *sem); //销毁一个信号量

4int sem_wait(sem_t *sem); //给信号量加锁 --操作 类似于pthread_mutex_lock
	//当信号量的值为 0 时,再次 -- 就会阻塞
5int sem_post(sem_t *sem);	 //给信号量解锁 ++操作 类似于pthread_mutex_unlock
	//当信号量的值为 N 时, 再次 ++ 就会阻塞
6int sem_trywait(sem_t *sem); //尝试对信号量进行加锁 --操作 类似于pthread_mutex_trylock
	//当信号量的值为 0 时,再次 -- 就会阻塞
7int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); //尝试限时对信号量加锁2:abs_timeout采用的是绝对时间。			
	定时1秒:
		time_t cur = time(NULL); 获取当前时间。
		struct timespec t;	定义timespec 结构体变量t
		t.tv_sec = cur+1; 定时1秒
		t.tv_nsec = t.tv_sec +100; 
		sem_timedwait(&sem, &t); 传参
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。

信号量实现生产者消费者模型

在这里插入图片描述

产品数量star_num,空格数量blanK_num,其中消费者不断的进行消费,将star_num–,blank_num++。同理生产者不断的生产,将star_num++,blank_num–。star_num和blank_num加减到0或者5的时候就不能在进行减或者加了。

sem信号量消费者模型demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define NUM 5
int queue[NUM] = {0};//数据,比作为星星

sem_t blank_num,star_num;//创建空格和星星信号量

void* producer(void* arg){
    int i = 0;
    while(1){
        
        sem_wait(&blank_num); //blank_num--
        queue[i] = rand()%1000 +1;//生产星星
        printf("produce star num = %d\n",queue[i]);
        sem_post(&star_num); //star_num++
        
        i = (i+1)%NUM;  //借助数组的下标实现环形
        
        sleep(rand()%2);        
        
    }
}


void* consumer(void* arg){
    
    int i = 0;
    while(1){
        
        sem_wait(&star_num); //star_num--
        printf("consumer star num = %d\n",queue[i]);
        queue[i] = 0;//消费一个星星
        sem_post(&blank_num); //blank_num++
        
        i = (i+1)%NUM;  //借助数组的下标实现环形
        
        sleep(rand()%2);        
        
    }
}


int main(int argc,char** argv){
    
    pthread_t pid,cid;
    
    srand(time(NULL));
    
    sem_init(&blank_num,0,NUM);//空格子初始化信号量5
    sem_init(&star_num,0,0);//星星信号量初始化0
    
    pthread_create(&pid,NULL,&producer,NULL);//创建生产者线程
    pthread_create(&cid,NULL,&consumer,NULL);//创建消费者线程
    
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    
    sem_destroy(&blank_num);
    sem_destroy(&star_num);
    
    return 0;
}
//执行结果部分
/*
produce star num = 763
consumer star num = 763
produce star num = 715
consumer star num = 715
produce star num = 635
consumer star num = 635
produce star num = 1000
consumer star num = 1000
*/
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值