线程模型

 
1. 线程管理
线程和函数调用很类似:
都与主程序(主线程)共享同样的存储空间;变量的使用范围也一样 --- 线程和函数都只能调用自己函数体内定义的变量和全局变量;
差别是线程的执行和调用线程的执行是并行(异步)的,而函数和调用函数的执行是串行的,所以要注意同步和互斥;
线程在传递参数和结果返回上有自己的接口。
 
线程函数,如果成功都返回0,如果不成功,都会返回非零的错误码。他们不设置errno,因此调用程序不能用perror来报告错误。必须用strerror (error)处理。
 
1.1    用ID引用线程
POSIX线程由Pthread_t类型的ID引用。
#include <pthread.h>
pthread_t pthread_self (void)                        返回自己的线程ID
int pthread_equal (pthread_t t1, pthread_t t2)           比较线程ID是否相等,相等则返回非0值,不相等,返回0
1.2    线程创建
#include <pthread.h>
int pthread_create (pthread_t *restrict thread,
                                   const pthread_attr_t *restrict attr,
                                   void *( *start_routine) (void *),
                                   void *restrict arg );
参数thread 指向新创建的线程ID。
参数attr 表示一个封装了线程的各种属性的属性对象,一般设置为NULL(默认属性)。
参数start_routine 是线程开始执行的时候调用的函数的名字。
函数start_routine 有一个由arg指定的参数,该参数是一个指向void的指针。
函数start_routine 返回一个指向void的指针,该返回值被pthread_join当作退出状态来处理。
参数arg 是传递给函数start_routine 的参数。
 
如果成功pthread_create 返回0,如果不成功,pthread_create返回一个非零的错误码 :
EAGAIN 系统没有创建线程所需的资源,或者创建线程会超出系统对一个进程中线程数的限制。
EINVAL attr参数是无效的
EPERM    调用程序没有适当的权限来设定调度策略或attr指定的参数
例:
#include <stdio.h>
#include <pthread.h>
 
void *start_routine (void *arg)
{
char *temp = (char *) arg ;
                  printf(“%s”, temp) ;
                  return NULL ;
}
 
int main(void)
{
       int       error ;
       pthread   tid ;
       char *buf = “hello” ;        // 线程是在进程的地址空间运行的,所以可以传递指针。
       if ( error = pthread_create(&tid, NULL, start_routine, (void *)buf ) )
       {
              fprintf (stderr, “Failed to create pthread: %s/n”, strerror (error)) ;
exit(1);
}
 
exit(0);
}
 
1.3    分离和连接
在 Linux 中,缺省情况下线程是以一种可连接(joinable)状态被创建的。在可连接状态下,其他线程可以在一个线程终止时对其进行同步,然后用 pthread_join() 函数恢复其终止代码。这个可连接线程的线程资源只有在它被插入之后才能释放。
在分离(detached)状态下,线程的资源在线程终止时会被立即释放。您可以通过对线程属性对象调用 pthread_attr_setdetachstate() 来设置分离状态:
 
分离#include <pthread.h>
int pthread_detach (pthread_t thread); 
pthread_detach函数成功返回0,不成功,返回一个非零的错误码如下:
EINVAL thread 对应的不是一个可接合的线程
ESRCH    没有ID为thread的线程
例1:
void *thr_fun(void *arg);
{}
 
