多线程学习

多线程入门概述

向线程传递参数

1.如果线程调用的函数在一个类中时,应该把该函数写成静态成员函数

class Hello {
public:
	static void* say_hello(void * args) {
		printf("hello from thread\n");
		pthread_exit((void *)1);
	}
};

2.向线程调用的函数传入参数,直接在 p t h r e a d pthread pthread_ c r e a t e create create函数最后一个参数加上参数的地址,例:

void* say_hello(void * args) {
	int i = * (int *)args;
	printf("hello from thread, i = %d\n", i);
	pthread_exit((void *)1);
}
int para = 10;
pthread_create(&tid, NULL, say_hello, &para);

3.如果需要传入多个参数,则可以将上一条中的参数地址换成结构体的地址

#include <iostream>
#include <string>
#include <stdio.h>
#include <pthread.h>
struct arg_type {
	int a;
	std::string b;
};
void * say_hello(void *args) {
	arg_type arg_temp = *(arg_type *)args;
	std::cout << "hello from thread,arg_temp.a = " << arg_temp.a << ",arg_temp.b = " << arg_temp.b  << std::endl;
	pthread_exit((void *)1);
}

int main(int argc, char *argv[]) {
	pthread_t tid;
	pthread_t thread;
	arg_type arg_temp;
	arg_temp.a = 10;
	arg_temp.b = "I am number one";
	int iret = pthread_create(&tid, NULL, say_hello, &arg_temp);
	if (iret) {
		std::cout << "pthread_create error: iret=" << iret << std::endl;
		return iret;
	}
	void *retval;
	iret = pthread_join(tid, &retval);
	if (iret) {
		std::cout << "pthread_join error: iret=" << iret << std::endl;
		return iret;
	}
	std::cout << "retval = " << (long)retval << std::endl;
	return 0;
}

打印线程id

4.两种方式打印线程的 i d id id

  • 在线程调用函数中使用 p t h r e a d pthread pthread_ s e l f self self函数来获得线程 i d id id
  • 在创建函数时生成的 i d id id
#include <iostream>
#include <string>
#include <stdio.h>
#include <pthread.h>
void * function(void *args) {
//调用pthread_self()函数打印线程id
	std::cout << "thread id in pthread=" << pthread_self() << std::endl;
	pthread_exit((void *)1);
}

int main(int argc, char *argv[]) {
	int i = 10;
	pthread_t thread;
	int iret = pthread_cretae(&thread, NULL, &function, &i);
	if (iret) {
		std::cout << "pthread_create error: iret=" << iret << std::endl;
		return iret;
	} 
	//创建函数时生成的id
	std::cout << "thread id in process=" << thread << std::endl;
	void *retval;
	iret = pthread_join(tid, &retval);
	if (iret) {
		std::cout << "pthread_join error: iret=" << iret << std::endl;
		return iret;
	}
	std::cout << "retval = " << (long)retval << std::endl;
	return 0;
}

线程属性

5.线程有一组属性可以在线程被创建时指定。该组属性被封装在一个对象中,该对象可用来设置一个或一组线程的属性。线程属性对象的类型为 p t h r e a d pthread pthread_ a t t r attr attr_ t t t,包含在 p t h r e a d . h pthread.h pthread.h头文件中

线程属性结构如下:

typedef struct {
	int detachstate;                     //线程的分离状态
	int schedpolicy;                     //线程调度策略
	structsched_param schedparam;  //线程的调度参数
	int inheritsched;                     //线程的继承性
	int scope;                               //线程的作用域
	size_t guardsize;                    //线程栈末尾的警戒缓冲区大小
	int stackaddr_set;                   //线程的栈设置
	void * stackaddr;                    //线程栈的位置
	size_t stacksize;                     //线程栈的大小
}pthread_attr_t;

以下重点理解分离状态如何设置:

分离状态:若线程终止时,线程处于分离状态,系统将不保留线程终止的状态

通过下面两个函数,设置和获取线程的分离属性:

//获取当前的detachstate线程属性
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//初始设置线程属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int *detachstate);

将线程设置为结束状态分离后,线程的结束状态将不能被进程中的其他线程得到,同时保存线程结束状态的存储区域也将变得不能用

