Linux多线程与同步互斥机制

前言

第1章 Linux多线程

1.1 线程概述

由于进程的地址空间是私有的因此在进程间上下文切换时,系统开销比较大;
为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程;
在同一个进程中创建的线程共享该进程的地址空间;
Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度;

1.1.1 线程特点

1.1.1.1 多个线程共享同一个进程的资源

◆ 可执行的指令
静态数据
全局变量

◆ 进程中打开的文件描述符
◆ 信号处理函数
◆ 当前工作目录
◆ 用户ID
◆ 用户组ID

1.1.1.2 每个线程私有的资源如下

◆ 线程ID (TID)
◆ PC(程序计数器)和相关寄存器

局部变量
◆ 返回地址
◆ 错误号 (errno)
◆ 信号掩码和优先级
◆ 执行状态和属性

1.1.2 线程与进程间关系

线程是一个轻量级的进程;
线程运行需要用到的系统资源(cpu,内存)都是进程给它分配的;
进程是操作系统分配资源的最小单位
线程是操作系统运行的最小单位;
创建线程的目的:为了解决在同一个进程中并发地处理多个任务
./QQ 只能发信息,其它功能都没有
不爽,功能太少,我要听歌,视频 在这里插入图片描述

1.2 多线程编程

1.2.1 线程创建 pthread_create()

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                      void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.

 参数:thread ----》线程的id  系统分配
       attr ----》 线程的属性:可分离属性和不可分离属性,一般情况下设置为NULL,表示使用系统默认的属性(不可分离)
       void *(*start_routine) (void *)----》线程的功能函数,你创建线程需要完成的任务就靠它来实现
        arg ----》传递给线程功能函数的参数
 注意:
	千万不要把线程的创建理解为函数调用(线程它就是进程中一个独立运行的小单元)
 	既然线程是独立运行的小单元,在主线程中如果没有使用线程的回收函数,就必须让主线程稍微延时一下,等子线程有充足的时间去完成代码。
  补充:*((int *)arg)    将空类型的指针arg强转成int * -----》然后解引用
        ((void *)&(a))  将整数a取地址 ----》变成指针了----》然后再将该指针强转成void *类型

1.2.2 线程退出 pthread_exit()

#include <pthread.h>
void pthread_exit(void *retval);
 参数:retval ----》线程退出是状态信息
 返回值:void

1.2.3 线程回收 ptread_join()

  #include <pthread.h>
  int pthread_join(pthread_t thread, void **retval);
  参数:thread ----》你要回收的那个线程的id
        **retval ----》保存线程退出时的状态信息
  返回值:0成功
		  -1失败

1.2.4 线程的取消pthread_cancel()

#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:thread ----》你要取消的那个线程的id号
返回值:0成功
			-1失败

线程的取消分两种状态:
可取消
分为两种类型: 立即取消,适当的时候取消
不可取消
设置线程是否可取消状态:pthread_setcancelstate()

1.2.5 线程的取消属性设置pthread_setcancelstate()

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
参数:state----》可取消:PTHREAD_CANCEL_ENABLE
               不可取消: PTHREAD_CANCEL_DISABLE
int pthread_setcanceltype(int type, int *oldtype);
参数:type----》立即:PTHREAD_CANCEL_ASYNCHRONOUS
               		 立马退出,没有商量的余地
               适当:PTHREAD_CANCEL_DEFERRED
                     在你取消之后会执行下一句代码之后再退出(执行一次)
                不可取消

1.2.6 线程属性的初始化与销毁pthread_attr_init()

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

