Linux多线程--pthread

简介

一个进程如果只含有一个控制线程,那么该进程在某一时间段内只能做一件事。但是如果有了多个线程后,可以为每个线程分配一个任务,多个线程并发执行,将这些任务并行化。

下面介绍的线程库函数是由POSIX标准定义的,称为"pthread"或"POSIX线程"。

一、线程标识

线程与进程类似,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但是线程ID只在它所属进程的上下文中才有意义。

线程ID用 pthread_t 数据类型表示,不同操作系统的 pthread_t 可能不同。例如在Linux3.2.0使用无符号长整形表示 pthread_t,FreeBSD 8.0和Mac OS X10.6.8用一个指向 pthread 结构的指针来表示 pthread_t 数据类型。

pthread_self

线程可以通过调用 pthread_self 来获取自身的线程ID。

#include <pthread.h>

// 返回值为调用线程的线程ID
pthread_t pthread_self(void);

由于不同操作系统的 pthread_t 实现不同,因此要将其作为一个结构来处理。在需要比较两个线程ID是否相同时可以使用 pthread_equal 函数。

pthread_equal

#include <pthread.h>

//相等时,返回非0值;不等时,返回0;
int pthread_equal(pthread_t tid1, pthread_t tid2);

二、线程创建

通常情况下程序开始运行时,它是以单进程中的单个控制线程启动的。需要新增线程可以通过调用pthread_create 函数创建线程。

pthread_create

#include <pthread.h>

//创建成功返回 0;失败返回错误码
int pthread_create(pthread_t* restrict tidp,const pthread_attr_t* restrict_attr,void* (*start_rtn)(void*),void *restrict arg);

输入参数:

(1)tidp:事先创建好的pthread_t类型的参数。成功时tidp指向的内存单元被设置为新创建线程的线程ID。

(2)attr:用于定制各种不同的线程属性。通常直接设为NULL。

(3)start_rtn:新创建线程从此函数开始运行。

(4)arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。

传递参数的时候传地址: pthread_create(&ntid, NULL, thr_fn, &param1);

线程函数的第一句通常是获取传入参数:Param tmp = *(Param *)arg;

举例向线程函数传递两个或以上的参数:

#include "apue.h"
#include <pthread.h>
#include "apueerror.h"
#include <iostream>
#include <string>
using namespace std;
pthread_t ntid;
 
void printids(const char *s){
	pid_t		pid;
	pthread_t	tid;
 
	pid = getpid();
	tid = pthread_self();
	printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,
	  (unsigned long)tid, (unsigned long)tid);
}
 
struct Param {
	int a;
	int b;
	int c;
};
 
 
void *thr_fn(void *arg) {
    cout << "----enter sub thread--------" << endl;
	Param tmp = *(Param *)arg;
	cout << "tmp.a=" << tmp.a << endl;
	cout << "tmp.b=" << tmp.b << endl;
	cout << "tmp.c=" << tmp.c << endl;
 
	printids("new thread: ");
	cout << "Change to C++ code!!" << endl;
    cout << "----exit from sub thread----" << endl;
	return((void *)0);
}
 
int main(void){
	int		err;
	int num = 123;
	Param param1;
	param1.a = 11;
	param1.b = 22;
	param1.c = 33;
    //通过结构体向线程函数传入多个参数
	err = pthread_create(&ntid, NULL, thr_fn, &param1);
 
	if (err != 0){
        err_exit(err, "can't create thread");
    }
	printids("main thread:");
	sleep(1);
	exit(0);
}

上面的例子中有两点需要注意,一是主线程要休眠,否则它就有可能退出,导致新线程还没有机会运行整个进程就已经终止了。二是新线程不能安全的使用 ntid,而是通过调用 pthread_self 函数来获取自己的线程ID,因为如果新线程在主线程调用 pthread_create 返回之前就运行了,那么新线程看到的是未经初始化的 ntid 的内容,这个内容并不是正确的线程ID。

线程创建时并不能保证哪个线程会先运行:是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

三、线程终止

在多线程编程中,一个线程结束执行的方式有三种:

  1. 线程将指定函数体中的代码执行完后自行结束;
  2. 线程执行过程中,被同一进程中的其它线程(包括主线程)强制终止;
  3. 线程执行过程中,遇到 pthread_exit() 函数结束执行。

注意,默认属性的线程执行结束后并不会立即释放占用的资源,直到整个进程执行结束,所有线程的资源以及整个进程占用的资源才会被操作系统回收。

 实现线程资源及时回收的常用方法有两种,一种是修改线程属性,另一种是在另一个线程中调用 pthread_join() 函数。


pthread_exit

在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。但会导致子线程还在,内存无法被回收,成为僵尸进程。 

#include <pthread.h>

void pthread_exit(void *retval);

retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。

注意,retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。


在很多情况下,主线程创建并启动了子线程,如果子线程中涉及到大量耗时操作,主线程往往会在子线程之前结束,但是如果主线程中需要子线程的处理结果,就需要等待子线程执行完成。这时就要用到 pthread_join 函数。

pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

args:
    pthread_t thread: 被连接线程的线程号
    void **retval : 指向一个指向被连接线程的返回码的指针的指针
return:
    线程连接的状态,0是成功,非0是失败

当A线程调用线程B并 pthread_join() 时,A线程会处于阻塞状态,直到B线程结束后,A线程才会继续执行下去。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:

  1. 被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。

  2. 一个线程只能被一个线程所连接。

  3. 被连接的线程必须是非分离的,否则连接会出错。
    所以可以看出pthread_join()有两种作用:1-用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。2-对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。 


一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号(后续称“Cancel”信号),从而令目标线程结束执行。

pthread_cancel

#include <pthread.h>

//返回值:若成功,返回0;否则,返回错误编号
int pthread_cancel(pthread_t thread);

发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。在默认情况下,pthread_cancel 函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_ CANCELED (值为 -1)的pthread_exit 函数,但是,线程可以选择忽略取消或者控制如何被取消。注意pthread_cancel并不等待线程终止,它仅仅提出请求。

使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
 
static void *new_thread_start(void *arg){
    printf("新线程--running\n");
    for ( ; ; )
    sleep(1);
    return (void *)0;
}
 
int main(void){
    pthread_t tid;
    void *tret;
    int ret;
 
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
 
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
 
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

当主线程发送取消请求之后,新线程便退出了,而且退出码为-1,也就是 PTHREAD_CANCELED。


默认情况下,线程会响应其它线程发送的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者设置取消方式,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型。

#include <pthread.h>
 
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

pthread_setcancelstate

pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数 state 中给定的值,并将线程之前的取消性状态保存在参数 oldstate 指向的缓冲区中,如果对之前的状态不感兴趣,Linux 允许将参数 oldstate 设置为 NULL;pthread_setcancelstate()调用成功将返回 0,失败返 回非 0 值的错误码。

pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作记为一次原子操作。

参数 state 必须是以下值之一:

PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。

在新线程的 new_thread_start()函数中调用 pthread_setcancelstate()函数将线程的取消性状态设置为 PTHREAD_CANCEL_DISABLE,我们来试试,此时主线程还能不能取消新线程,示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
 
static void *new_thread_start(void *arg){
    /* 设置为不可被取消 */
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    for ( ; ; ) {
        printf("新线程--running\n");
        sleep(2);
    }
    return (void *)0;
}
 
int main(void){
    pthread_t tid;
    void *tret;
    int ret;
 
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
 
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
 
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

结果如下:

可以看出无法终止新线程。 

pthread_setcanceltype

如果线程的取消性状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型, 而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux 下允许将参数 oldtype 设置为 NULL。同样pthread_setcanceltype()函数调用成功将返回 0,失败返回非 0 值的错误码。

pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。

参数 type 必须是以下值之一:

PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型。PTHREAD_CANCEL_ASYNCHRONOUS:收到取消请求后,立即退出。


取消点

若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

那什么是取消点呢?所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,在没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。

取消点函数包括哪些呢?

大家也可以通过 man 手册进行查询,命令为"man 7 pthreads"。


pthread_testcancel

假设线程执行的是一个不含取消点的循环(譬如 for 循环、while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它,就如上小节最后给大家列举的例子。

在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。其函数原型如下所示:

#include <pthread.h>
 
void pthread_testcancel(void);

接下来做一个测试,在 new_thread_start 函数的 for 循环体中执行 pthread_testcancel()函数,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
 
static void *new_thread_start(void *arg){
    printf("新线程--start run\n");
    for ( ; ; ) {
        pthread_testcancel();
    }
    return (void *)0;
}
 
int main(void){
    pthread_t tid;
    void *tret;
    int ret;
 
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);
 
    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }
 
    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
    fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);
    exit(0);
}

结果如下:


pthread_detach

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

#include <pthread.h>
 
int pthread_detach(pthread_t thread);

成功返回 0;失败返回错误码;

一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如:

pthread_detach(pthread_self());

pthread_kill

将信号sig发送到由tid指定的线程,tid所指定的线程必须与调用线程在同一个进程中。

#include <pthread.h>
 
int pthread_kill(pthread_t thread);

成功返回:0
线程不存在:ESRCH
信号不合法:EINVAL

pthread_kill可不是kill,而是向线程发送signal。大部分signal的默认动作是终止进程的运行,所以,我们才要用signal()去抓信号并加上处理函数。如果线程代码内不做处理,则按照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。pthread_kill(threadid, SIGKILL)也一样,杀死整个进程。
如果要获得正确的行为,就需要在线程内实现signal(SIGKILL,sig_handler)了。
所以,如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则,就会影响整个进程。