例1:分离一个线程(在未创建时将其设置为分离状态)

/*************************************************************************
	> File Name: test1.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Wed 23 Jan 2019 04:55:02 PM CST
 ************************************************************************/

#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <string.h>
using namespace std;

void * tfn1(void *arg) {
	cout << "the thread" << endl;
	return NULL;
}
int main(void) {
	int iret;
	pthread_t tid;
	pthread_attr_t attr;
	iret = pthread_attr_init(&attr);
	if (iret) {
		cout << "can't init attr " << strerror(iret) << endl;
		return iret;
	}
	iret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	if (iret) {
		cout << "can't set  thread " << strerror(iret) << endl;
		return iret;
	}
	iret = pthread_create(&tid, &attr, tfn1, NULL);
	if (iret) {
		cout << "can't create thread " << strerror(iret) << endl;
		return iret;
	}
	iret = pthread_join(tid, NULL);
	if (iret) {
		cout << "thread has been detached" << endl;
		return iret;
	}
	return 0;
}

例2:分离一个已经创建的线程(调用 p t h r e a d pthread pthread_ d e t a c h detach detach()函数)

/*************************************************************************
	> File Name: test2.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Thu 24 Jan 2019 02:04:58 PM CST
 ************************************************************************/

#include<iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
using namespace std;

void *tfn1(void * arg) {
	cout << "the sub thread sleeping for 5 seconds" << endl;
	sleep(5);
	cout << "the thread done" << endl;
	return NULL;
}
int main() {
	pthread_t tid;
	int iret;
	iret = pthread_create(&tid, NULL, tfn1, NULL);
	if (iret) {
		cout << "can't 'create thread " << strerror(iret) << endl;
		return iret;
	}
	iret = pthread_detach(tid); /* 设置线程为分离状态 */
	if (iret) {
		cout << "can't detach thread " << strerror(iret) << endl;
		return iret;
	}
	iret = pthread_join(tid, NULL);
	/* 由于状态分离,因此得不到线程的结束状态信息,此函数会出错 */
	if (iret) {
		cout << "thread has been detached" << endl;
	}
	cout << "the main thread sleeping for 8 seconds" << endl;
	sleep(8);
	cout << "the main thread done." << endl;
	return 0;
}

在这里插入图片描述

多线程同步

6.多线程同步

如果多个任务可以共享资源,特别是写入某个变量的时候,就需要解决同步的问题

例1:用多线程来模拟火车售票系统

/*************************************************************************
	> File Name: sellTickets.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Thu 24 Jan 2019 02:30:00 PM CST
 ************************************************************************/

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

int total_ticket_num = 20;
void *sell_ticket(void *arg) {
	for (int i = 0; i < 20; ++i) {
		if (total_ticket_num > 0) {
			sleep(1);
			cout << "sell the " << 20 - total_ticket_num + 1 << "th ticket" << endl;
			total_ticket_num--;
		}
	}
}

int main() {
	int iret;
	pthread_t tids[4];
	int i = 0;
	for (i = 0; i < 4; ++i) {
		int iret = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
		if (iret) {
			cout << "pthread_create error, iret=" << iret << endl;
			return iret;
		}
	}
	sleep(20);
	void *retval;
	for (i = 0; i < 4; ++i) {
		iret = pthread_join(tids[i], &retval);
		if (iret) {
			cout << "tid=" << tids[i] << " join error, iret=" << iret << endl;
			return iret;
		}
		cout << "retval=" << (long*)retval << endl;
	}
	return 0;
}

在这里插入图片描述
从上述结果可以得出:本来只有20张票却卖出了23张票,显然其中某个线程在售票过程中条件判断错误,以至于卖出了不存在的票

我们知道,在同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令却很难说清楚哪一个先执行。如果运行的结果依赖于不同的线程执行的先后的话,那么就会造成竞争条件,在这样的情况下,计算机的结果很难预知,所以应尽量避免竞争条件的形成

对于多线程程序来说,同步是指在一定时间内只允许某一个线程访问某个资源。而在此时间内,不允许其他的线程访问该资源。可以通过互斥锁( m u t e x mutex mutex)、条件变量( c o n d i t i o n v a r i a b l e conditionvariable conditionvariable)、读写锁( r e a d e r reader reader- w r i t e r writer writer l o c k lock lock)和信号量( s e m p h o r e semphore semphore)来同步资源