1.2.7 线程的分离状态:pthread_attr_setdetachstate()

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数:pthread_attr_t attr ----》线程的属性结构体
      	 	Detach state       = PTHREAD_CREATE_JOINABLE ;分离状态                                    
         	Scope            = PTHREAD_SCOPE_SYSTEM;作用域                                    
         	Inherit scheduler    = PTHREAD_INHERIT_SCHED;调度器                                     
         	Scheduling policy   = SCHED_OTHER;调度策略  轮询,抢占式                                     
         	Scheduling priority  = 0;优先级
         	Guard size         = 4096 bytes
         	Stack address       = 0x40196000;栈地址                                    
         	Stack size          = 0x201000 bytes;栈内存的大小                                     
    	detachstate ----》使用宏来表示分离和不可分离状态
                   PTHREAD_CREATE_DETACHED  可分离
                   PTHREAD_CREATE_JOINABLE   不可分离

1.2.8 线程编译

线程编译:gcc   .c  -o  c   -pthread  

1.3 多线程优先级

线程优先级可以由线程属性控制,linux 内核提供了很多函数操作 pthread_attr_t 结构体,修改后将该结构体 通过 函数pthread_create 的第二个参数传递给线程即可

1.3.1 Linux内核三种调度策略

SCHED_OTHER(0):分时调度策略
线程默认调度策略,不区分优先级,该调度方式通过分时来完成的。当线程处于这种调度策略时,对线程进行优先级设置会失败。但高优先级的线程可抢占处于该调度策略的线程。
SCHED_FIFO(1):实时调度策略
先进先出原则,这种调度方式有优先级之分,并且无时间片概念,处于该调度策略时,高优先级的进程将会一直占用CPU直到有更高优先级的线程出现,将线程设置为该调度策略的时候需要超级用户模式。
SCHED_RR(2):实时调度策略
时间片轮转,当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平

1.3.2 Linux线程优先级设置

线程优先级可以由线程属性控制,linux 内核提供了很多函数操作 pthread_attr_t 结构体,修改后将该结构体 通过 函数pthread_create 的第二个参数传递给线程即可

1、线程调度策略设置和获取函数,pthread_attr_t为线程属性,policy是线程调度策略

int pthread_attr_setschedpolicy(pthread_attr_*, int policy)
int pthread_attr_getschedpolicy(const pthread_attr_t *, int * policy)

2、线程优先级设置和获取函数

int pthread_attr_setschedparam(pthread_attr_t *,const struct sched_param *);
int pthread_attr_getschedparam(const pthread_attr_t *,struct sched_param *);

结构体 sched_param 中只有一项 __sched_priority 代表优先级,值越大优先级越高
3、获取系统调度策略的最大最小优先级

int sched_get_priority_max( int policy );
int sched_get_priority_min( int policy );

4、获取和设置线程的继承性 默认是 PTHREAD_INHERIT_SCHED

int pthread_attr_getinheritsched(const pthread_attr_t * attr,int *inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t * attr,int inheritsched);

5、线程绑定CPU

int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset);

cpu_set_t这个结构体的理解类似于select中的fd_set,可以理解为cpu集,通过约定好的宏来进行清除、设置以及判断:

//初始化,设为空 
void CPU_ZERO (cpu_set_t *set);  
//将某个cpu加入cpu集中  
void CPU_SET (int cpu, cpu_set_t *set);  
//将某个cpu从cpu集中移出  
void CPU_CLR (int cpu, cpu_set_t *set);  
//判断某个cpu是否已在cpu集中设置了  
int CPU_ISSET (int cpu, const cpu_set_t *set);

演示代码

#define _GNU_SOURCE
#include <stdio.h>
#include <math.h>
#include <pthread.h>
#include <math.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sched.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>

void *Thread1(void) {
	while(1)
	{
		printf("This is the 1th\n");		
		sleep(1);
	}
}
void *Thread2(void) {
	struct timeval tv;
	while(1){
			gettimeofday(&tv,NULL);
			printf("change priority befor millisecond:%ld\n",tv.tv_sec*1000 + tv.tv_usec/1000);

			sleep(1);
    	}
}
void Thread3(void) {
	struct timeval tv;
	while(1) {
			gettimeofday(&tv, NULL);
			printf("change priority after millisecond:%ld\n",tv.tv_sec*1000 + tv.tv_usec/1000);
			sleep(1);
    	}
}

