学习笔记之线程同步,互斥锁死锁,生产者和消费者模型以及信号量相关知识

一、线程同步

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A,A再继续操作。

在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

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

int num = 0;
void* run(void* arg)
{
	for(int i=0; i<1000000; i++)
	{
		num++;
	}
}

int main(int argc,const char* argv[])
{
	pthread_t tid1,tid2;
	pthread_create(&tid1,NULL,run,NULL);
	pthread_create(&tid2,NULL,run,NULL);
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	printf("%d\n",num);
}

二、互斥锁

注意:如果man手册中查不到这系列函数,可以安装以下内容:
	sudo apt-get install glibc-doc
	sudo apt-get install manpages-posix-dev
    
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
功能:定义并初始化互斥锁
    
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t*
mutexattr);
功能:初始化一互斥锁,会被初始化为非锁定状态
    
int pthread_mutex_lock (pthread_mutex_t* mutex);
功能:加锁,当互斥锁已经是锁定状态时,调用者会阻塞,直到互斥被解开,当前线程才会加锁成功并返回。
    
int pthread_mutex_unlock (pthread_mutex_t* mutex);
功能:解锁,解锁后等待加锁的线程才能加锁成功。
    
int pthread_mutex_destroy (pthread_mutex_t* mutex);
功能:销毁锁
    
int pthread_mutex_trylock (pthread_mutex_t *__mutex)
功能:加测试锁,如果不加锁刚立即返回
    
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
							const struct timespec *restrict abs_timeout);
功能:倒计时加锁,如果超时还不加上则立即返回。
struct timespec{
	time_t tv_sec; 		/* Seconds. */
	long int tv_nsec; 	/* Nanoseconds.*/ 1= 1000000000 纳秒
};
#include <stdio.h>
#include <pthread.h>
/*
执行流程:
	1、互斥锁被初始化为非锁定状态
	2、线程1调用pthread_mutex_lock函数,立即返回,互斥量呈锁定状态;
	3、线程2调用pthread_mutex_lock函数,阻塞等待;
	4、线程1调用pthread_mutex_unlock函数,互斥量呈非锁定状态;
	5、线程2被唤醒,从pthread_mutex_lock函数中返回,互斥量呈锁定状态
*/

pthread_mutex_t mutex;
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int num = 0;
void* run(void* arg)
{
	for(int i=0; i<1000000; i++)
	{
		pthread_mutex_lock(&mutex);
		num++;
		pthread_mutex_unlock(&mutex);
	}
}

int main(int argc,const char* argv[])
{
    pthread_mutex_init(&mutex,NULL);
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run,NULL);
    pthread_create(&pid2,NULL,run,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    pthread_mutex_destroy(&mutex);
    printf("%d\n",num);
}

三、读写锁

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
功能:定义并初始化读写锁
    
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
						const pthread_rwlockattr_t *restrict attr);
功能:初始化读写锁
    
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:加读锁,如果不能加则阻塞等待
    
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:加写锁,如果不能加则阻塞等待
    
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:尝试加读锁,如果不能加则立即返回
    
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:尝试加写锁,如果不能加则立即返回
    
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
								const struct timespec *restrict abstime);
功能:带倒计时加读锁,超时则立即返回
    
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
								const struct timespec *restrict abstime);
功能:带倒计时加写锁,超时则立即返回
    
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:销毁读写锁
    
使用读写锁的线程应根据后续的操作进行加锁,如果只对数据进行读取则只加读锁即可,只有对数据进行修改时才应该加写锁,与互斥锁的区别是,它能让只读的线程加上锁,使用原理与文件锁一样。
    线程A 线程B
    读锁 读锁 OK
    读锁 写锁 NO
    写锁 读锁 NO
    写锁 写锁 NO
#include <pthread.h>

// 创建读写锁
pthread_rwlock_t rwlock;

int num = 0;
void* run(void* arg)
{
	for(int i=0; i<1000000; i++)
	{
		// 加写锁
		pthread_rwlock_wrlock(&rwlock);
		num++;
		// 解锁
		pthread_rwlock_unlock(&rwlock);
	}
}