互斥锁

互斥锁:一个特殊的变量,它有锁上( l o c k lock lock)和打开( o p e n open open)两种状态。互斥锁一般被设置为全局变量,打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开,其他想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。

互斥锁使用过程中,主要有pthread_mutex_init、pthread_mutex_destory、pthread_mutex_lock、pthread_mutex_unlock这几个函数,分别完成锁的初始化、锁的销毁、上锁和释放锁操作

创建一个锁有两种方式:静态创建与动态创建
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态创建
pthread_mutex_init(); 动态创建

例2:用互斥锁同步资源(解决火车售票问题)


/*************************************************************************
	> File Name: sellTickets.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Thu 24 Jan 2019 02:30:00 PM CST
 ************************************************************************/

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;
int total_ticket_num = 20;
void *sell_ticket(void *arg) {
	for (int i = 0; i < 20; ++i) {
		pthread_mutex_lock(&mutex_x);
		if (total_ticket_num > 0) {
			sleep(1);
			cout << "sell the " << 20 - total_ticket_num + 1 << "th ticket" << endl;
			total_ticket_num--;
		}
		pthread_mutex_unlock(&mutex_x);
	}
}

int main() {
	int iret;
	pthread_t tids[4];
	int i = 0;
	for (i = 0; i < 4; ++i) {
		int iret = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
		if (iret) {
			cout << "pthread_create error, iret=" << iret << endl;
			return iret;
		}
	}
	sleep(20);
	void *retval;
	for (i = 0; i < 4; ++i) {
		iret = pthread_join(tids[i], &retval);
		if (iret) {
			cout << "tid=" << tids[i] << " join error, iret=" << iret << endl;
			return iret;
		}
		cout << "retval=" << (long*)retval << endl;
	}
	return 0;
}

在这里插入图片描述

p t h r e a d pthread pthread_ m u t e x mutex mutex_ t r y l o c k ( ) trylock() trylock()语义与 p t h r e a d pthread pthread_ m u t e x mutex mutex_ l o c k ( ) lock() lock()类似,不同的是在锁已经被占据时返回 E B U S Y EBUSY EBUSY,而不是挂起等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);

例3:使用 p t h r e a d pthread pthread_ m u t e x mutex mutex_ t r y l o c k ( ) trylock() trylock()测试加锁

/*************************************************************************
	> File Name: sellTickets1.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Fri 25 Jan 2019 07:31:03 PM CST
 ************************************************************************/

#include<iostream>
#include <cstdlib>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
using namespace std;

pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;
int total_ticket_num = 20;
void *sell_ticket1(void *arg) {
	for (int i = 0; i < 20; ++i) {
		pthread_mutex_lock(&mutex_x);
		if (total_ticket_num > 0) {
			cout << "thread1 sell the " << 20 - total_ticket_num + 1 << "th ticket" << endl;
			--total_ticket_num;
		}
		sleep(1);
		pthread_mutex_unlock(&mutex_x);
		sleep(1);
	}
	return 0;
}

void *sell_ticket2(void *arg) {
	int iret = 0;
	iret = pthread_mutex_trylock(&mutex_x);
	for (int i = 0; i < 10; ++i) {
		if (iret == EBUSY) {
			cout << "sell_ticket2:the variable is locked by sell_ticket1." << endl;
		} else if(iret == 0) {
			if (total_ticket_num > 0) {
				cout << "thread2 sell the " << 20 - total_ticket_num + 1 << "th ticket" << endl;
				total_ticket_num--;
			}
			pthread_mutex_unlock(&mutex_x);
		}
		sleep(1);
	}
	return 0;
}

