Linux系统编程-线程

目录

线程的概念

线程的创建和终止

创建线程

线程终止

1.pthread_exit

2. pthread_join

 3.pthread_cleanup_push&pthread_cleanup_pop

线程的取消

1.pthread_cancel

2. pthread_setcancelstate

3. pthread_setcanceltype

4.pthread_testcancel

5. pthread_detach

线程同步

1.pthread_mutex_init

2. pthread_mutex_destroy

3. pthread_mutex_lock

4. pthread_mutex_trylock

5. pthread_mutex_unlock

6. pthread_once

条件变量(pthread_cond_t类型)

1.pthread_cond_init

2. pthread_cond_destroy

3.pthread_cond_wait

4. pthread_cond_broadcast

5. pthread_cond_signal

6. pthread_cond_timedwait

线程属性

互斥量属性

条件变量属性

重入与设置屏蔽

多线程实现令牌桶

main.c

mytbf.c

mytbf.h

多线程实现读写管道

pipe.c

pipe.h


线程的概念

        会话是用来承载进程组的,里面可以有一个或多个进程,一个线程中可以有一个或多个线程
        线程的本质就是一个正在运行的函数 ,线程没有主次之分(main函数 也只是一个main线程),多个线程之间共享内存,线程的调度取决于调度器的测略
        posix线程是一套标准,而不是实现,我们主要讨论这套标准,线程标识 pthead_t 类型不确定


线程的创建和终止

创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    成功时返回 0。失败时返回错误码。


    pthread_t *thread: 指向 pthread_t 类型的指针,用于存储新创建的线程的标识符。


    const pthread_attr_t *attr: 指向线程属性对象的指针,可以指定线程的属性,如栈大小、调度策略等。如果不需要指定特定的属性,可以传递 NULL。


    void *(*start_routine) (void *): 线程函数的入口点,即线程执行的函数。这个函数接受一个 void* 类型的参数,并返回一个 void* 类型的值。


    void *arg: 传递给线程函数的参数。

线程终止

        线程从启动例程返回,返回值就是线程的退出码,线程可以被同一进程的其他线程取消,线程调用`pthread_exit()`函数

1.pthread_exit

void pthread_exit(void *retval);

结束自己的执行并将返回值retval传递给等待该线程结束的线程

2. pthread_join

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

        调用此函数的线程将阻塞,直到指定的线程终止。一旦被等待的线程终止,如果需要获取线程的返回值,则传入一个非 NULL 的指针。*retval指针将被设置为指向线程的返回值。

 3.pthread_cleanup_push&pthread_cleanup_pop

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute); //0不执行1执行

        类似钩子函数,程序只要正常终止,钩子函数就会被逆序调用,push设置钩子函数和参数,pop决定是否执行。因为这两个函数其实是宏,有push必须有pop


线程的取消

多线程任务 有时需要取消部分任务(线程)
取消有2种状态:
            - 不允许
            - 允许
                - 异步cancel
                - 推迟cancel(默认) 推迟到cancel点再响应
                            - cancel点 : POSIX定义的cancel点,都是可能引发阻塞的系统调用

1.pthread_cancel

int pthread_cancel(pthread_t thread);

        请求取消指定的线程,成功时返回 0。失败时返回错误码。

2. pthread_setcancelstate

int pthread_setcancelstate(int state, int *oldstate);

        用于控制线程的取消状态,PTHREAD_CANCEL_ENABLE: 允许取消请求。PTHREAD_CANCEL_DISABLE: 禁止取消请求。成功时返回 0。失败时返回错误码。

3. pthread_setcanceltype

int pthread_setcanceltype(int type, int *oldtype);

        用于控制线程的取消状态和类型,PTHREAD_CANCEL_DEFERRED: 取消请求被推迟,直到线程退出不能被取消的系统调用。PTHREAD_CANCEL_ASYNCHRONOUS: 取消请求可以立即被处理。

int pthread_detach(pthread_t thread);

4.pthread_testcancel

void pthread_testcancel(void);

        什么都不做,就是原地设置一个cancel点

5. pthread_detach

int pthread_detach(pthread_t thread);

        将一个线程设置为分离状态(detached state)。在分离状态下,线程结束时不会等待其他线程来回收其资源,系统会自动回收这些资源。


线程同步

        实现线程同步的互斥锁,pthread_mutex_t类型的互斥量锁住的是一段代码而不是一个变量。