int main(int argc,const char* argv[])
{
	// 初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run,NULL);
    pthread_create(&pid2,NULL,run,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    printf("%d\n",num);
}

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

// 创建读写锁
pthread_rwlock_t rwlock;

void* run1(void* arg)
{
    // 加读锁
    pthread_rwlock_rdlock(&rwlock);
    sleep(1);
    printf("%lu 线程1加读锁成功!\n",pthread_self());
    // 解锁
    pthread_rwlock_unlock(&rwlock);
}
void* run2(void* arg)
{
    // 加读锁
    pthread_rwlock_rdlock(&rwlock);
    sleep(1);
    printf("%lu 线程2加读锁成功!\n",pthread_self());
    // 解锁
    pthread_rwlock_unlock(&rwlock);
}

int main(int argc,const char* argv[])
{
    // 初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run1,NULL);
    pthread_create(&pid2,NULL,run2,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
}

四、死锁问题

什么是死锁:

​ 多个线程互相等待对方资源,在得到所需要的资源之前都不会释放自己的资源,然后造成循环等待的现象,称为死锁。

死锁产生四大必要条件:

1 、资源互斥

2 、占有且等待

3 、资源不可剥夺

4 、环路等待

以上四个条件缺一不可,只要有一个不满足就不能构成死锁。

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

// 创建三个互斥锁并初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;

void* run1(void* arg)
{
    pthread_mutex_lock(&mutex1);
    usleep(100);
    pthread_mutex_lock(&mutex2);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

void* run2(void* arg)
{
    pthread_mutex_lock(&mutex2);
    usleep(100);
    pthread_mutex_lock(&mutex3);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}

void* run3(void* arg)
{
    pthread_mutex_lock(&mutex3);
    usleep(100);
    pthread_mutex_lock(&mutex1);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex3);
}

int main(int argc,const char* argv[])
{
    // 创建三个线程
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,run1,NULL);
    pthread_create(&tid2,NULL,run2,NULL);
    pthread_create(&tid3,NULL,run3,NULL);
    // 主线程等待三个子线程结束
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    return 0;
}
如休防止出现死锁:

构成死锁的四个条件只有一个不成立,就不会产生死锁了。

1 、破坏互斥条件,让资源能够共享使用(准备多份)。

2 、破坏占有且等待的条件,一次申请完成它所有需要的资源,资源没有满足前不让它运行,一旦开始运行就一直归它所有, 缺点是系统资源会被浪费。

3 、破坏不可剥夺的条件,当已经占有了一些资源,请求新的资源而获取不到,然后就释放已经获取到的资源,缺点是实现起来比较复杂,释放已经获取到的资源可能会造成前一阶段的工作浪费。

4 、破坏循环等待的条件,采用顺序分配资源的方法,在系统中为资源进行编号,规定线程必须按照编号递增的顺序获取资源,缺点是资源必须相对稳定,这样就限制了资源的增加和减少。

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

// 创建三个互斥锁并初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;

void* run1(void* arg)
{
	while(1)
	{
        pthread_mutex_lock(&mutex1);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex2))
        break;
        pthread_mutex_unlock(&mutex1);
    }
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

void* run2(void* arg)	
{
	while(1)
	{
        pthread_mutex_lock(&mutex2);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex3))
        break;
        pthread_mutex_unlock(&mutex2);
    }
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}

void* run3(void* arg)
{
	while(1)
	{
        pthread_mutex_lock(&mutex3);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex1))
        break;
        pthread_mutex_unlock(&mutex3);
	}
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex3);
}

int main(int argc,const char* argv[])
{
    // 创建三个线程
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,run1,NULL);
    pthread_create(&tid2,NULL,run2,NULL);
    pthread_create(&tid3,NULL,run3,NULL);
    // 主线程等待三个子线程结束
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    return 0;
}

检测死锁的方法:

总体思路:观察+分析

方法 1 :阅读代码,分析各线程的加锁步骤。

方法 2 :使用strace追踪程序的执行流程。

方法 3 :查看日志观察程序的业务执行过程。

方法 4 :使用gdb调试,查看各线程的执行情况。

1、把断点打在线程创建完毕后
2、run
3、info threads 查看所有线程
4、thread n 进程指定的线程
5、bt 查看线程堆栈信息
6、配合s/n单步调试

五、原子操作

所谓的原子操作就是不可被拆分的操作,对于多线程对全局变量进行操作时,就再也不用再线程锁了,和pthread_mutex_t保护作用是一样的,也是线程安全的,有些编译器在使用时需要加-march=i686编译参数。

type __sync_fetch_and_add (type *ptr, type value); // +
type __sync_fetch_and_sub (type *ptr, type value); // -
type __sync_fetch_and_and (type *ptr, type value); // &
type __sync_fetch_and_or (type *ptr, type value); // |
type __sync_fetch_and_nand (type *ptr, type value); // ~
type __sync_fetch_and_xor (type *ptr, type value); // ^
功能:以上操作返回的是*ptr的旧值
    
type __sync_add_and_fetch (type *ptr, type value); // +
type __sync_sub_and_fetch (type *ptr, type value); // -
type __sync_and_and_fetch (type *ptr, type value); // &
type __sync_or_and_fetch (type *ptr, type value); // |
type __sync_nand_and_fetch (type *ptr, type value); // ~
type __sync_xor_and_fetch (type *ptr, type value); // ^
功能:以上操作返回的是*ptr与value计算后的值
    
type __sync_lock_test_and_set (type *ptr, type value);
功能:把value赋值给*ptr,并返回*ptr的旧值
    