void *thread_func(void *arg) {   
	cpu_set_t cpuset;
	int policy, ret;  
    struct sched_param param;  
    
    //获取线程调度参数   
    ret = pthread_getschedparam(pthread_self(), &policy, &param);  
    if(ret != 0) {  
        printf("pthread_getschedparam %s\n", strerror(ret) );  
        exit(1);  
    }  
    
    if (SCHED_FIFO == policy) {  
        printf("policy:SCHED_FIFO\n");  
    }  
    else if (SCHED_OTHER == policy) {  
        printf("policy:SCHED_OTHER\n");  
    }  
    else if (SCHED_RR == policy) {  
        printf("policy:SCHED_RR\n");  
    }  
    printf("thread_func priority is %d\n", param.sched_priority);  

	CPU_ZERO(&cpuset);
	CPU_SET(1, &cpuset); /* cpu 1 is in cpuset now */		

	/* bind process to processor 1 */
	if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) !=0) {
		perror("pthread_setaffinity_np");
	}

	printf("Core 1 is running! this pthread PID is %d\n",gettid());

	Thread3();
}

int main(int argc, char *argv[]) { 
	pthread_t my_thread, id1,id2;
	pthread_attr_t attr;
	struct sched_param sp;
	int policy, inher;
	time_t startwtime, endwtime;
	int ret = 0, rs = 0;

	bzero((void*)&sp, sizeof(sp));

	pid_t pid = gettid();
	pthread_t tid = pthread_self();
	printf("main: pid=%d, tid=%lu\n", pid, tid);
	/* 在默认情况下通过pthread_create函数创建的线程是非分离属性的,
	 * 由pthread_create函数的第二个参数决定,
	 * 在非分离的情况下,当一个线程结束的时候,
	 * 它所占用的系统资源并没有完全真正的释放,也没有真正终止。
	 * 只有在pthread_join函数返回时,该线程才会释放自己的资源。
	 * 或者是设置在分离属性的情况下,一个线程结束会立即释放它所占用的资源。
	 * pthread_join()函数会一直阻塞调用线程,直到指定的线程终止。
	 * 当pthread_join()返回之后,应用程序可回收与已终止线程关联的任何数据存储空间。
	 * 但是,同时需要注意,一定要和上面创建的某一线程配套使用,这样还可以起到互斥的作用。
	 * 否则多线程可能抢占CPU资源,导致运行结果不确定。
	 */

	rs = pthread_attr_init(&attr);
	assert(rs == 0);

	//获取继承的调度策略   
    ret = pthread_attr_getinheritsched(&attr, &inher);  
    if (ret!=0) {  
        printf("pthread_attr_getinheritsched\n%s\n", strerror(ret));  
        exit(1);  
    }  
    //   
    if (inher == PTHREAD_EXPLICIT_SCHED) {  
        printf("PTHREAD_EXPLICIT_SCHED\n");  
    }  
    else if (inher == PTHREAD_INHERIT_SCHED) {     
        printf("PTHREAD_INHERIT_SCHED\n");  
        inher = PTHREAD_EXPLICIT_SCHED;  
    }  
    
    //设置继承的调度策略   
    //必需设置inher的属性为 PTHREAD_EXPLICIT_SCHED,否则设置线程的优先级会被忽略   
    ret = pthread_attr_setinheritsched(&attr, inher);  
    if (ret!=0)  
    {  
        printf("pthread_attr_setinheritsched\n%s\n", strerror(ret));  
        exit(1);  
    }  

	policy = SCHED_RR; //需要超级用户权限
	pthread_attr_setschedpolicy( &attr, policy );//设置 调度策略为FIFO
	assert( rs == 0 );
	
	const int priority = 51;	//设置优先级 为51
	sp.__sched_priority = priority;
	if(pthread_attr_setschedparam(&attr, &sp) != 0){//设置优先级
		printf("pthread set sched priority failed\n");
	}

	//创建子线程
	if (pthread_create(&my_thread, &attr, thread_func, NULL) != 0) {
		perror("pthread_create failed\n");
	}

	ret = pthread_create(&id1, NULL, (void*)Thread1, NULL);
	if(ret) {
		printf("cread pthread1 failed\n ");
		return -1;
	}

	ret = pthread_create(&id2, NULL, (void*)Thread2, NULL);
	if(ret) {
		printf("cread pthread2 failed\n ");
		return -1;
	}
	
	//回收子线程
	pthread_join(my_thread, NULL);
	pthread_join(id1, NULL);
	pthread_join(id2, NULL);
	
	return 0;
}

