Linux中线程使用详解

photo note 同时被 2 个专栏收录
3 篇文章 0 订阅
12 篇文章 49 订阅

Linux下多线程详解pdf文档下载:点击这里!

Linux中线程和进程的区别http://blog.csdn.net/qq_21792169/article/details/50437304


线程退出的条件:下面任意一个都可以。

1.调用pthread_exit函数退出。

2.其他线程调用pthread_cancel取消该线程,且该线程可被取消。

3.创建线程的进程退出或者整个函数结束。

4.当前线程代码执行完毕。

5.其中的一个线程执行exec类函数执行新的代码,替换当前进程所有地址空间。


当线程中休眠或者死循环时候,需要在住进程中调用pthread_join等待线程结束,死循环可以通过另外一个休眠的线程来结束,举例说明,让LCD显示摄像头数据,但是我们中途需要点击触摸屏来退出显示,视频显示是一个死循环来不停的读取视频数据,那么我们就可以创建两个线程,一个负责视频的不停读取,一个负责获取触摸屏数据,没有数据就休眠,当休眠被唤醒后就调用pthread_cancel取消死循环的线程,设计思路基本是这样。也可以采取进程实现这个操作。


线程与进程
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。

  使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

  2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

 

一、线程标识

  • 线程有ID, 但不是系统唯一, 而是进程环境中唯一有效.
  • 线程的句柄是pthread_t类型, 该类型不能作为整数处理, 而是一个结构.

下面介绍两个函数:

  • 头文件: <pthread.h>
  • 原型: int pthread_equal(pthread_t tid1, pthread_t tid2);
  • 返回值: 相等返回非0, 不相等返回0.
  • 说明: 比较两个线程ID是否相等.

 

  • 头文件: <pthread.h>
  • 原型: pthread_t pthread_self();
  • 返回值: 返回调用线程的线程ID.

二、线程创建

 在执行中创建一个线程, 可以为该线程分配它需要做的工作(线程执行函数), 该线程共享进程的资源. 创建线程的函数pthread_create()

  • 头文件: <pthread.h>
  • 原型: int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(start_rtn)(void), void *restrict arg);
  • 返回值: 成功则返回0, 否则返回错误编号.
  • 参数:
    • tidp: 指向新创建线程ID的变量, 作为函数的输出.
    • attr: 用于定制各种不同的线程属性, NULL为默认属性(见下).
    • start_rtn: 函数指针, 为线程开始执行的函数名.该函数可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由 pthread_join()获取
    • arg: 函数的唯一无类型(void)指针参数, 如要传多个参数, 可以用结构封装.

linux下多线程程序的编译方法:

       因为pthread的库不是linux系统的库,所以在进行编译的时候要加上     -lpthread

       # gcc filename -lpthread  //默认情况下gcc使用c库,要使用额外的库要这样选择使用的库

 

例:thread_create.c

#include <stdio.h>

#include <pthread.h>  //包线程要包含

void *mythread1(void)

