网络编程入门05

这几天因为工作需求,学习了多线程编程,用线程池写了一个小工具,对于网络库的回调,多线程理解加深了一层理解。发觉,利用理解多线程,理解这些小的组件,也非常重要。被工作打断,直接阅读net模块有点难度,打算从base模块阅读起。
class noncopyable{} //把赋值构造和赋值操作直接删除,默认构造和析构使用默认操作。
class copyable{} //把构造和析构设置默认,其余不变,编译器设置

//编译器本身提供的原子操作
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)


type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)
//这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值。

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true.
第二个函数在返回操作之前的值。

type __sync_lock_test_and_set (type *ptr, type value, ...)
   将*ptr设为value并返回*ptr操作之前的值。

void __sync_lock_release (type *ptr, ...)
     将*ptr置0
AtomicIntegerT 原子类的实现利用了gcc提供原子操作,在c++11中有了std::atomic<int> 类
template <typename T>
class AtomicIntegerT:noncopyable{
public:
    T get() {
        return __sync_val_compare_and_swap(&value_, 0, 0);
    }
    T getAndAdd(T x) {
       return __sync_fetch_and_add(&value_, x);
    }
    T addAndGet(T x) {
       return getAndAdd(x) + x;
    }
    T incrementAndGet() {
       return addAndGet(1);
    }
    T decrementAndGet() {
       return addAndGet(-1);
    }
     void add(T x) {
       getAndAdd(x);
     }
     void increment() {
       getAndAdd(1);
     }
     void decrement() {
             getAndAdd(-1);
     }
     T getAndSet(T x) {
          return __sync_lock_test_and_set(&value_, x);
     }
private:
   volatile T value_;  初始化为0
}
typedef detail::AtomicIntegerT<int32_t> AtomicInt32;
typedef detail::AtomicIntegerT<int64_t> AtomicInt64;

关于Type.h文件 主要是对c++基本转换做了一个封装

void memZero(void* p, size_t n), 从p开始的n个字节清空操作

template<typename To, typename From>
inline To implicit_cast(From const &f)   一个模板方法,从from到to类型的隐式转换
{
  return f;
}

template<typename To, typename From>     // use like this: down_cast<T*>(foo);    从基类指针向子类指针转换
inline To down_cast(From* f)                     // so we only accept pointers
{
  // Ensures that To is a sub-type of From *.  This test is here only
  // for compile-time type checking, and has no overhead in an
  // optimized build at run-time, as it will be optimized away
  // completely.
  if (false)
  {
    implicit_cast<From*, To>(0);
  }

#if !defined(NDEBUG) && !defined(GOOGLE_PROTOBUF_NO_RTTI)
  assert(f == NULL || dynamic_cast<To>(f) != NULL);  // RTTI: debug mode only!
#endif
  return static_cast<To>(f);
}

CurrentThread.h 属于一个命名空间

//__thread修饰的全局变量每个线程各有一份实体,互不影响。初始化值只能是编译期常量。
extern __thread int t_cachedTid;
extern __thread char t_tidString[32];
extern __thread int t_tidStringLength;
extern __thread const char* t_threadName;

__builtin_expect

这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)。
意思是:EXP==N的概率很大。

一般的使用方法是将__builtin_expect指令封装为likely和unlikely宏。这两个宏的写法如下.

#define likely(x) __builtin_expect(!!(x), 1) //x很可能为真       
#define unlikely(x) __builtin_expect(!!(x), 0) //x很可能为假

//内核中的 likely() 与 unlikely()
if(likely(value))  //等价于 if(value)
if(unlikely(value))  //也等价于 if(value)

void cacheTid();

int tid(){
  if (__builtin_expect(t_cachedTid == 0, 0)) 
    {   
      cacheTid();  //这个函数在Thread.cc中进行实现
    }   
    return t_cachedTid;
}

在Thread.cc
void CurrentThread::cacheTid()
{
  if (t_cachedTid == 0)
  {
    t_cachedTid = detail::gettid();
    t_tidStringLength = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);
  }
}
在Thread.cc 的detail命名空间下

//syscall(SYS_gettid))
>NAME       
>syscall - 间接系统调用
// syscall() 执行一个系统调用,根据指定的参数number和所有系统调用的汇编语言接口来确定调用哪个系统调用。
// 系统调用所使用的符号常量可以在头文件里面找到。

pid_t gettid()
{
  return static_cast<pid_t>(::syscall(SYS_gettid));
}

const char* tidString() {return t_tidString;}

int tidStringLength() { return t_tidStringLength; }

const char* name() { return t_threadName;}