第2章 线程同步和互斥机制

2.1 线程安全机制

多线程共享同一个进程的地址空间**;**
优点:线程间很容易进行通信;
缺点:线程间数据交互容易互相干扰;

2.1.1 信号量

信号量代表某一类资源,其值表示系统中该资源的数量;
信号量的值为非负整数。
1、信号量特点
信号量是一个受保护的变量,只能通过三种操作来访问
◆初始化
◆P操作(申请资源)
当信号量的值等于0时, 该操作将可能引起线程睡眠
当信号量的值大于0时, 该操作将会使得其值减1
◆V操作(释放资源)
当当前没有线程正在等待该信号量时, 该操作将使得其值加1
当当前有线程正在等待该信号量时, 该操作将唤醒该线程
2、信号量相关操作函数

	*sem_t----》信号量的类型*
  (1)信号量的初始化sem_init()
       #include <semaphore.h>
       int sem_init(sem_t *sem, int pshared, unsigned int value);

        Link with -lrt or -pthread.
             参数: sem_t ----》存放待初始化信号量的变量类型
             pshared  ---》信号量共享的范围(0: 线程间使用   非0:进程间使用)
             value ----》信号量的初值
       返回值:0成功	-1失败

   (2)信号量等待(类似于p操作)sem_wait()
   	    #include <semaphore.h>
        int sem_wait(sem_t *sem);
        参数:sem_t  ----》上一步初始化完以后的信号量
		返回值:0成功	-1失败

   (3)信号量的唤醒(类似于v操作) sem_post( ); 
      	#include <semaphore.h>
        int sem_post(sem_t *sem);
        Link with -lrt or -pthread.
		参数:sem_t  ----》上一步初始化完以后的信号量
		返回值:0成功	-1失败

   (4)信号量的销毁sem_destroy()
   		#include <semaphore.h>
   		int sem_destroy(sem_t *sem);

小结:
不管是进程间通信还是线程间的通信:为了达到协调的目的,几乎都是采用阻塞的办法(因为你只有限制了别人的运行,你才有机会“垄断”资源),但是相应的也必须要有让阻塞解除的操作。
进程间的信号量 ----》只有信号量的值为0的时候比较奇特,为0的时候p操作会阻塞当前进程,什么时候解除呢? 另外的进程进行V操作

2.1.2 互斥锁

引入互斥(mutual exclusion)锁的目的是用来保证共享数据操作的完整性。
每个临界资源都由一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源
线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

1、互斥锁的特点
	协调不同线程之间对于共享资源的访问;   
   互斥锁有上锁和解锁两种操作,如果一个线程上锁了(还没有解锁)的情况,其它线程是不能对这个锁进行上锁/解锁操作(如果进行了以上两种操作,会阻塞);
    **谁上锁,谁解锁,不能交叉。**