1.pthread_mutex_init

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

        初始化一个互斥锁,const pthread_mutexattr_t *attr: 指向互斥锁属性的指针。如果不需要特殊属性,可以传递 NULL。成功时返回 0。失败时返回错误码。

2. pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);

        销毁一个互斥锁。

3. pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

        堵塞锁定一个互斥锁,成功时返回 0。失败时返回错误码。

4. pthread_mutex_trylock

int pthread_mutex_trylock(pthread_mutex_t *mutex);

        尝试锁定一个互斥锁,但不阻塞。如果互斥锁成功被锁定,返回 0。如果互斥锁已经被其他线程锁定,返回 EBUSY。其他错误时返回相应的错误码。

5. pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t *mutex);

        解锁一个互斥锁。只有当前拥有互斥锁的线程才能调用此函数解锁。如果调用线程不是互斥锁的所有者,则会返回 EPERM 错误。

6. pthread_once

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

        通常用于全局变量的初始化,确保在多线程环境中,全局变量的初始化不会被多次执行


        pthread_once_t *once_control: 指向 pthread_once_t 类型的变量,该变量用于控制初始化函数的执行。这个变量必须在调用 pthread_once() 之前被初始化为 PTHREAD_ONCE_INIT。


        void (*init_routine)(void): 指向初始化函数的指针。这个函数将在第一次调用 pthread_once() 时执行,并且只会执行一次。

条件变量(pthread_cond_t类型)

1.pthread_cond_init

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

        初始化一个条件变量,pthread_cond_t *cond: 指向条件变量对象的指针。const pthread_condattr_t *attr: 指向条件变量属性的指针。如果不需要特殊属性,可以传递 NULL。

2. pthread_cond_destroy

int pthread_cond_destroy(pthread_cond_t *cond);

        销毁一个条件变量。

3.pthread_cond_wait

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

        函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。

4. pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

        唤醒所有等待指定条件变量的线程。

5. pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

        唤醒一个等待指定条件变量的线程。

6. pthread_cond_timedwait

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

         允许指定一个超时时间,这样线程就不会无限期地等待条件变量。如果条件变量在指定的时间内被触发,线程会像 pthread_cond_wait 一样继续执行;如果超时时间到达,线程会恢复执行.


线程属性

int pthread_attr_init(pthread_attr_t *attr);
//初始化线程的属性对象,设置其为默认值。

int pthread_attr_destroy(pthread_attr_t *attr);
//销毁线程属性对象,释放与其关联的资源。

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, const cpu_set_t *cpuset); // 设置线程的CPU亲和性,指定线程可以在哪些CPU上运行。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); // 设置线程的分离状态,决定线程是否应该被创建为分离态。

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize); // 设置线程栈的保护区域大小,用于防止栈溢出。

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); // 设置线程的调度继承属性,决定新线程是否继承创建它的线程的调度属性。

int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); // 设置线程的调度参数,如优先级等。

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); // 设置线程的调度策略,如轮转调度等。

int pthread_attr_setscope(pthread_attr_t *attr, int scope); // 设置线程的并发范围,如系统范围或进程范围。

int pthread_attr_setsigmask_np(pthread_attr_t *attr, const sigset_t *sigmask); // 设置线程的信号掩码,决定线程忽略哪些信号。

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); // 设置线程栈的属性,如栈的大小和位置。

int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); // 设置线程栈的起始地址。

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); // 设置线程栈的大小。

int pthread_getattr_np(pthread_t thread, pthread_attr_t *attr); // 获取线程的属性。

int pthread_setattr_default_np(pthread_attr_t *attr); // 设置默认的线程属性。

互斥量属性

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
//同上

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared); // 获取互斥锁属性,确定互斥锁是否可以被不同进程中的线程共享。

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); // 设置互斥锁属性,指定互斥锁是否可以被不同进程中的线程共享。

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared); // 获取互斥锁属性,此属性决定互斥锁是否可以跨进程共享。

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); // 设置互斥锁属性,允许指定互斥锁是否可以跨进程共享。

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); // 获取互斥锁属性中的类型字段,该字段定义了互斥锁的类型。

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); // 设置互斥锁的类型,如正常互斥锁、错误检测互斥锁、递归互斥锁等。

条件变量属性

int pthread_condattr_destroy(pthread_condattr_t *attr); // 销毁条件变量属性对象,释放与其关联的资源。

int pthread_condattr_init(pthread_condattr_t *attr); // 初始化条件变量属性对象,设置其为默认值。