bool isMainThread(); //在thread.cc中实现
{
   return tid() == ::getpid();
}

struct timespec;
typedef long time_t;

#ifndef _TIMESPEC
#define _TIMESPEC

struct timespec {
time_t tv_sec; // seconds 
long tv_nsec; // and nanoseconds 
};

nanosleep( )---------以纳秒为单位
#include<time.h>
 struct timespec
{
  time_t  tv_sec;         /* 秒seconds */
  long    tv_nsec;        /* 纳秒nanoseconds */
};

int nanosleep(const struct timespec *req, struct timespec *rem);

//return: 若进程暂停到参数*req所指定的时间,成功则返回0,若有信号中断则返回-1,并且将剩余微秒数记录在*rem中。
//req->tv_sec是以秒为单位,而tv_nsec以毫微秒为单位(10的-9次方秒)。
//由于调用nanosleep是是进程进入TASK_INTERRUPTIBLE,这种状态是会相应信号而进入TASK_RUNNING状态的。


void sleepUsec(int64_t usec); //在thread.cc实现
{
   struct timespec ts = { 0, 0 };
   ts.tv_sec = static_cast<time_t>(usec / Timestamp::kMicroSecondsPerSecond);
   ts.tv_nsec = static_cast<long>(usec % Timestamp::kMicroSecondsPerSecond * 1000);
   ::nanosleep(&ts, NULL);
}

#include <execinfo.h>
 
/* Store up to SIZE return address of the current program state in
   ARRAY and return the exact number of values stored.  */
int backtrace(void **array, int size);
 
/* Return names of functions from the backtrace list in ARRAY in a newly
   malloc()ed memory block.  */
char **backtrace_symbols(void *const *array, int size);
 
/* This function is similar to backtrace_symbols() but it writes the result
   immediately to a file.  */
void backtrace_symbols_fd(void *const *array, int size, int fd);

string stackTrace(bool demangle);  //打印信息栈操作,普通程序员不用。在CurrentThread.cc中实现

Mutex.h 是对mutex的封装

GNU C 的一大特色就是__attribute__ 机制。attribute 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。
attribute 书写特征是:attribute 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。
attribute 语法格式为:attribute ((attribute-list))

class MutexLock : noncopyable{
private:
  friend class Condition;//右元类
  pthread_mutex_t mutex_;
  pid_t holder_;
       class UnassignGuard : noncopyable{  //属于MutexLock的一个内部类,含有一个MutexLock的引用
          public:
               ctr(MutexLock& owner) : owner_(owner) {
                     owner_.unassignHolder();
                }
                ~dtr() {
                       owner_.assignHolder();
                 }
          private:
             MutexLock& owner_;
        }
}

class MutexLockGuard : noncopyable{
private:
  MutexLock& mutex_;
public:
   MutexLockGuard(MutexLock& mutex) : mutex_(mutex) {
      mutex_.lock();
   }
   ~MutexLockGuard() {
      mutex_.unlock();
  }
}

Condition.h 对应于C++11的条件变量封装,基本上使用pthread库元素mutex和pthread_cond_t原生变量实现c++11的条件变量

/* 初始化一个条件变量 */
int pthread_cond_init (pthread_cond_t* cond, pthread_condattr_t *cond_attr);

/* 销毁一个条件变量 */
int pthread_cond_destroy(pthread_cond_t* cond);

/* 令一个消费者等待在条件变量上 */
int pthread_cond_wait(pthread_cond_t* cond);

/* 生产者通知等待在条件变量上的消费者 */
int pthread_cond_signal(pthread_cond_t* cond);

/* 生产者向消费者广播消息 */
int pthread_cond_broadcast(pthread_cond_t* cond);

class Condition : noncopyable {
private:
   MutexLock& mutex_;
   pthread_cond_t   pcond_;
public:
   Condition(MutexLock& mutex) : mutex_(mutex) {
     pthread_cond_init(&pcond_, NULL);  
   }
   ~Condition() {
      pthread_cond_destory(&pcond_);
   }
   void wait() {
      MutexLock::UnassignGuard ug(mutex_);   //利用栈上对象,实现对锁的释放,在结束,对锁资源回收
      pthread_cond_wait(&pcond_, mutex_.getPthreadMutex());
   }
   void notify() {
      pthread_cond_signal(&pcond_); //通知一个等待此条件变量的线程
   }
   bool waitForSeconds(double seconds);// 等待一定时间

   void notifyAll() {
      pthread_cond_broadcast(&pcond_); //通知所有线程 
   }
}

CountDownLatch.h 一个倒计时的condition_variable; 当count数字减为0,一次通知,所有倒数钱的wait都会进入等待队列