2、线程锁相关的函数
        pthread_mutex_t  ----》互斥锁的类型
        
  (1)线程锁的初始化pthread_mutex_init() 
      int pthread_mutex_init(pthread_mutex_t *restrict mutex,
          				  const pthread_mutexattr_t *restrict attr);
      参数:mutex ---》锁类型的指针变量
             attr ---》锁的属性  一般设置为NULL,使用默认属性
	 返回值:0成功	 -1失败

  (2)上锁pthread_mutex_lock()
      #include <pthread.h>
      int pthread_mutex_lock(pthread_mutex_t *mutex);  
      参数:mutex ---》初始化完毕之后的互斥锁
            如果有其它的线程已经对该锁上锁了,那么本线程再次上锁就会发生阻塞 
	  返回值:0成功		-1 失败

      int pthread_mutex_trylock(pthread_mutex_t *mutex);
            如果有其它的线程已经对该锁上锁了,本线程就不上锁,返回,不阻塞

 (3)解锁 pthread_mutex_unlock()
      int pthread_mutex_unlock(pthread_mutex_t *mutex);
	  参数:mutex ---》互斥锁
	  返回值:0成功		-1 失败

 (4)销毁pthread_mutex_destroy()
      int pthread_mutex_destroy(pthread_mutex_t *mutex);
      
  线程锁一般配合着条件变量来操作实现效果更好

2.1.3 条件变量

条件变量是另一种逻辑稍微复杂一点点的同步互斥机制,他必须跟互斥锁一起配合使用

1、条件变量的特点
   不可以单独使用,它是配合互斥锁一起使用的;
   wait()这个函数两个作用,先解锁然后让调用它的线程阻塞;
   **一旦wait()被signal()唤醒,wait()就会立马上锁,并解除阻塞**。
2、条件变量相关的接口函数
	pthread_cond_t  ----》条件变量类型
  (1)条件变量的初始化pthread_cond_init()          
       int pthread_cond_init(pthread_cond_t *restrict cond,
          				 const pthread_condattr_t *restrict attr);
       参数:pthread_cond_t ----》条件变量类型
             attr ----》设置为NULL,使用系统默认的属性
		返回值:0成功	-1失败

  (2)阻塞条件变量 pthread_cond_wait()
        int pthread_cond_wait(pthread_cond_t *restrict cond,
          					pthread_mutex_t *restrict mutex);
        参数:cond ---》条件变量
              mutex ---》互斥锁
		返回值:0成功	-1失败

  (3)唤醒条件变量 pthread_cond_signal()
       #include <pthread.h>
		//通知所有的被阻塞的条件变量,唤醒
   	   int pthread_cond_broadcast(pthread_cond_t *cond);
       //唤醒条件变量  
   	   int pthread_cond_signal(pthread_cond_t *cond);
             
 (4)条件变量销毁pthread_cond_destroy()
  #include <pthread.h>
  int pthread_cond_destroy(pthread_cond_t *cond);
  		参数:cond ---》条件变量
              mutex ---》互斥锁
		返回值:0成功	-1失败

小结:
使用互斥锁(互相排斥)或者条件变量注意:在没有人为干扰的情况(没有sleep()函数的情况下),线程间的切换是随机的,作为程序员应该去人为的按照你的逻辑去干扰一下(usleep)。
pthread_cond_wait()跟pthread_cond_signal是一对,不要弄成了其它线程根本就没有pthread_cond_signal,你就想当然地认为只要其它线程解锁了,我的wait就能解除阻塞(不对的)

2.2 线程池

实践项目源代码下载链接:linux线程池实现数据拷贝项目
【线程池实现拷贝功能】
1、使用线程池的概念,利用linux多线程对一个目录以及目录中嵌套的文件进行拷贝,保证拷贝数据的完整性,并且多线程并发进行拷贝,节约程序运行时间,提高工作效率。
2、涉及linux文件IO、linux多线程&线程池、数据结构(链表使用)、Makefile等编程知识

1、图解
在这里插入图片描述
2、线程池的特点:
综合了数据结构的链表,以及线程中的互斥锁,条件变量等等知识点的综合性工具
3、线程池的工作原理
(1)首先我们创建线程池的目的是为了帮助我处理多个任务的,既然是用来处理任务的:我们就想到一个办法,将各个要处理的任务封装成结构体,这个结构体理所当然必须包含最少以下三个成员变量:(通过创建一个任务链表,将所有需要处理的任务封装在这个链表中,那我是如何知道任务节点需要我做什么事情呢?很简单,任务接头体中有函数指针告诉你)

    struct task
    {
          void *(*routine)(void *)  -----》处理任务的函数,之所以该函数指针声明成这个样子,是为了增强通用性
          void *arg; ----》传递给任务函数的参数
          struct task *next;
     }task ;