如果int sig是0,这是一个保留信号,作用是用来判断线程是不是还活着,检查tid的有效性。

四、线程清理

线程可以安排他退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序,线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说他们的执行顺序与他们注册的顺序相反。 

可以通过pthread_cleanup_push和pthread_cleanup_pop函数进行注册和取消清理处理程序。

#include <pthread.h>

void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_clean_pop(int execute);

void(*rtn)(void *): 线程清理函数

当线程执行以下动作时,调用清理函数,调用的参数为arg:

  1. 调用pthread_exit时
  2. 响应取消请求时
  3. 用非零execute参数调用pthread_cleanup_pop时

比如 thread1:
执行

pthread_mutex_lock(&mutex);

//一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接
sock = accept(......);            //这里是随便找的一个可以阻塞的接口

pthread_mutex_unlock(&mutex);

这个例子中,如果线程1执行accept时,线程会阻塞(也就是等在那里,有客户端连接的时候才返回,或则出现其他故障),线程等待中......

这时候线程2发现线程1等了很久,不赖烦了,他想关掉线程1,于是调用pthread_cancel()或者类似函数,请求线程1立即退出。

这时候线程1仍然在accept等待中,当它收到线程2的cancel信号后,就会从accept中退出,然后终止线程,注意这个时候线程1还没有执行:
pthread_mutex_unlock(&mutex);
也就是说锁资源没有释放,这回造成其他线程的死锁问题。

所以必须在线程接收到cancel后用一种方法来保证异常退出(也就是线程没达到终点)时可以做清理工作(主要是解锁方面),pthread_cleanup_push与pthread_cleanup_pop就是这样的。

pthread_cleanup_push(some_clean_func,...)
pthread_mutex_lock(&mutex);

//一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接
sock = accept(......);            //这里是随便找的一个可以阻塞的接口

pthread_mutex_unlock(&mutex);
pthread_cleanup_pop(0);
return NULL;

上面的代码,如果accept被cancel后线程退出,会自动调用some_clean_func函数,在这个函数中你可以释放锁资源。如果accept没有被cancel,那么线程继续执行,当pthread_mutex_unlock(&mutex);表示线程自己正确的释放资源了,而执行pthread_cleanup_pop(0);也就是取消掉前面的some_clean_func函数。接着return线程就正确的结束了。

通俗点就是:
pthread_cleanup_push注册一个回调函数,如果你的线程在对应的pthread_cleanup_pop之前异常退出(return是正常退出,其他是异常),那么系统就会执行这个回调函数(回调函数要做什么你自己决定)。但是如果在pthread_cleanup_pop之前没有异常退出,pthread_cleanup_pop就把对应的回调函数取消了。


五、线程同步

pthread_mutex_t 

当多个线程需要同时访问修改同一变量的时候,需要进行线程的同步处理,保证数据的一致性。pthread提供了互斥量,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。

互斥量类型为 pthread_mutex_t,在使用互斥变量以前,必须首先对它进行初始化:

  1. 静态分配,设置为常量PTHREAD_MUTEX_INITIALIZER
  2. 动态分配,用函数初始化, 使用结束必须释放

动态分配相关的函数如下:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

加锁解锁函数如下:

#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);

如果调用 pthread_mutex_lock,此时互斥量已经上锁,调用线程会阻塞直到互斥量解锁。

如果不希望线程被阻塞,可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用 pthread_mutex_trylock 时互斥量处于未锁住状态,那么 pthread_mutex_trylock将锁住互斥量,不会出现阻塞直接返回0,否则pthread_mutex_trylock 就会失败,不能锁住互斥量,返回EBUSY。


pthread_cond_t 

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

条件变量的类型为 pthread_cond_t,在使用条件变量以前,必须首先对它进行初始化:

  1. 静态分配,设置为常量PTHREAD_COND_INITIALIZER
  2. 动态分配,用函数初始化

动态分配相关的函数如下:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);

 条件变量相关的函数如下:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); 

pthread_cond_wait用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它 pthread_cond_wait()必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。

pthread_cond_timedwait函数的功能与pthread_cond_wait函数相似,只是多了一个超时。

pthread_cond_signal函数至少能唤醒一个等待该条件的线程,而pthread_cond_broadcast函数则能唤醒等待该条件的所有线程。

六、信号相关

pthread_sigmask

每个线程均有自己的信号屏蔽集(信号掩码),可以使用pthread_sigmask函数来屏蔽某个线程对某些信号的 响应处理,仅留下需要处理该信号的线程来处理指定的信号。

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A-sL1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值