class CountDownLatch : noncopyable {

private:
   mutable MutexLock mutex_;
   Condition condition_;
   int count_;
public:
   CountDownLatch(int count):mutex_(), condition_(mutex_), count_(count) {
   }
   void wait() {
     MutexLockGuard lock(mutex_);
         while (count_ > 0) {
             condition_.wait();
         }
    }
    void countDown() {
        MutexLockGuard lock(mutex_);
        --count_;
        if (count_ == 0) {
          condition_.notifyAll();
         }
     }
     int getCount() const {
        MutexLockGuard lock(mutex_);
        return count_;
        }
}

Thread.h里有些关于线程创建,运行的封装

函数简介

 pthread_create是UNIX环境创建线程函数

头文件

  #include<pthread.h>

函数声明

  int pthread_create(pthread_t restrict tidp,const pthread_attr_t restrict_attr,voidstart_rtn)(void),void restrict arg);

返回值

  若成功则返回0,否则返回出错编号

参数

  第一个参数为指向线程标识符的指针。

  第二个参数用来设置线程属性。

  第三个参数是线程运行函数的地址。

  最后一个参数是运行函数的参数。

void* startThread(void* obj) {
  ThreadData* data = static_cast<ThreadData*> (obj);
  data->runInThread();
  delete data;
  return NULL;
}

class Thread : noncopyable {
private:
    void setDefaultName() {
          int num = numCreated_.incrementAndGet();
          if (name_.empty())
          {
            char buf[32];
            snprintf(buf, sizeof buf, "Thread%d", num);
            name_ = buf;
          }

     }
    bool started_;
    bool joined_;
    pthread_t pthreadId_;
     pid_t tid_;
     ThreadFunc func_;
     string name;
     CountDownLatch latch_;
     static AtomicInt32 numCreated_;
public:
       typedef std::function<void()> ThreadFun;  //对一个没有返回值的可调用对象定义为线程函数,主要为线程需要执行的任务
       Thread(ThreadFunc, const string&name = string())   //构造函数,传入一个线程函数,和名字
              :started_(false),
            joined_(false),
            pthreadId_(0),
            tid_(0),
            func_(std::move(func)),
            name_(n),
            latch_(1) {
              setDefaultName();
            }
    
       ~Thread{
          if (started_ && !joined_) {
            pthread_detach(pthreadId);
          }
       }
       void start() {
         assert(!started_);
         started_ = true;
         ThreadData* data = new ThreadData(func_, name_, &tid, &latch_);
         if (pthread_create(&pthreadId_, NULL, &startThread, data)) {  //返回非0值,认为失败
            started_ = false;
            delete data;
         }
         else {
             latch_.wait();  //主线程等待条件变量
            assert(tid_ > 0);
         }
        }
      int join() {
         assert(started_);
         assert(!joined_);
         joined_ = true;
         return pthread_join(pthreadId_, NULL);
      }
      bool started() {
      return started_;
     }
     pid_t tid() {
      return tid_; 
     }
      const string& name const {
          return name_;
      }
     static int numCreated() {
           return numCreated_.get();
     }

}