int main() {
	pthread_t tids[2];
	int iret = pthread_create(&tids[0], NULL, &sell_ticket1, NULL);
	if (iret) {
		cout << "pthread create error, iret=" << iret << endl;
		return iret;
	}
	iret = pthread_create(&tids[1], NULL, &sell_ticket2, NULL);
	if (iret) {
		cout << "pthread create error, iret=" << iret << endl;
		return iret;
	}
	sleep(30);
	void *retval;
	iret = pthread_join(tids[0], &retval);
	if (iret) {
		cout << "tid=" << tids[0] << " join error, iret=" << iret << endl;
	} else {
		cout << "retval=" << (long*)retval << endl;
	}
	iret = pthread_join(tids[1], &retval);
	if (iret) {
		cout << "tid=" << tids[1] << " join error, iret=" << iret << endl; 
	} else {
		cout << "retval=" << (long*)retval << endl;
	}
	return 0;
}

在这里插入图片描述

条件变量

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,常常和互斥锁一起使用。使用时,条件变量被用来阻塞另一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并重新测试条件是否满足

创建:静态创建 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 动态创建:int
pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t
*cond_attr);

注销:pthread_cond_destroy();

等待:条件等待:pthread_cond_wait() 计时等待:pthread_cond_timedwait();

激发:激活一个等待该条件的线程:pthread_cond_signal();
激活所有线程:pthread_cond_broadcast();

例4:条件变量的初次使用:

/*************************************************************************
	> File Name: use_cond.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Sat 26 Jan 2019 10:04:21 PM CST
 ************************************************************************/

#include<iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
int x = 10;
int y = 20;
void *func1(void *arg) {
	cout << "func1 start" << endl;
	pthread_mutex_lock(&qlock);
	while (x < y) {
		pthread_cond_wait(&qready, &qlock);
	}
	pthread_mutex_unlock(&qlock);
	sleep(3);
	cout << "func1 end" << endl;
}
void *func2(void *arg) {
	cout << "func2 start" << endl;
	pthread_mutex_lock(&qlock);
	x = 20;
	y = 10;
	cout << "has change x and y" << endl;
	pthread_mutex_unlock(&qlock);
	if (x > y) {
		pthread_cond_signal(&qready);
	}
	cout << "func2 end" << endl;
}

int main(int argc, char **argv) {
	pthread_t tid1, tid2;
	int iret;
	iret = pthread_create(&tid1, NULL, func1, NULL);
	if (iret) {
		cout << "pthread1 create error" << endl;
		return iret;
	}
	sleep(2);
	iret = pthread_create(&tid2, NULL, func2, NULL);
	if (iret) {
		cout << "pthread2 create error" << endl;
		return iret;
	}
	sleep(5);
	return 0;
}

在这里插入图片描述

读写锁

(1)读写锁比起互斥锁具有更高的适用性与并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁

  • 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞
  • 当读写锁在读加锁状态时,所有试图以读模式对它加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程都会被阻塞
  • 当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求则长期阻塞

(2)读写锁机制是由POSIX提供的,如果写者没有持有读写锁,那么所有的读者都可以持有这把锁,而一旦有某个写者阻塞在上锁的时候,那么就由POSIX系统来决定是否允许读者获取该锁

初始化和销毁读写锁
初始化:静态赋予常值PTHREAD_RWLOCK_INITIALIZER来初始化;通过调用pthread_rwlock_init()来动态初始化
销毁:调用pthread_rwlock_destory()

//使用默认属性
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destory(pthread_rwlock_t *rwptr);
//使用非默认属性
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destory(pthread_rwlockattr_t *attr);

获取和释放锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);   //获取读出锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);  //获取写入锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);  //释放一个读出或写入锁

采用非阻塞的方式获取读写锁

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

例5:读写锁的使用

/*************************************************************************
	> File Name: lock.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Wed 06 Feb 2019 02:37:08 PM CST
 ************************************************************************/

#include<iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define THREADNUM 5
pthread_rwlock_t rwlock;
void *readers(void *arg) {
	pthread_rwlock_rdlock(&rwlock);
	cout << "reader " << (long)arg << " got the lock" << endl;
	pthread_rwlock_unlock(&rwlock);
	pthread_exit((void*)0);
}
void *writers(void *arg) {
	pthread_rwlock_wrlock(&rwlock);
	cout << "writer " << (long)arg << " got the lock" << endl;
	pthread_rwlock_unlock(&rwlock);
	pthread_exit((void *)0);
}
int main(int argc, char **argv) {
	int iret, i;
	pthread_t writer_id, reader_id;
	pthread_attr_t attr;
	int nreadercount = 1, nwritercount = 1;
	iret = pthread_rwlock_init(&rwlock, NULL);
	if (iret) {
		fprintf(stderr, "init lock failed\n");
		return iret;
	}
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	for (i = 0; i < THREADNUM; ++i) {
		if (i % 3) {
			pthread_create(&reader_id, &attr, readers, (void *)nreadercount);
			cout << "create reader " << nreadercount++ << endl;
		} else {
			pthread_create(&writer_id, &attr, writers, (void *)nwritercount);
			cout << "create writer " << nwritercount++ << endl;
		}
	}
	sleep(5);
	return 0;
}