__sync_lock_release(type *ptr);
功能:将*ptr赋值为0
#include <stdio.h>
#include <pthread.h>

int num = 0;
void* run(void* arg)
{
    for(int i=0; i<100000000; i++)
    {
        __sync_fetch_and_add(&num,1);
    }
}
int main(int argc,const char* argv[])
{
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run,NULL);
    pthread_create(&pid2,NULL,run,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    printf("%d\n",num);
}

原子操作的优点:

1 、速度贼快

2 、不会产生死锁

原子操作的缺点:

1 、该功能并不通用,有些编译器不支持。

2 、type只能是整数相关的类型,浮点型和自定义类型无法使用。

六、生产者与消费者模型

生产者: 生产数据的线程,这类的线程负责从用户端、客户端接收数据,然后把数据Push到存储中介。

消费者: 负责消耗数据的线程,对生产者线程生产的数据进行(判断、筛选、使用、响应、存储)处理。
存储中介: 也叫数据仓库,是生产者线程与消费者线程之间的数据缓冲区,用于平衡二者之间的生产速度与消耗速度不均衡的问题,通过缓冲区隔离生产者和消费者,与二者直连相比,避免相互等待,提高运行效率。

问题 1 : 生产快于消费,缓冲区满,撑死。

解决方法:负责生产的线程通知负责消费的线程全速消费,然后进入休眠。

问题 2 : 消费快于生产,缓冲区空,饿死。

解决方法:负责消费的线程通知负责生产的线程全速生产,然后进入休眠。

七、条件变量

条件变量是利用线程间共享的"全局变量"进行同步的一种机制,主要包括两个动作:

​ 1 、线程等待"条件变量的条件成立"而休眠;

​ 2 、等"条件成立"叫醒休眠的线程。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr);
//亦可pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 使调用线程睡入条件变量cond,同时释放互斥锁mutex
int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex);

int pthread_cond_timedwait (pthread_cond_t* cond,
    pthread_mutex_t* mutex,
    const struct timespec* abstime);

struct timespec {
    time_t tv_sec; // Seconds
    long tv_nsec; // Nanoseconds [0 - 999999999]
};

// 从条件变量cond中唤出一个线程,
// 令其重新获得原先的互斥锁
int pthread_cond_signal (pthread_cond_t* cond);

注意:被唤出的线程此刻将从pthread_cond_wait函数中返回,
但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。
    
// 从条件变量cond中唤醒所有线程
int pthread_cond_broadcast (pthread_cond_t* cond);

// 销毁条件变量
int pthread_cond_destroy (pthread_cond_t* cond);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
// 仓库最大容量
#define STACK_MAX 50

// 仓库
char stack[STACK_MAX];
// 仓库的入口和出口
int top;

// 保护仓库入口的互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 当仓库满时线程睡入的条件变量
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
// 当仓库空时线程睡入的条件变量
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;

// 显示仓库内容
void show_stack(const char* opt,char data)
{
    if(50 < top || 0 > top)
    {
        printf("爆仓了 %d\n",top);
        exit(0);
    }
    for(int i=0; i<top; i++)
    {
        printf("%c",stack[i]);
    }
    printf("%s%c\n",opt,data);
}
// 生产者
void* production(void* arg)
{
    for(;;)
    {
        // 往仓库中push数据,加锁保护入口
        pthread_mutex_lock(&mutex);
        // 发现仓库满了
        while(top >= STACK_MAX)
        {
            // 叫睡所有消费线程全速消费,虽然暂时只能醒来一个消费者线程,但所有消费线程已经加
            入到争夺互斥锁的过程
            pthread_cond_broadcast(&empty);
            // 生产者线程睡入满仓条件变量
            pthread_cond_wait(&full,&mutex);
        }
        
        // 生产数据
        char data = rand()%26+'A';
        // 显示仓库
        show_stack("<-",data);
        // 数据添加到仓库
        stack[top++] = data;
        // 模拟生产数据消耗的时间
        usleep(rand()%5*10000);
        
        // 此时仓库已经不空了,叫醒一个空仓条件休眠的消费者线程
        pthread_cond_signal(&empty);
        
        // 生产完毕解锁入口
        pthread_mutex_unlock(&mutex);
    }
}

// 消费者
void* consumption(void* arg)
{
    for(;;)
    {
        // 从仓库中消费数据,加锁保护出口
        pthread_mutex_lock(&mutex);
        // 发现空仓
        while(0 == top)
        {
            // 叫醒所有生产线程全速生产,虽然暂时只能醒来一个生产者线程,但所有生产者线程都加
            入到争夺互斥锁的过程
            pthread_cond_broadcast(&full);
            // 消费者线程睡入空仓条件变量
            pthread_cond_wait(&empty,&mutex);
        }
        
        // 从仓库中消费数据
        char data = stack[top--];
        // 显示仓库内容
        show_stack("->",data);
        // 模拟消费数据所消耗的时间
        usleep(rand()%5*10000);
        
        // 此时已经不满,叫醒一个因为满仓而休眠的线程
        pthread_cond_signal(&full);
        
        // 消费完毕解锁出口
        pthread_mutex_unlock(&mutex);
    }
}