struct ThreadData{  //data里拿的几乎都是指针
 typedef muduo::Thread::ThreadFunc ThreadFunc; //简化命名
  ThreadFunc func_;
  string name_;
  pid_t* tid_;
  CountDownLatch* latch_;
  ThreadData(ThreadFunc func,
             const string& name,
             pid_t* tid,
             CountDownLatch* latch)
    : func_(std::move(func)),
      name_(name),
      tid_(tid),
      latch_(latch)
  { }
  void runInThread()   //基本上就是把ThreadFunc 跑起来,加上了捕获异常安全检查
  {
    *tid_ = muduo::CurrentThread::tid();
    tid_ = NULL;
    latch_->countDown();
    latch_ = NULL;

    muduo::CurrentThread::t_threadName = name_.empty() ? "muduoThread" : name_.c_str();
    ::prctl(PR_SET_NAME, muduo::CurrentThread::t_threadName);  //这是一个系统调用,几乎用不到的东西
   try
    {
      func_();
      muduo::CurrentThread::t_threadName = "finished";
    }
   catch (const Exception& ex)
    {
      muduo::CurrentThread::t_threadName = "crashed";
      fprintf(stderr, "exception caught in Thread %s\n", name_.c_str());
      fprintf(stderr, "reason: %s\n", ex.what());
      fprintf(stderr, "stack trace: %s\n", ex.stackTrace());
      abort();
    }
    catch (const std::exception& ex)
    {
      muduo::CurrentThread::t_threadName = "crashed";
      fprintf(stderr, "exception caught in Thread %s\n", name_.c_str());
      fprintf(stderr, "reason: %s\n", ex.what());
      abort();
    }
    catch (...)
    {
      muduo::CurrentThread::t_threadName = "crashed";
      fprintf(stderr, "unknown exception caught in Thread %s\n", name_.c_str());
      throw; // rethrow
    }
}

void afterFork()
{
  muduo::CurrentThread::t_cachedTid = 0;
  muduo::CurrentThread::t_threadName = "main";
  CurrentThread::tid();
  // no need to call pthread_atfork(NULL, NULL, &afterFork);
}

class ThreadNameInitializer
{
 public:
  ThreadNameInitializer()
  {
    muduo::CurrentThread::t_threadName = "main";
    CurrentThread::tid();
    pthread_atfork(NULL, NULL, &afterFork);
  }
};
ThreadNameInitializer init;

ThreadLocal.h 文件对线程存储的一个包装

线程中特有的线程存储, Thread Specific Data 。线程存储有什么用了?他是什么意思了?大家都知道,在多线程程序中,所有线程共享程序中的变量。现在有一全局变量,所有线>程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它,那么就需要使用线程存储了。表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程>中又是单独存储的。这就是线程存储的意义
创建一个类型为pthread_key_t类型的变量。
调用pthread_key_create()来创建该变量。该函数有两个参数,第一个参数就是上面声明的pthread_key_t变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调>用。该函数指针可以设成 NULL,这样系统将调用默认的清理函数。该函数成功返回0.其他任何返回值都表示出现了错误。
当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。该函数有两个参数,第一个为前面声明的pthread_key_t变量,第二个为void变量,这样你可以存储任何类型的>?>值。
如果需要取出所存储的值,调用pthread_getspecific()。该函数的参数为前面提到的pthread_key_t变量,该函数返回void
类型的值。下面是前面提到的函数的原型:

int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
template<typename T>
class ThreadLoacl : noncopable {
private:
   pthread_key_t pkey_;
   static void destrutor(void* x) {
       T* obj = static_cast<T*> (x);
       typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
       T_must_be_complete_type dummy; (void) dummy;
        delete obj;
    ThreadLocal() {
         pthread_key_create(&pkey_,  &ThreadLoacl::destructor);
     }
     ~ThreadLocal() {
         pthread_key_delete(pkey_);
       }
       T& value() {
         T* perThreadValue = static_cast<T*> (pthread_getspecific(pkey_));
         if (!perThreadValue) {
            T* newObj = new T(); 
             pthread_setspecific(pkey_, newObj);
             perThreadValue = newobj;
         }
         return *perThreadValue;
       }
}

Singleton.h 对单例模式的实现 模板里套模板。。。需要对模板编程有点了解

detail::
  template<typename T>
  struct has_no_destory{
      template<typename C> 
       static char test(decltype(&C::no_destory));
       template <typename C> static int32_t test(...);
       const static bool value = sizeof(test<T>(0)) == 1;
  }


muduo::

template<typename T>
class Singleton : noncopyable {
private:
   static pthread_once_t ponce_;
   static T*                       value_;
public:
       Singleton() = delete;
       ~Singleton() = delete;
      static T& instance() {
          pthread_once(&ponce_, &Singleton::init);
          assert(value_ != NULL);
          return *value_;
      }
private:
      static void init()
      {
        value_ = new T();
        if (!detail::has_no_destroy<T>::value)
        {
              ::atexit(destroy);
        }
      }
 
       static void destroy()
      {
        typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
        T_must_be_complete_type dummy; (void) dummy;

        delete value_;
        value_ = NULL;
      }
}

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;

ThreadLocalSingleton.h ThreadLocal单例模式

template<typename T>
class ThreadLocalSingleton : noncopyable
{
 public:
  ThreadLocalSingleton() = delete;
  ~ThreadLocalSingleton() = delete;

  static T& instance()
  {
    if (!t_value_)
    {
      t_value_ = new T();
      deleter_.set(t_value_);
    }
    return *t_value_;
  }
  static T* pointer()
  {
    return t_value_;
  }
private:
  static void destructor(void* obj)
  {
    assert(obj == t_value_);
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];  ///来将运行时错误转变为编译期错误。这句话其实就是定义了一个固定大小的char型数组,数组名为        
                                                                                                             //type_must_be_complete,数组大小是多少呢?是sizeof(T)?1:-1, ?:
                                                                                                             //这个三元操作符大家都很熟悉了,若sizeof(T)非0,这个表达式的值为1,
                                                                                                             //即typedef了一个大小为1的char型数组,否则定义一个大小为-1的数组。数组大小还能为负数?
                                                                                                               //当然不能,于是就会报错,而且是编译期错误,于是就将一个动态运行时错误在编译时就发现了。
    T_must_be_complete_type dummy; (void) dummy;
    delete t_value_;
    t_value_ = 0;
  }
    class Deleter
  {
   public:
    Deleter()
    {
      pthread_key_create(&pkey_, &ThreadLocalSingleton::destructor);
    }