在这里插入图片描述

信号量

信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区。要使用信号量同步,需要包含头文件 s e m a p h o r e . h semaphore.h semaphore.h。信号量函数的名字都以sem_ 打头

(1)sem_init函数: 用于创建信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

(2)sem_wait函数:用于以原子操作的方式将信号量的值减1

int sem_wait(sem_t *sem);

(3)sem_post函数:用于以源自操作的方式将信号量的值加1

int sem_post(sem_t *sem);

(4)sem_destory函数:用于对用完的信号量进行清理

int sem_destory(sem_t *sem);

例6:用信号量模拟窗口服务系统

/*************************************************************************
	> File Name: wait.cpp
	> Author: ersheng
	> Mail: ershengaaa@163.com 
	> Created Time: Wed 06 Feb 2019 04:00:07 PM CST
 ************************************************************************/

#include<iostream>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
using namespace std;

#define CUSTOMER_NUM 10
//将信号量定义为全局变量,方便多个线程共享
sem_t sem;
//每个线程要运行的例程
void *get_service(void *thread_id) {
	int customer_id = *((int *)thread_id);
	if (sem_wait(&sem) == 0) {
		usleep(100);
		cout << "custom " << customer_id << " receive service ..." << endl;
		sem_post(&sem);
	}
}
int main(int argc, char **argv) {
	//初始化信号量,初始值为2,表示有2个顾客可以同时接受服务
	sem_init(&sem, 0, 2);
	//为每个顾客定义一个线程id
	pthread_t customers[CUSTOMER_NUM];
	int i, iret;
	//为每个顾客生成一个线程
	for (i = 0; i <CUSTOMER_NUM; ++i) {
		int customer_id = i;
		iret = pthread_create(&customers[i], NULL, get_service, &customer_id);
		if (iret) {
			perror("pthread_create");
			return iret;
		} else {
			cout << "Custom " << i << " arrived." << endl;
		}
		usleep(100);
	}
	int j;
	for (j = 0; j < CUSTOMER_NUM; ++j) {
		pthread_join(customers[j], NULL);
	}
	sem_destroy(&sem);
	return 0;
}

在这里插入图片描述

多线程重入

可重入函数指可以由多于一个任务并发使用,而不必担心数据错误的函数。
不可重入函数只能由一个任务所占用,除非能确保函数的互斥

可重入函数可以在任意时刻被中断,稍后再继续运行,且不会丢失数据。可重入函数要在使用全局变量时保护自己的数据

  • 可重入函数特点:

1)不为连续的调用持有静态数据
2)不返回指向静态数据的指针
3)所有数据都由函数的调用者提供
4)使用本地数据,或者通过制作全局数据的本地副本来保护全局数据
5)如果必须访问全局变量,要利用互斥锁、信号量等来保护全局变量
6)绝不调用任何不可重入较函数

  • 不可重入函数特点:

1)函数中使用了静态变量,无论是全局静态变量还是局部静态变量
2)函数返回静态变量
3)函数中调用了不可重入函数
4)函数体内使用了静态的数据结构
5)函数体内调用了malloc()或者free()函数
6)函数体内调用了其他标准I/O函数

编写的多线程程序,通过定义宏_ R E E N T R A N T REENTRANT REENTRANT来告诉编译器需要可重入功能,这个宏的定义必须出现在程序中的任何# i n c l u d e include include语句之前

  • _REENTRANT作用

1)它会对部分函数重新定义他们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r
2)stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数
3)在error.h中定义的变量error现在将成为一个函数调用,它能够以一种安全的多线程方式来获取真正的errno值

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值