(2)第一步创建的任务链表由谁来处理呢???-----》由线程池来处理

在这里插入图片描述

A:  线程池究竟是什么东西呢?-----》实际上就是一个结构体(是你精心设计的结构体)                      
typedef struct  thread_pool
{
  	pthread_mutex_t  mymutex;  为扣节点做准备
  	pthread_cond_t   mycond;    配合互斥锁使用
 	struct task *mytask;          任务类型结构体指针

  	unsigned long *pthreadid;      存储线程id
  	int curpthreadnum;           当前线程池中有多少个活动的线程
  	int curtasknum;              当前任务链表中有效任务节点的数量
   	bool shutdown;               标识线程池是否被销毁
}thread_pool ;

B: 线程的功能函数
void *process(void *arg)
{
 	struct thread_pool  *pool2  =  ( struct thread_pool  *)arg;
 	while(1)
 	{
     	pthread_mutex_lock(&(pool2->mymutex));
     	while(pool2->curtasknum==0&&pool2->shutdown==false)
     	{
         	pthread_cond_wait();
     	}
     	if(pool2->curtasknum==0&&pool2->shutdown==true)
     	{
         	pthread_mutex_unlock(&(pool2->mymutex));
         	pthread_exit();
     	}
     	//扣节点(遍历)
     	(head->next)  
     	pool2->curtasknum--;
     	pthread_mutex_unlock(&(pool2->mymutex));
     	//扣完之后调用任务链表结构体中的那个回调函数去处理该任务
      	(pool2->mytask->routine)(   );
 	}
     pthread_exit();
}
for(i=0 ; i<7; i++)
{
     pthread_create(&pthreadid[i],NULL,process,(void *)pool)
}

(3)使用线程池代码的思路:
 封装几个函数:

  A: 线程池的初始化
  	int pool_init(thread_pool *pool , int n)
    {
         pthread_mutex_t  mymutex;  
         pthread_cond_t   mycond; 
         struct task *mytask ---->malloc分配堆空间,你创建的任务的链表的表头节点                        
         pool-> unsigned long *pthreadid; -----》malloc分配堆空间,n*sizeof()
         int curpthreadnum;       n        
         int curtasknum;          0      
         bool shutdown;          false  
         // 循环创建线程    n个
        for(i=0 ; i<n; i++)
		{
		     pthread_create(&pthreadid[i],NULL,process,(void *)pool)
		}
    }
   	void *play_music(void *ags)
    {
              
    }

   B:添加任务节点的函数
   int add_task(thread_pool *pool  ,  void *(*p)(void *),void *arg)
   {
         while(1)
          {
                pthread_mutex_lock(&(pool->mymutex));
                task *mytemtask = malloc()
                mytemtask->routine = play_music;  //调用回调函数play_music()
                mytemtask->arg = NULL;
                mytemtask->next = NULL;
                // 设置了线程池能够处理的链表最大长度是200
                if(pool->curtasknum>200)
                   {
                         printf() ;
                         pthread_mutex_unlock(&(pool->mymutex));

                   }
                添加任务节点(添加到任务链表的结尾位置)
                p =pool->mytask;
                while(p->next !=NULL)
                    p= p->next;
                mytemtask = p->next;
                pool->curtasknum++;
                pthread_mutex_unlock(&(pool2->mymutex));
                pthread_cond_signal();
          }
   }

	C:线程池的销毁
  int pool_destroy(thread_pool *pool)
  {
     	pool->shutdown= true;
       	pthread_mutex_destroy();
      	pthread_cond_destroy();
   	    pool->curtasknum = 0;
     	free() ;  //释放之前申请的堆空间 
   	   for()
        pthread_join()
   }

附录-参考文献

https://blog.csdn.net/weixin_44845857/article/details/118409858

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值