重入与设置屏蔽

多线程中的IO,IO函数支持多线程因为会加锁解锁缓冲区
- getchar_unlocked

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

该函数用于设置调用线程的信号屏蔽字(mask),并可选地获取之前的屏蔽字。


- int how: 指定如何设置信号屏蔽字,可以是以下几种操作之一:
  - SIG_BLOCK: 将当前屏蔽字与参数 set 指定的信号集进行按位或操作,添加信号到屏蔽字中。
  - SIG_UNBLOCK: 将当前屏蔽字与参数 set 指定的信号集的补集进行按位与操作,从屏蔽字中移除信号。
  - SIG_SETMASK: 直接用参数 set 指定的信号集替换当前的信号屏蔽字。


- const sigset_t *set: 指向新的信号集的指针,该信号集将根据 how 参数的值影响当前的信号屏蔽字。


- sigset_t *oldset: 指向一个信号集的指针,用于存储调用前线程的信号屏蔽字。如果不需要这个信息,可以传递 NULL。

int pthread_kill(pthread_t thread, int sig);

        该函数用于向指定的线程发送信号。函数是线程安全的信号发送方法,它允许在多线程程序中向特定线程发送信号,而不是向整个进程发送信号。这与全局的 `kill` 函数不同,后者向进程发送信号,而不管信号在哪个线程中被捕捉或处理。


多线程实现令牌桶

main.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <math.h>

#include "mytbf.h"

static const int SIZE = 1024;
static const int CPS = 10;
static const int BURST = 100;//最大令牌数

static volatile int token = 0;//持有令牌数

int main(int argc,char** argv)
{
  if (argc < 2){
    fprintf(stdout,"Usage...");
    exit(1);
  }

  mytbf_t *tbf;

  tbf = mytbf_init(CPS,BURST);
  if (tbf == NULL){
    fprintf(stderr,"tbf init error");
    exit(1);
  }

  //打开文件
  int sfd,dfd = 0;
  do{
    sfd = open(argv[1],O_RDONLY);
    if (sfd < 0){
      if (errno == EINTR)
        continue;
      fprintf(stderr,"%s\n",strerror(errno));
      exit(1);
    }
  }while(sfd < 0);

  char buf[SIZE];
    
  while(1){
        
    int len,ret,pos = 0;
    int size = mytbf_fetchtoken(tbf,SIZE);
        
    //int i = 0;
    //while(i < 2){
    //    sleep(1);
    //    i++;
    //}

    if (size < 0){
      fprintf(stderr,"mytbf_fetchtoken()%s\n",strerror(-size));
      exit(1);
    }

    len = read(sfd,buf,size);
    while (len < 0){
      if (errno == EINTR)
        continue;
      strerror(errno);
      break;
    }

    //读取结束
    if (len == 0){
      break;
    }

    //要是读到结尾没用完token
    if (size - len > 0){
      mytbf_returntoken(tbf,size-len);
    }

    //以防写入不足
    while(len > 0){
      ret = write(dfd,buf+pos,len);
      while (ret < 0){
        if (errno == EINTR){
          continue;
        }
        printf("%s\n",strerror(errno));
        exit(1);
      }

      pos += ret;
      len -= ret;
    }
  }

  close(sfd);
  mytbf_destroy(tbf);

  exit(0);
}

mytbf.c

#include <errno.h>
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <pthread.h>
#include <string.h>

#include "mytbf.h"

struct mytbf_st{
  int csp;
  int burst;
  int token;
  int pos;//任务列表的下标
  pthread_mutex_t mut;
  pthread_cond_t cond;
};

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

static pthread_t ptid;
static pthread_once_t pth_once = PTHREAD_ONCE_INIT;

static struct mytbf_st *job[MYTBF_MAX];
static volatile int inited = 0;

static int get_free_pos_unlocked(){
  for (int i = 0;i < MYTBF_MAX;i++){
    if (job[i] == NULL)
      return  i;
  }
  return -1;
}

//线程处理函数
static void *handler(void *p){
  struct timespec ts;
  ts.tv_sec = 1;
  ts.tv_nsec = 0;

  while(1){
    pthread_mutex_lock(&mutex);
    for (int i = 0;i < MYTBF_MAX;i++){
      if (job[i] != NULL){
        pthread_mutex_lock(&job[i]->mut);
        job[i]->token += job[i]->csp;
        if (job[i]->token > job[i]->burst){
          job[i]->token = job[i]->burst;
        }
        pthread_cond_broadcast(&job[i]->cond);
        pthread_mutex_unlock(&job[i]->mut);
      }
    }
    pthread_mutex_unlock(&mutex);
    nanosleep(&ts,NULL);

  }
  pthread_exit(NULL);
}