int main(void)
{
int         error;
pthread_t    tid;
if(error = pthread_create(&tid, NULL, thr_fun, NULL);
{
fprintf(stderr, “Failed to create thread: %s/n”, strerror(error) );
exit(1);
}
if(error = pthread_detach(tid) )        //主线程执行分离
{
Fprintf(stderr, “Failed to detach thread: %s/n”, strerror(error);
Exit(2);
}
 
Exit(0);
}
 
例2:
void *thr_fun(void *arg)
{
int error;
if( error = pthread_detach(pthread_self()) )    //线程函数,自己分离出去
{
fprintf( stderr, “Failed to detach :%s/n”, strerror(error) );
exit(1);
}
return NULL;
}
 
连接#include <pthread.h>
int pthread_join (pthread_t thread ,void **value_ptr);
pthread_join函数成功返回0,不成功,返回一个非零的错误码如下:
EINVAL thread 对应的不是一个可接合的线程
ESRCH    没有ID为thread的线程
pthread_join 函数调用函数挂起,直到第一个参数指定的目标线程终止为止。
参数value_ptr 为指向返回值的指针,这个返回值由pthread_exit或者return 返回
 
1.4    退出和取消
1.4.1退出
#include <pthread.h>
void pthread_exit( void *value_ptr);
POSIX没有为pthread_exit 定义任何错误。
value_ptr 必须指向线程退出后仍然存在的数据,因此线程不应该为 value_ptr 使用指向自动局部数据的指针(分配在栈上)。代之以 malloc 函数分配结构或全局结构。。
例:
#include <pthread.h>
#include <string.h>
#include <stdio.h>
 
 
struct foo
{
    int a;
    int b;
    int c;
    int d;
};
 
 
void printfoo(const char *s, const struct foo *fp)
{
    printf(s);
    printf(" structure at 0x%x/n", (unsigned)fp);
    printf(" foo.a = %d/n", fp->a);
    printf(" foo.b = %d/n", (*fp).b);
    printf(" foo.c = %d/n", fp->c);
    printf(" foo.d = %d/n", fp->d);
 
}
 
 
void * thr_fn1(void *arg)
{
    struct foo foo = {1,2,3,4};
 
    printfoo("thread 1:/n", &foo);
    pthread_exit( (void *)&foo );    //   返回变量的指针
}
 
void * thr_fn2(void *arg)
{
    printf("thread 2: ID is %d/n", pthread_self());
    pthread_exit( (void *)0 );
 
}
 
 
int main(void)
{
    int        err;
    pthread_t tid1;
    pthread_t tid2;
    struct foo *fp;               //定义相应变量的指针准备接收
 
    if( err = pthread_create(&tid1, NULL, thr_fn1, NULL) )
    {
        fprintf( stderr, "Failed to create thread 1:%s/n", strerror(err) );
        exit(1);
    }
 
    if( err = pthread_join(tid1, (void **)&fp ) ) //传递指向指针的指针给函数,获得
返回值
    {
        fprintf( stderr, "Failed to join thread 1:%s/n", strerror(err) );
        exit(2);
    }
 
    sleep(1);
 
    printf("parent starting second thread/n");
 
    if( err = pthread_create(&tid2, NULL, thr_fn2, NULL) )
    {
        fprintf( stderr, "Failed to create thread 2:%s/n", strerror(err) );
        exit(3);
    }
 
    sleep(1);
 
    printfoo("parent:/n", fp);
 
}
结果:(由于struct foo foo分配在栈上,被第二个线程覆盖,出现了问题)
thread 1:
 structure at 0xb7e3c430
 foo.a = 1
 foo.b = 2
 foo.c = 3
 foo.d = 4
parent starting second thread
thread 2: ID is -1209807952
parent:
 structure at 0xb7e3c430
 foo.a = -1208727122
 foo.b = -1208557808
 foo.c = -1208598540
 foo.d = -1208605084
 
 
1.4.2取消
#include <pthread.h>
int pthread_cancel (pthread_t thread);
如果成功,函数返回0;不成功,返回非零的错误码,没有定义必须检测的错误;
pthread_cancel 有一个参数,这个参数是要取消的目标线程的ID。
pthread_cancel 不阻塞调用线程,发出取消请求后就返回了,结果有目标线程的类型和取消状态决定。
线程处于PTHREAD_CANCEL_ENABLE状态,它就接受取消请求(默认情况)。
线程处于PTHREAD_CANCEL_DISABLE状态,取消请求将被挂起。
 
pthread_setcancelstate 函数可以改变调用线程的取消状态
#include <pthread.h>
int pthread_setcancelstate( int state, int *oldstate );
如果成功,返回0;不成功,返回一个非零的错误码,没有定义必须检测的错误
state 说明要设置的新状态;oldstate 指向一个整数的指针,这个整数中装载了以前的状态。
作为一个通用的原则,改变了其取消状态或类型的函数应该在返回之前恢复他们的值。
 
pthread_setcanceltype函数可以改变调用线程的取消类型
#include <pthread.h>
int pthread_setcanceltype( int type, int *oldtype);
void pthread_testcancel(void);
pthread_setcanceltype函数如果成功,返回0;如果不成功就返回一个错误码,没有定义必须检测错误码
当线程取消状态是 DISABLE 时,有取消请求则会挂起该请求(称为未决请求);当线程的取消状态变为 ENALBE 时,线程将在下一个取消点上对所有的未决的取消请求进行处理:
线程处于PTHREAD_CANCEL_ASYNCHRONOUS(异步取消), 则立即取消线程;
线程处于PTHREAD_CANCEL_DEFERRED(延迟取消), 则到下一个取消点才取消线程;
pthread_testcancel函数用来设置取消点,POSIX还定义了很多一些其他取消点(详见《UNIX环境高级编程》P332 表12-7
 
1.5向线程传递参数并将值返回
1.5.1 线程内动态分配空间
#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
 
#define BLKSIZE     1024
#define WRITE_FLAG (O_WRONLY|O_CREAT|O_TRUNC)
#define READ_FLAG   O_RDONLY
#define PERM        (S_IRUSR|S_IWUSR)
 
int copyfd (fromfd, tofd)
{
    char    buf[BLKSIZE];
    int     readbytes;
    int     writebytes;
    int     totalbytes;
 
    totalbytes = 0;
 
    for ( ; ;)
    {
        if ( (readbytes = read(fromfd, buf, BLKSIZE)) <= 0 )
        {
            break;
        }
        if ( (writebytes = write(tofd, buf, readbytes)) == -1 )
        {
            break;
        }
 
        totalbytes += writebytes;
 
    }
 
    return totalbytes;
 
}
 
void * copyfile (void *arg)
{
    int       *i;
    int       fromfd;
    int       tofd;
    int       error;
   
   
    fromfd = *((int *)arg);
    tofd   = *((int *)arg + 1);
    i = malloc (sizeof(int));
    (*)i = copyfd (fromfd, tofd);
 
    return (void *)i;
 
}
 
 
int main (int argc, char *argv[])
{
    int *          n;    //同线程函数返回值一个类型
    int            error;
    pthread_t      tid;
    int            fd[2];
 
    if (argc != 3)
    {
        fprintf (stderr, "Usage: %s fromfile tofile/n", argv[0]);
        exit(1);
    }
 
    if ( (fd[0] = open (argv[1], READ_FLAG)) == -1 )
    {
        perror ("Failed to open fromfile");
        exit(2);
    }
    if ( (fd[1] = open (argv[2], WRITE_FLAG, PERM)) == -1)
    {
        perror ("Failed to open tofile");
        exit(3);
    }
 
    if ( (error = pthread_create (&tid, NULL, copyfile, (void *)fd)) != 0 )
    {
        fprintf (stderr, "Failed to create thread:%s/n", strerror(error));
        exit(4);
    }
 
    if ( (error = pthread_join (tid, (void **)&n)) != 0 )
    {
        fprintf (stderr, "Failed to join thread:%s/n", strerror(error));
        exit(5);
    }
 
    printf ("copy bytes:%d/n", (*n));
 
    exit(0);
 
}
程序绿色部分是线程向调用函数返回值:返回值一定是指针;返回值所指必须是malloc申请的堆上数据或者是全局变量,不可用自动的或静态的变量(栈上数据在函数完成后将被释放);
与普通函数调用返回值得关系:如果返回值是指针,所指必须是malloc申请的堆上数据或者是全局变量,不可用自动的或静态的变量(栈上数据在函数完成后将被释放);返回值还可以不是指针,则可以返回栈上数据(值拷贝性质的);
程序红色部分是主程序向线程传递参数
1.5.2 由于动态分配空间来装载单个整数效率很低;所以,另一种方法是由调用线程分配线程返回值所指空间
例:
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 
#define   READ_FLAG    O_RDONLY
#define   WRITE_FLAG   (O_WRONLY|O_CREAT|O_TRUNC)
#define   PERM         (S_IRUSR|S_IWUSR)
#define   BLKSIZE      1024
 
int copyfd (int fromfd, int tofd)
{
    int       readbytes;
    int       writebytes;
    int       totalbytes;
    char      buf[BLKSIZE];
 
    totalbytes = 0;
 
    for (; ;)
    {
        if ( (readbytes = read (fromfd, buf, BLKSIZE)) <= 0 )
        {
            break;
        }
        if ( (writebytes = write (tofd, buf, readbytes)) == -1 )
        {
            break;
        }
 
        totalbytes += writebytes;
 
    }
 
    return totalbytes;
 
}
 
void * copyfilepass (void *arg)
{
    int      *argint;
 
    argint = (int *)arg;
 
    argint[2] = copyfd (argint[0], argint[1]);
 
    close (argint[0]);      //一定要关闭打开的文件描述符
    close (argint[1]);
 
 return (void *)(argint+2); //通过pthread_exit返回指针,也可以通过targs[2]直接访问
(但是不推荐)。
 
}
 
int main (int argc, char *argv[])
{
    int        *n;
    int        err;
    int        targs[3];    //调用线程分配空间
    pthread_t tid;
 
    if (argc != 3)
    {
        fprintf (stderr, "Usage:%s fromfile tofile/n", argv[0]);
        exit (1);
    }
 
    if ( (targs[0] = open (argv[1], READ_FLAG)) == -1 )
    {
        perror ("Failet to open fromfile");
        exit (2);
    }
    if ( (targs[1] = open (argv[2], WRITE_FLAG)) == -1 )
    {
        perror ("Failed to open tofile");
        exit (3);
    }
 
    if ( (err = pthread_create (&tid, NULL, copyfilepass, (void *)targs)) != 0 )
    {
        fprintf (stderr, "Failet to create thread:%s/n", strerror (err));
        exit(4);
    }
    if ( (err = pthread_join (tid, (void **)&n)) != 0 )
    {
        fprintf (stderr, "Failet to join thread:%s/n", strerror (err));
        exit (5);
    }
 
    printf ("Number of bytes copied: %d/n", (*n));
 
    exit (0);
}
 
1.5.3创建了多个线程时,在你确信线程已经完成对参数的访问之前,不要重复使用装载了线程参数的变量
例:
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#define NUMTHREADS 10
 
void * printarg (void *arg)
{
    fprintf (stderr, "Thread %d received %d/n",
(unsigned)pthread_self(), *((int *)arg));
    return NULL;
}
 
int main(void)
{
    pthread_t    tid[NUMTHREADS];
    int          err;
    int          i;
    int          j;
   
    for (i=0; i<NUMTHREADS; i++)        //主线程中的传递给线程的参数变量一直修改
    {
        if ( (err = pthread_create (tid+i, NULL, printarg, (void *)&i)) != 0 )
        {
            fprintf (stderr, "Failed to create thread:%s/n", strerror (err));
            tid[i] = pthread_self();
        }
        //sleep (1);
       
    }
 
    for (j=0; j<NUMTHREADS; j++)
    {
        if ( pthread_equal(pthread_self(), tid[j]) == 0 )
        {
            continue;
        }
        if ( (err = pthread_join (tid[j], NULL)) != 0 )
        {
            fprintf (stderr, "Failed to join thread:%s/n", strerror (err));
        }
    }
 
    printf ("All threads done/n");
    return 0;
}
A. 运行源程序不做任何修改:
结果:随着系统调度线程的方式而变化。一种可能的情况是在任何一个线程打印出参数值之前,main就结束了创建线程的循环。这种情况下,所有线程打印出的值都是10
B. 在printarg的起始处放置一个sleep(1)调用
结果:每个线程都输出10,这是main完成循环之后的i值
C. 在第一个for循环中pthread_create调用之后,放置一个sleep(1)调用
结果:由于线程在i值发生变化之前就执行了,所以每个线程都输出正确的值
D. 在第一个for循环之后放置sleep(1)
结果:同A
 
1.5.4关于线程返回值的讨论
例:
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
 
void *whichexit (void *arg)
{
    int      n;
    int      np1[1];
    int      *np2;
    char     s1[10];
    char     s2[] = "I am done";
   
    n = 3;
    np1[0] = n;
    np2 = malloc (sizeof(int));
    *np2 = n;
    strcpy (s1, "Done");
 
    return NULL;
 
}
A.n
返回值是以一个指针,而不是一个整数,因此这个值是无效的
B.&n
整数n是自动存储变量,因此在函数终止之后访问他时非法的(这条同样适用于普通函数调用的返回)
C.(int*)n
这是从线程返回整数的常用办法。整数被强制转换成一个指针。当另外一个线程为这个线程调用pthread_join时,又将这个指针重新转换为整数。虽然大多数实现中,能够工作,但还是应该避免此种情况。(c标准中指出整数可以转换成指针,指针也可以转换成整数,但转换的结果是由实现定义的。它不保证将整数转换成指针,然后再将指针转换回来时的结果会和原来的一样
D.np1
数组np1是自动存储类,因此在函数终止后访问这个数组是非法的
E.np2
在被释放之前,动态分配的空间一直是可用的,所以这种方法是安全的
F.s1
数组s1是自动存储类,因此在函数终止后访问这个数组是非法的
G.s2
数组s2是自动存储类,因此在函数终止后访问这个数组是非法的
H. This works
因为文字串有静态存储的生存期限,所以是有效的
I.strerror(EINTR)
如果strerror不是线程安全的,那么是无效的。即使在一个strerror是线程安全的系统中,也不能保证产生的字符串在线程终止后仍然可用
 
1.6 线程安全
1.6.1 在线程函数中不能调用非线程安全的库函数,如果调用可能会产生错误的结果
1.6.2 线程安全(thread-safe):如果多个线程能够同时执行函数的多个活动请求而不会互相干扰,那么这个函数就是线程安全的。
1.6.3 不是所有的函数都要求是线程安全的,例如strerror函数就不保证是线程安全的。所以我们只能在主线程中使用strerror,通常为pthread_create和pthread_join产生错误信息。
1.6.4 关于errno,由于errno是一个全局外部变量,当系统函数产生一个错误时就会设置errno。对于多线程来说,这种实现方式是无法工作的。本质上,每个线程都有一份私有的errno拷贝。主线程不能直接访问一个接合线程的errno,因此,如果需要的话,必须通过pthread_join的最后一个参数来返回这些信息
 
1.7 用户线程和内核线程
1.7.1用户级线程在与他所在的进程中的其他线程竞争处理器资源;用户级线程开销很低。
1.7.2内核级线程在全系统范围内竞争处理器资源;内核级线程的同步和数据共享比整个线程的同步和数据共享的开销要低,但内核级线程的管理要比用户级线程高。
1.7.3混合线程模型,通过提供两个级别的控制,具备了用户级和内核级模型的优点。
1.7.4POSIX模型是一个混合模型,模型中包括两级调度 线程级和内核实体级;线程与用户线程类似,内核实体由内核调度;由线程库决定需要多少内核实体,以及它们是如何映射的;
1.7.5POSIX引入一个线程调度竞争范围,赋予程序员一些控制权限,可以控制怎样将内核实体映射为线程。线程的contentionscope属性可以使PTHREAD_SCOPE_PROCESS和PTHREAD_SCOPE_SYSTEM。带PTHREAD_SCOPE_PROCESS属性的线程与它所在的进程中的其他线程竞争处理器资源(用户级线程);带有PTHREAD_SCOPE_SYSTEM属性的线程很像内核级线程,它们在全系统范围内竞争处理器资源;可以使用pthread_attr_getscope来获得属性,并用pthread_attr_setscope来设置属性。
 
1.8 线程属性(不包括条件对象和互斥锁的属性)
1.8.1POSIX用面向对象的方式表示和设置线程属性,属性对象只有在线程创建的时候会对线程产生影响。POSIX将栈的大小和调度策略这样的特征封装到一个pthread_attr_t类型的对象中,避免了用大量参数来调用pthread_create的情况。
1.8.2属性对象
#include <pthread.h>
int pthread_attr_init(pthread_attr_t   *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_init()函数用默认值对一个线程属性对象初始化。
pthread_attr_destroy()函数将属性对象的值设为无效。
这两个函数都有一个参数,是一个指向pthread_attr_t属性对象的指针。
成功返回0,不成功返回非零的错误码;如果没有足够的内存,来创建线程属性对象,函数pthread_attr_init就将errno设置为ENOMEM。
1.8.3线程状态
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
如果成功,返回0。如果不成功,返回一个非零的错误码。
如果detachstate无效,pthread_attr_setdetachstate函数就将errno设置为EINVAL。
1.8.4线程栈
对进程来说,虚拟地址空间的大小是固定的。进程中只有一个栈,他的大小不是问题;
但对于多个线程的进程来说,同样大小的虚拟地址要被所有的线程共享,如果应用程序时用了太多的线程,致使线程栈的累积大小超过了可用的虚拟地址的大小,这时就需要减小线程栈默认地址的大小。
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *restrict attr,
    void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack(pthread_attr_t *attr,
    void *stackaddr, size_t stacksize);
成功返回0,不成功返回非零的错误码。如果stacksize超出范围,pthread_attr_setstack函数将设置errno为EINVAL。
#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,
    size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
如果成功返回0,不成功返回非零的错误码。 如果attr或guardsize是无效的,返回EINVAL。
1.8.5线程调度
1.8.5.1竞争范围控制函数
对象的竞争范围控制函数,contentionscope可能的取值是PTHREAD_SCOPE_PROCESS和PTHREAD_SCOPE_SYSTEM。
#include <pthread.h>
int pthread_attr_getscope(const pthread_attr_t *restrict attr,
                            Int *restrict contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
成功返回0,不成功返回非零的错误码,这些函数没有定义必须检测的错误。
1.8.5.2调度策路继承方式控制函数
inheritsched的两个取值为,PTHREAD_INHERIT_SCHED(调度策略都从创建线程继承,其他都被忽略)和PTHREAD_EXPLICIT_SCHED(线程使用属性对象attr中的调度策略)。
#include <pthread.h>
int pthread_attr_getinheritsched(const pthread_attr_t *restrict attr,
                                    int *restrict inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
成功返回0,不成功返回非零的错误码,没有定义函数必须检测的错误。
1.8.5.3调度参数的获取和设置函数
#include <sched.h>
struct sched_param结构 中封装了int sched_priority 字段和其他一些调度参数
#include <pthread.h>
int pthread_attr_getschedparam(const pthread_attr_t *restrict attr,
                    strcut sched_param *restrict param);
int pthread_attr_setschedparam(pthread_attr_t *restrict attr,
                    const struct sched_param *restrict param);
成功返回0,不成功返回非零的错误码,没有定义必须检测的错误
1.8.5.4调度参数中调度策略(sched_priority成员)的控制
可以直接对param.sched_priority进行操作;也可以使用如下特定的函数;
#include <pthread.h>
int pthread_attr_getschedpolicy(const pthread_attr_t *restrict attr,
                        int *restrict policy);
int pthread_attr_setschedpolicy(pthread_attr_t *restrict attr,
                        int policy);    
成功返回0,不成功返回非零的错误码,没有定义必须检测的错误。
sched_priority可选的调度策略如下:
SCHED_FIFO(先进先出):
SCHED_RR(轮转调度):当运行线程用完他的时间片之后,就被放入他的优先级队列的末尾,其他行为与先进先出类似。
SCHED_SPORADIC(分散调度):
SCHED_OTHER(最常见的是抢占优先级策略):
这些策略的实际行为取决于调度范围和其他的因素。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值