    ~Deleter()
    {
      pthread_key_delete(pkey_);
    }
    void set(T* newObj)
    {
      assert(pthread_getspecific(pkey_) == NULL);
      pthread_setspecific(pkey_, newObj);
    }

    pthread_key_t pkey_;
  };

  static __thread T* t_value_;
  static Deleter deleter_;
};
template<typename T>
__thread T* ThreadLocalSingleton<T>::t_value_ = 0;

template<typename T>
typename ThreadLocalSingleton<T>::Deleter ThreadLocalSingleton<T>::deleter_;

ThreadPool.h 线程池的设计

class ThreadPool : noncopyable
{
private:
private:
  bool isFull() const REQUIRES(mutex_);  //判断队列是否满
  void runInThread();  //线程循环
  Task take();  //从任务队列取一个任务

  mutable MutexLock mutex_;     //锁
  Condition notEmpty_ GUARDED_BY(mutex_);   //非空条件变量
  Condition notFull_ GUARDED_BY(mutex_);  //非满条件变量
  string name_;
  Task threadInitCallback_;        //处理任务的对象
  std::vector<std::unique_ptr<muduo::Thread>> threads_; //线程队列
  std::deque<Task> queue_ GUARDED_BY(mutex_);  //任务队列
  size_t maxQueueSize_;         //任务队列大小
  bool running_;          //判断是否还在运行

 public:
      typedef std::function<void ()> Task;        //一个可调用对象包装成任务

      explicit ThreadPool(const string& nameArg = string("ThreadPool"))  //初始化
       : mutex_(),
        notEmpty_(mutex_),
        notFull_(mutex_),
        name_(nameArg),
        maxQueueSize_(0),
        running_(false)
        {}
      ~ThreadPool() {
         if (running_) {
               stop();
         }
       }

      // Must be called before start().
      void setMaxQueueSize(int maxSize) { maxQueueSize_ = maxSize; }
      void setThreadInitCallback(const Task& cb)
      { threadInitCallback_ = cb; }

      void start(int numThreads) {
          assert(threads_.empty()); //确定线程池为空
          running_ = true;
          threads_.reserve(numThreads);
          for (int i = 0; i < numThreads; ++i)
          {
                char id[32];
                snprintf(id, sizeof id, "%d", i+1);
            threads_.emplace_back(new muduo::Thread(
                      std::bind(&ThreadPool::runInThread, this), name_+id)); //每一个线程的启动函数都是runInThread
            threads_[i]->start();
          }
          if (numThreads == 0 && threadInitCallback_)
          {
                threadInitCallback_();
          }

      }
      void stop(){
       {
          MutexLockGuard lock(mutex_);
          running_ = false;
          notEmpty_.notifyAll();
        }
          for (auto& thr : threads_)
          {
                thr->join();
           }
       }

      const string& name() const
      { return name_; }

      size_t queueSize() const {
          MutexLockGuard lock(mutex_);
          return queue_.size();
      }
      void run(Task f) {
            if (threads_.empty())  //如果没有子线程,就在ThreadPool当前线程执行
          {
            task();
          }
          else //把任务加入任务队列
          {
            MutexLockGuard lock(mutex_);
            while (isFull())
            {
              notFull_.wait();
            }
            assert(!isFull());
            queue_.push_back(std::move(task));
            notEmpty_.notify();
      }
     Task take() {  //从任务队列中去一个任务
          MutexLockGuard lock(mutex_);
          // always use a while-loop, due to spurious wakeup
          while (queue_.empty() && running_)
         {
                notEmpty_.wait();
          }
          Task task;
          if (!queue_.empty())
          {
            task = queue_.front();
            queue_.pop_front();
            if (maxQueueSize_ > 0)
            {
              notFull_.notify();
            }
          }
          return task;
         }
     }
    void runInThread() {              
      if (threadInitCallback_)
    {
      threadInitCallback_();
    }
    while (running_)                     //取出任务,执行. 加入了一些异常处理
    {
      Task task(take());
      if (task)
      {
        task();
      }
    }

    }

转载于:https://www.cnblogs.com/aiqingyi/p/11306521.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值