int main(int argc,const char* argv[])
{
    srand(time(NULL));
    
    pthread_t tids[10];
    for(int i=0; i<10; i++)
    {
        if(i%2)
        	pthread_create(tids+i,NULL,production,NULL);
        else
        	pthread_create(tids+i,NULL,consumption,NULL);
    }
    for(int i=0; i<10; i++)
    {
        pthread_join(tids[i],NULL);
    }
    return 0;
}

八、信号量

多线程使用的信号量:
#include <semaphore.h>
sem_t sem;

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:给信号量设置初始值
pshared:信号量的使用范围
	0 线程间使用
	nonzero 进程之间使用
    
int sem_wait(sem_t *sem);
功能:信号量减1操作,如果信号量已经等于0,则阻塞
    
int sem_trywait(sem_t *sem);
功能:尝试对信号量减1操作,能减返回0成功,不能减返回-1失败,不会阻塞
    
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:带倒计时的对信号减1操作,能减返回0成功,不能减超时返回-1失败,阻塞abs_timeout一段时间
    
int sem_post(sem_t *sem);
功能:对信号量执行加1操作
    
int sem_getvalue(sem_t *sem, int *sval);
功能:获取信号量的值
    
int sem_destroy(sem_t *sem);
功能:销毁信号量

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

// 仓库最大容量
#define STACK_MAX 50

// 仓库
char stack[STACK_MAX];
// 仓库的入口和出口
int top;

// 定义信号量
sem_t data_sem,null_sem;

// 显示仓库内容
void show_stack(const char* opt,char data)
{
    if(50 < top || 0 > top)
    {
        printf("爆仓了 %d\n",top);
        exit(0);
    }
    for(int i=0; i<top; i++)
    {
    	printf("%c",stack[i]);
    }
   	 printf("%s%c\n",opt,data);
}

// 生产者
void* production(void* arg)
{
    for(;;)
    {
        // 空位置的数量减1
        sem_wait(&null_sem);
        // 生产数据
        char data = rand()%26+'A';
        // 显示仓库
        show_stack("<-",data);
        // 数据添加到仓库
        stack[top] = data;
        __sync_fetch_and_add(&top,1);
        usleep(rand()%100*100);
        // 数量的数量加1
        sem_post(&data_sem);
    }
}

// 消费者
void* consumption(void* arg)
{
    for(;;)
    {
        // 数据的数量减1
        sem_wait(&data_sem);
        // 从仓库中消费数据
        char data = stack[top];
        __sync_fetch_and_sub(&top,1);
        // 显示仓库内容
        show_stack("->",data);
        // 模拟消费数据所消耗的时间
        usleep(rand()%1000*10);
        // 空位置的数量加1
        sem_post(&null_sem);
    }
}

int main(int argc,const char* argv[])
{
    srand(time(NULL));
    
    // 初始化空位置的数量
    sem_init(&null_sem,0,STACK_MAX);
    // 初始化数据的数量
    sem_init(&data_sem,0,0);
    
    pthread_t tids[10];
    for(int i=0; i<10; i++)
    {
        if(i<5)
       		pthread_create(tids+i,NULL,production,NULL);
        else
       		pthread_create(tids+i,NULL,consumption,NULL);
    }
    
    for(int i=0; i<10; i++)
    {
        pthread_join(tids[i],NULL);
    }
    
    return 0;
}

多进程使用的信号量:

sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
功能:在内核创建一个信号量对象
name:信号量的名字
oflag:
    O_CREAT 不存在则创建信号量,存在则获取
    O_EXCL 如果信号量已经存在,返回失败
mode:信号量的权限
value:信号量的初始值
    
sem_t *sem_open(const char *name, int oflag);
功能:获取信号,或相关属性
    
int sem_unlink(const char *name);
功能:删除信号量

// 进程A
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

int main(int argc,const char* argv[])
{
    sem_t* sem = sem_open("sem_name",O_CREAT,0644,0);
    for(;;)
    {
        sem_post(sem);
        sleep(1);
        int value = 0;
        sem_getvalue(sem,&value);
        printf("%d\n",value);
	}
    
	return 0;
}

// 进程B
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

int main(int argc,const char* argv[])
{
    sem_t* sem = sem_open("sem_name",O_CREAT);
    for(;;)
    {
        sem_wait(sem);
        sleep(2);
        int value = 0;
        sem_getvalue(sem,&value);
        printf("%d\n",value);
    }
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值