//卸载线程处理模块
static void mod_unload(){
  pthread_cancel(ptid);
  pthread_join(ptid,NULL);
  for (int i = 0;i < MYTBF_MAX;i++){
    if (job[i] != NULL){
      mytbf_destroy(job[i]);
    }
    free(job[i]);
  }

  pthread_mutex_destroy(&mutex);
}

//装载线程处理模块
static void mod_load(){

  int err = pthread_create(&ptid,NULL,handler,NULL);
  if (err){
    fprintf(stderr,"%s\n",strerror(err));
  }

  atexit(mod_unload);
}

mytbf_t *mytbf_init(int cps,int burst){
  struct mytbf_st *tbf;

  pthread_once(&pth_once,mod_load);

  tbf = malloc(sizeof(*tbf));
  if (tbf == NULL){
    return NULL;
  }
  tbf->token = 0;
  tbf->csp = cps;
  tbf->burst = burst;
  pthread_mutex_init(&tbf->mut,NULL);
  pthread_cond_init(&tbf->cond,NULL);

  pthread_mutex_lock(&mutex);
  //将新的tbf装载到任务组中
  int pos = get_free_pos_unlocked();
  if (pos == -1){
    free(tbf);
    pthread_mutex_unlock(&mutex);
    return NULL;
  }

  tbf->pos = pos;
  job[pos] = tbf;
    
  pthread_mutex_unlock(&mutex);

  return tbf;
}

//获取token ptr是一个 void * size是用户想要获取的token数
int mytbf_fetchtoken(mytbf_t *ptr,int size){
  struct mytbf_st *tbf = ptr;

  if (size <= 0){
    return -EINVAL;
  }
    
  //有token继续
  pthread_mutex_lock(&tbf->mut);
  while (tbf->token <= 0){
    pthread_cond_wait(&tbf->cond,&tbf->mut);//等通知 抢锁
  }

  int n =tbf->token<size?tbf->token:size;
  tbf->token -= n;

  pthread_mutex_unlock(&tbf->mut);
  //用户获取了 n 个token
  return n;
}

//归还token ptr是一个 void *
int mytbf_returntoken(mytbf_t *ptr,int size){
  struct mytbf_st *tbf = ptr;

  if (size <= 0){
    return -EINVAL;
  }
  pthread_mutex_lock(&tbf->mut);
  tbf->token += size;
  if (tbf->token > tbf->burst)
    tbf->token = tbf->burst;
  pthread_cond_broadcast(&tbf->cond);
  pthread_mutex_unlock(&tbf->mut);

  return size;
}

int mytbf_destroy(mytbf_t *ptr){
  struct mytbf_st *tbf = ptr;
  pthread_mutex_lock(&mutex);
  job[tbf->pos] = NULL;
  pthread_mutex_unlock(&mutex);

  pthread_mutex_destroy(&tbf->mut);
  pthread_cond_destroy(&tbf->cond);

  free(tbf);
  return 0;
}

mytbf.h

#ifndef MYTBF_H__
#define MYTBF_H__

#define MYTBF_MAX 1024

typedef void mytbf_t;

mytbf_t *mytbf_init(int cps,int burst);

//获取token
int mytbf_fetchtoken(mytbf_t *,int);
//归还token
int mytbf_returntoken(mytbf_t *,int);

int mytbf_destroy(mytbf_t *);

#endif

多线程实现读写管道

pipe.c

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

#include "mypipe.h"

struct mypipe_st{
    int head;
    int tail;
    char data[PIPESIZE];
    int datasize;
    int count_reader;
    int count_writer;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
};

mypipe_t *mypipe_init(){
    struct mypipe_st *pipe;
    pipe = malloc(sizeof(*pipe));
    if (pipe == NULL){
        return NULL;
    }

    //pipe结构初始化
    pipe->head = 0;
    pipe->tail = 0;
    pipe->datasize = 0;
    pthread_mutex_init(&pipe->mutex,NULL);
    pthread_cond_init(&pipe->cond,NULL);

    return pipe;
}