{

   int i;

   for(i=0;i<100;i++)

   {

      printf("this is the 1st pthread,created by zieckey.\n");

      sleep(1);

   }

void *mythread2(void)

{

    int i;

for(i=0;i<100;i++)

   {

      printf("this is the 2st pthread,created by zieckey.\n");

      sleep(1);

   }

}

int main()

{

    int ret=0;

    pthread_tid1,id2;

   ret=pthread_create(&id1,NULL,(void*)mythread1,NULL);

    if(ret)

    {

        printf("create pthread error!\n");

         return -1; 

    }

   ret=pthread_create(&id2,NULL,(void*)mythread2,NULL);

    if(ret)

    {

        printf("create pthread error!\n");

         return  -1; 

    }

   pthread_join(id1,NULL);

   pthread_join(id2,NULL);

 

    return 0;

}

编译步骤:gcc thread_create .c -lpthread -othread_create

例2: thread_int.c  //向线程函数传递整形参数

#include <stdio.h>

#include <pthread.h>

#include <unistd.h>

void *create(void *arg)

{

    int *num;

    num=(int *)arg;

   printf("create parameter is %d \n",*num);

    return (void *)0;

}

int main(int argc,char *argv[])

{

   pthread_t tidp;

    int error;

    int test=4;

    int*attr=&test;

 

  error=pthread_create(&tidp,NULL,create,(void*)attr);

    if(error)

     {

       printf("pthread_create is created is not created...\n");

       return -1;

    }

   sleep(1);

  printf("pthread_create is created...\n");

   return 0;

}

注:字符串,结构参数,一样道理


三、线程属性

 pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项:

 __detachstate,表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到  PTHREAD_CREATE_JOINABLE状态。

 

__schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和  SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过  pthread_setschedparam()来改变。

 

__schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。

 

__inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。

 

 __scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:  PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。

 

pthread_attr_t结构中还有一些值,为了设置这些属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、 pthread_attr_destroy()和与各个属性相关的pthread_attr_get(),pthread_attr_set()函数。

 

  pthread_create()中,第二个参数(pthread_attr_t)为将要创建的thread属性。通常情况下配置为NULL,使用缺省设置就可以了。但了解这些属性,有利于更好的理解thread.

属性对象(pthread_attr_t)是不透明的,而且不能通过赋值直接进行修改。系统提供了一组函数,用于初始化、配置和销毁每种对象类型。

 创建属性:

int pthread_attr_init(pthread_attr_t *attr);

创建的属性设定为缺省设置。

 销毁属性:

int pthread_attr_destroy(pthread_attr_t *attr);

 

一:设置分离状态:

线程的分离状态有2种:PTHREAD_CREATE_JOINABLE(非分离状态), PTHREAD_CREATE_DETACHED(分离状态)

分离状态含义如下:

如果使用 PTHREAD_CREATE_JOINABLE 创建非分离线程,则假设应用程序将等待线程完成。也就是说,程序将对线程执行 pthread_join。 非分离线程在终止后,必须要有一个线程用 join 来等待它。否则,不会释放该线程的资源以供新线程使用,而这通常会导致内存泄漏。因此,如果不希望线程被等待,请将该线程作为分离线程来创建。

 

如果使用 PTHREAD_CREATE_DETACHED 创建分离thread,则表明此thread在退出时会自动回收资源和thread ID.

 

Sam之前很喜欢使用分离thread. 但现在慢慢使用中觉得这样是个不好的习惯。因为分离thread有个问题:主程序退出时,很难确认子thread已经退出。只好使用全局变量来标明子thread已经正常退出了。

另外:不管创建分离还是非分离的thread.在子thread全部退出之前退出主程序都是很有风险的。如果主thread选择return,或者调用exit()退出,则所有thread都会被kill掉。这样很容易出错。Sam上次出的问题其实就是这个。但如果主thread只是调用pthread_exit().则仅主线程本身终止。进程及进程内的其他线程将继续存在。所有线程都已终止时,进程也将终止。

 

intpthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

得到当前和分离状态和设置当前的分离状态。

 

二:设置栈溢出保护区大小:

栈溢出概念:

·                     溢出保护可能会导致系统资源浪费。如果应用程序创建大量线程,并且已知这些线程永远不会溢出其栈,则可以关闭溢出保护区。通过关闭溢出保护区,可以节省系统资源。

·                     线程在栈上分配大型数据结构时,可能需要较大的溢出保护区来检测栈溢出。

int pthread_attr_getguardsize(const pthread_attr_t *restrictattr,size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);

设置和得到栈溢出保护区。如果guardsize设为0。则表示不设置栈溢出保护区。guardsize 的值向上舍入为PAGESIZE 的倍数。

 

三:设置thread竞用范围:

竞用范围(PTHREAD_SCOPE_SYSTEM 或 PTHREAD_SCOPE_PROCESS)指 使用 PTHREAD_SCOPE_SYSTEM 时,此线程将与系统中的所有线程进行竞争。使用 PTHREAD_SCOPE_PROCESS 时,此线程将与进程中的其他线程进行竞争。

 

int pthread_attr_getscope(const pthread_attr_t *restrict attr,int*restrict contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);

 

四:设置线程并行级别:

int pthread_getconcurrency(void);
int pthread_setconcurrency(int new_level);

Sam不理解这个意思。

 

五:设置调度策略:

POSIX 标准指定 SCHED_FIFO(先入先出)、SCHED_RR(循环)或 SCHED_OTHER(实现定义的方法)的调度策略属性。

·                     SCHED_FIFO

如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用 SCHED_FIFOSCHED_FIFO 基于 TS 调度类。

·                     SCHED_RR

如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用SCHED_RR(基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户ID 0

SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。

 

int pthread_attr_getschedpolicy(const pthread_attr_t *restrictattr,int *restrict policy);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

 

六:设置优先级:

int pthread_attr_getschedparam(const pthread_attr_t *restrictattr,struct sched_param *restrict param);

int pthread_attr_setschedparam(pthread_attr_t *restrict attr,
              conststruct sched_param *restrict param);

比较复杂,Sam没去研究。

 

七:设置栈大小:

当创建一个thread时,会给它分配一个栈空间,线程栈是从页边界开始的。任何指定的大小都被向上舍入到下一个页边界。不具备访问权限的页将被附加到栈的溢出端(第二项设置中设置)。

指定栈时,还应使用 PTHREAD_CREATE_JOINABLE 创建线程。在该线程的 pthread_join() 调用返回之前,不会释放该栈。在该线程终止之前,不会释放该线程的栈。了解这类线程是否已终止的唯一可靠方式是使用pthread_join

一般情况下,不需要为线程分配栈空间。系统会为每个线程的栈分配指定大小的虚拟内存。

#ulimit -a可以看到这个缺省大小

 四、线程终止

 

如果进程中的任一线程调用了exit,_Exit或者_exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么,把该信号发送到线程会终止整个进程。

单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。

(1):从启动例程中返回,返回值是线程的退出码

(2):线程可以被同一进程中的其他线程取消

(3):线程调用pthread_exit()

pthread_exit函数:

  • 原型: void pthread_exit(void *rval_ptr);
  • 头文件: <pthread.h>
  • 参数: rval_ptr是一个无类型指针, 指向线程的返回值存储变量.

 pthread_join函数:

  • 原型: int pthread_join(pthread_t thread, void **rval_ptr);
  • 头文件: <pthread.h>
  • 返回值: 成功则返回0, 否则返回错误编号.
  • 参数:
    • thread: 线程ID.
    • rval_ptr: 指向返回值的指针(返回值也是个指针).
  • 说明:
    • 调用线程将一直阻塞, 直到指定的线程调用pthread_exit, 从启动例程返回或被取消.
    • 如果线程从它的启动例程返回, rval_ptr包含返回码.
    • 如果线程被取消, 由rval_ptr指定的内存单元置为: PTHREAD_CANCELED.
    • 如果对返回值不关心, 可把rval_ptr设为NULL.

实例:

#include <pthread.h>

#include <stdio.h>

 

/* print process and thread IDs */

void printids(const char *s)

{

   pid_t pid, ppid;

   pthread_t tid;

     pid= getpid();

   ppid = getppid();

   tid = pthread_self();

    printf("%16s pid %5u ppid %5u tid %16u (0x%x) ",

            s, (unsigned int)pid, (unsigned int)ppid,

            (unsigned int)tid, (unsigned int)tid);

}

 /* thread process */

void *thread_func(void *arg);

{

   printids("new thread: ");

   return (void *)108;

}

 /* main func */

int main()

{

   int err;

   void *tret; /* thread return value */

   pthread_t ntid;

   err = pthread_create(&ntid, NULL, thread_func, NULL);

   if (err != 0)

       perror("can't create thread");

 

   err = pthread_join(ntid, &tret);

   if (err != 0)

       perror("can't join thread");

    printids("main thread: ");

   printf("thread exit code: %d ", (int)tret);

   sleep(1);

   return 0;

}

这段代码是通过前一个实例改编的执行流程如下:

  • 首先创建一个新线程, 该线程在打印完IDs后, 返回108.
  • 然后用pthread_join获取该线程返回值, 存于tret中.
  • 主线程打印IDs.
  • 主线程打印tret, 即新线程的返回值.

 

线程取消的定义:

一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。

 

线程取消的语义:

线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。

线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。

 

取消点定义:

根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、  pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程(http://blog.csdn.net/shanzhizi)从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用  pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:

pthread_testcancel();

 

   retcode = read(fd, buffer, length);

 

   pthread_testcancel(); 

 

程序设计方面的考虑:

如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。即如下代码段:

While(1)

{

    ………

    pthread_testcancel();

}

 

与线程取消相关的pthread函数:

intpthread_cancel(pthread_t thread):线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他进程。

发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。注意pthread_cancel并不等待线程终止,它仅仅提出请求。

 int pthread_setcancelstate(int state, int*oldstate):

设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和  PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为 NULL则存入原来的Cancel状态以便恢复。

 

int pthread_setcanceltype(int type, int*oldtype)

设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和  PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入原来的取消动作类型值。

 

void pthread_testcancel(void)

检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。

 http://blog.csdn.net/shanzhizi

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

#include <pthread.h>

void pthread_cleanup_push(void(*rtn)(void*),void *arg);

void pthread_cleanup_pop(int execute);

当线程执行以下动作时调用清理函数,调用参数为arg,清理函数的调用顺序用pthread_cleanup_push来安排。

调用pthread_exit

响应取消请求时

用非0的execute参数调用pthread_cleanup_pop时。

如果线程是通过从它的启动例程中返回而终止的话,那么它的清理处理程序就不会被调用,还要注意清理处理程序是按照与它们安装时相反的顺序被调用的。

 

int pthread_detach(pthread_t tid);

可以用于使线程进入分离状态。

 

  • 4
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

《Java线程(第三版)》,完整版本。作者:Scott Oaks、Henry Wong,翻译:O’Reilly Taiwan 公司,出版社:东南大学出版社,ISBN:756410239X,PDF 格式,扫描版,大小 29MB。本资源带有PDF书签,方便读者朋友阅读。 内容简介: 《Java线程》第三版已经过完整的扩编与修订。本书完整收录了来自java.util.concurrent的并发功能。新的章节涵盖到thread的性能、在Swing中使用thread、thread与Collection class、thread pool以及thread与I/O(传统的、新型的、中断的)。还无法配置J2SE 5.0的开发者可以借由附录中所提供的thread工具于较早的Java版本中达成类似的功能。. 本书涵盖了以下主题: ● Lock starvation与死锁检测 ● Atomic class与极简同步(J2SE 5.0) ● Swing、I/O以及Collection class与Java thread的交互 ● 序化地控制lock与condition variable(J2SE 5.0) ● Thread的性能与安全性 ● Thread pool(J2SE 5.0) ● Thread group ● 特定平台上的thread调度 ● Task scheduler(J2SE 5.0) ● 多处理器环境下的并行循环... 目录: 前言. 第一章Thread导论 Java术语 关于范例 为何要用Thread? 总结 第二章Thread的创建与管理 什么是Thread? 创建Thread Thread的生命周期 两种停止Thread的方式 Runnable工nterface Thread与对象 总结 第三章数据同步 Synchronized关键字 Volatile关键字 更多RaceCOnd“iOn的讨论 明确的 (explicit)Locking Lock Scope 选择Locking机制 Nested Lock 死锁 Lock公平 (Fairness) 总结 第四章Thread NOtification 等待与通知 条件变量 总结 第五章极简同步技巧 能避免同步吗? Atomic变量 Thread局部变量 总结 第六章高级同步议题 同步术语 J2SE 5.0中加入的同步Class 防止死锁 死锁检测 Lock饥饿 总结 第七章Thread与Swing Swing Threading的限制 事件派发Thread的处理 使用invokeLater()与invokeAndWait() 长时间运行的事件回调 总结 第八章Thread与Collection Class Collection Class的概述 同步与Collection Class .. 生产者/肖费者模式 使用CollectionClass 总结 第九章Thread调度 Thread调度的概述 以Thread优先级来调度 常见Threading的实现 总结 第十章Thread Pool 为何要用Thread Pool? Executor 使用Thread Pool Queue与大小 创建Thread Callable Task与Future结果 单一Thread化的访问 总结 第十一章Task的调度 Task调度的概述 java.util.Timer Class javax.Swing.Timer Class SCheduledThreadPoolExecutor Class 总结 第十二章Thread与I/O 传统的I/O服务器 新的I/O服务器 被中断的I/O 总结 第十三章各种Thread议题 Thread Group Thread与Java安全性 Daemon Thread Thread与C1ass的力口载 Thread与异常处理 Thread、Stack、 内存的使用 总结 第十四章Thread性能 性能的概述 同步的Collection Atomic变量与有竞争的同步 Thread的创建与Thread Pool 总结 第十五章 多处理器计算机的并行化循环 对单一Threaded序的并行化 多处理器扩大(scaling) 总结 附录 被撤换的Threading功能 索引...
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值