int mypipe_register(mypipe_t *ptr,int opmap){
    struct mypipe_st *pipe = ptr;
    
    pthread_mutex_lock(&pipe->mutex);
    if(opmap & PIPE_READER){
        pipe->count_reader++;
    }
    if(opmap & PIPE_WRITER){
        pipe->count_writer++;
    }

    //读写双方不全
    while(pipe->count_reader <= 0 || pipe->count_writer <= 0){
        pthread_cond_wait(&pipe->cond,&pipe->mutex);
    }

    pthread_cond_broadcast(&pipe->cond);//读写双方凑齐
    pthread_mutex_unlock(&pipe->mutex);
    return 0;

}

int mypipe_unregister(mypipe_t *ptr,int opmap){

    struct mypipe_st *pipe = ptr;
    
    pthread_mutex_lock(&pipe->mutex);
    if(opmap & PIPE_READER){
        pipe->count_reader--;
    }
    if(opmap & PIPE_WRITER){
        pipe->count_writer--;
    }
    //唤醒其他管道读写方检查读写者的数量
    pthread_cond_broadcast(&pipe->cond);

    pthread_mutex_unlock(&pipe->mutex);
    return 0;
}

static int mypipe_readbyte_unlocked(struct mypipe_st *pipe,char *data){
    //管道无数据
    if (pipe->datasize <= 0){
        return -1;
    }

    //管道有数据 读取一个现在管道的读端数据,用data保存
    *data = pipe->data[pipe->head];

    pipe->head = (pipe->head++)%PIPESIZE;
    pipe->datasize--;
    return 0;
}

static int mypipe_writebyte_unlocked(struct mypipe_st *pipe,const char *data){
    //管道数据满
    if (pipe->datasize >= PIPESIZE){
        return -1;
    }

    //管道有数据 读取一个现在管道的读端数据,用data保存
    pipe->data[pipe->tail+1] = *data;

    pipe->tail = (pipe->tail++)%PIPESIZE;
    pipe->datasize++;
    return 0;
}

int mypipe_read(mypipe_t *ptr,void *buf,size_t size){
    struct mypipe_st *pipe = ptr;
    
    pthread_mutex_lock(&pipe->mutex);
    
    while(pipe->datasize <= 0 && pipe->count_writer > 0){
        pthread_cond_wait(&pipe->cond,&pipe->mutex);
    }

    //管道空且没有写者
    if (pipe->datasize <= 0 && pipe->count_writer <= 0){
        pthread_mutex_unlock(&pipe->mutex);
        return 0;
    }

    //管道中有数据了
    for (int i = 0;i < size;i++){
        if (mypipe_readbyte_unlocked(pipe,buf+i) < 0){
            break;
        }
    }
    pthread_mutex_unlock(&pipe->mutex);

    return 0;
}

int mypipe_write(mypipe_t *ptr,const void *buf,size_t size){
    struct mypipe_st *pipe = ptr;
    
    pthread_mutex_lock(&pipe->mutex);
    
    while(pipe->datasize >= PIPESIZE && pipe->count_reader > 0){
        pthread_cond_wait(&pipe->cond,&pipe->mutex);
    }

    //管道空且没有读者
    if (pipe->datasize <= 0 && pipe->count_reader <= 0){
        pthread_mutex_unlock(&pipe->mutex);
        return 0;
    }

    //管道中有空间了
    for (int i = 0;i < size;i++){
        if (mypipe_writebyte_unlocked(pipe,buf+i) < 0){
            break;
        }
    }
    pthread_mutex_unlock(&pipe->mutex);

    return 0;

}

int mypipe_destory(mypipe_t *ptr){
    struct mypipe_st *pipe = ptr;
    pthread_mutex_destroy(&pipe->mutex);
    pthread_cond_destroy(&pipe->cond);
    free(pipe);

    return 0;
}

pipe.h

#ifndef MYPIPE_H__
#define MYPIPE_H__

#include <stdio.h>

#define PIPESIZE 1024
#define PIPE_READER 0x00000001UL //读者
#define PIPE_WRITER 0x00000002UL //写者


typedef void mypipe_t;

mypipe_t *mypipe_init();

//读者 写者 注册身份
int mypipe_register(mypipe_t *,int opmap);;
//读者 写者 注销身份
int mypipe_unregister(mypipe_t *,int opmap);

int mypipe_read(mypipe_t *,void *buf,size_t size);

int mypipe_write(mypipe_t *,const void *buf,size_t size);

int mypipe_destory(mypipe_t *);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值