C++多线程


  • 💂 个人主页:风间琉璃
  • 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
  • 💬 如果文章对你有帮助欢迎关注点赞收藏(一键三连)订阅专栏

前言

提示:这里可以添加本文要记录的大概内容:
在 C++11 之前,C++ 标准库并未提供内建的多线程支持。要在 C++ 中实现多线程,程序员需要依赖操作系统提供的原生线程库,如在 Linux 中使用 <pthread.h>,或在 Windows 中使用 <windows.h> 等平台特定的 API。

在这里插入图片描述

C++11提供了语言层面上的多线程,包含在头文件中。它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。


一、多线程

1.多线程使用

C++11中提供的线程类叫做std::thread,基于这个类创建一个新的线程只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。

// 1
thread() noexcept;
// 2
thread( thread&& other ) noexcept;
// 3
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// 4
thread( const thread& ) = delete;
  • 构造函数①:默认构造函数,构造一个线程对象,在这个线程中不执行任何处理动作

  • 构造函数②:移动构造函数,将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。

  • 构造函数③:创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数
    任务函数f的可选类型有很多,具体如下:

    • 普通函数类成员函数匿名函数仿函数(这些都是可调用对象类型)
    • 可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
  • 构造函数④:使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝

使用示例:

#include <iostream>
#include <thread> // c++ 多线程库


// 线程函数
void func(int n)
{
    for (int i = 0; i < n; ++i) 
    {
        std::cout << std::this_thread::get_id() <<" Thread Function executing: " << i << std::endl;
    }
}

int main()
{
    int n = 10;
    std::thread thread1(func,n);   // 创建线程,执行 func 函数
    thread1.join();                // 等待线程结束

    // 主线程继续执行
    for (int i = 0; i < 5; ++i) 
    {
        std::cout << std::this_thread::get_id() << " Main Thread executing: " << i << std::endl;
    }

    return 0;
}

应用程序启动后默认只有一个线程,这个线程一般称为主线程或父线程通过线程类创建出的线程一般称为子线程,每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的,可以通过ID来区分和识别各个已经存在的线程实例,获取线程ID的函数叫做get_id(),函数原型如下:

std::thread::id get_id() const noexcept;
thread1.get_id()  // 线程对象调用get_id()获取子线程ID

std::thread thread1(func,n)创建子线程对象thread1,func()函数会在子线程中运行,这是一个回调函数,线程对象创建后就会执行任务函数

func()的参数是通过thread的参数进行传递的,线程类的构造函数第三种是一个变参函数,因此无需担心线程任务函数的参数个数问题。任务函数func()一般返回值指定为void,因为子线程在调用这个函数的时候不会处理其返回值。

当线程启动后,一定要在和线程相关联的thread销毁(std::terminate())前,确定以何种方式等待线程执行结束,回收线程所使用的资源。程序必须要在线程对象销毁之前在二者之间作出选择,否则程序运行期间就会有bug产生。thread库提供了两种解决方案:

  • 加入式(join()):等待启动的线程完成,才会继续往下执行
  • 分离式(detach()):启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束

1.join()线程回收

join()阻塞当前线程,直到被调用的线程完成执行。在某个线程中通过子线程对象调用join()函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕后,join()会清理当前子线程中的相关资源然后返回,同时调用该函数的线程解除阻塞继续向下执行。

void join();

2.detach()线程分离

detach()函数的作用是进行线程分离,分离主线程和创建出的子线程。启动的线程自主在后台运行,在线程分离后,主线程退出也会一并销毁创建出的所有子线程,但是在主线程退出前,它可以脱离主线程继续独立的运行,任务执行完毕后,这个子线程会自动释放自己占用的系统资源。

void detach();

注意事项:线程分离函数detach()不会阻塞线程,子线程和主线程分离后,在主线程中就不能再对这个子线程做任何控制,比如:通过join()阻塞主线程等待子线程中的任务执行完毕,或者调用get_id()获取子线程的线程ID。

3.joinable()

joinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

bool joinable() const noexcept;

可以使用joinable判断是join模式还是detach模式:

if (thread1.joinable())
{
    thread1.join();
}

2.多线程参数传递的注意事项

参数传递有传值、传引用和传对象三种方式。

1.传值:传递简单的参数类型(如整数)时,通常是按值传递。这样做可以避免数据竞争,但对于较大的数据结构,可能会引入不必要的拷贝开销。

void threadFunction(int n) {
    // 使用 n
}

std::thread t(threadFunction, 5); // 按值传递

2.传引用:如果需要传递较大的数据结构或希望在线程间共享数据,可以按引用传递。需要使用 std::ref 包装参数以明确传递引用。

void threadFunction(std::vector<int>& vec) {
    // 修改 vec
}

std::vector<int> data = {1, 2, 3, 4, 5};
std::thread t(threadFunction, std::ref(data)); // 按引用传递

3.传递对象:传递对象时,需要考虑对象的拷贝构造函数和移动构造函数是否高效。如果对象不可拷贝或移动,可以考虑使用指针或智能指针。

class MyClass {
public:
    void operator()() {
        // 执行任务
    }
};

MyClass obj;
std::thread t(std::move(obj)); // 使用 std::move 传递对象

3.多线程常用函数

1.std::this_thread命名空间

std::this_thread 是 C++11 标准库中的一个命名空间,包含了一组用于与当前线程进行交互的函数。这些函数提供了一些基本的线程管理功能,如获取线程ID使线程休眠等。

1.std::this_thread::get_id

std::this_thread::get_id 返回当前线程的 std::thread::id,用于唯一标识当前线程。

2.std::this_thread::sleep_for

std::this_thread::sleep_for 使当前线程休眠指定的时间。调用该函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出CPU资源,代码也不会被执行。

#include <iostream>
#include <thread>
#include <chrono>

void doWork() {
    std::cout << "Work started..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Work finished after 2 seconds." << std::endl;
}

int main() {
    std::thread t(doWork);
    t.join();
    return 0;
}

需要注意的是:程序休眠完成后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到后才会变成运行态,这时候程序才会继续向下运行

3.std::this_thread::sleep_until

std::this_thread::sleep_until 使当前线程休眠直到指定的时间点

#include <iostream>
#include <thread>
#include <chrono>

void doWork() {
    std::cout << "Work started..." << std::endl;
    auto wakeUpTime = std::chrono::steady_clock::now() + std::chrono::seconds(2);
    std::this_thread::sleep_until(wakeUpTime);
    std::cout << "Work finished after 2 seconds." << std::endl;
}

int main() {
    std::thread t(doWork);
    t.join();
    return 0;
}

4.std::this_thread::yield

std::this_thread::yield 提示操作系统可以将当前线程的执行让位给另一个线程。这个函数不会阻塞当前线程,但它可以帮助更好地调度线程。

#include <iostream>
#include <thread>

void busyWork() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Doing busy work " << i << std::endl;
        std::this_thread::yield(); // Yield to allow other threads to run
    }
}

int main() {
    std::thread t(busyWork);
    t.join();
    return 0;
}
  • std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降
  • std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢

2.hardware_concurrency cpu核心数计算

thread线程类还提供了一个静态方法hardware_concurrency(),用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

static unsigned hardware_concurrency() noexcept;
int num = std::thread::hardware_concurrency();

二、线程的同步

只读数据在线程间操作时是安全稳定的,不需要特殊处理,直接读即可。然而,当一些线程进行写操作,而另一些线程进行读操作时,如果不进行特殊处理,程序肯定会崩溃

为了解决这个问题,针对读写操作,需要在对某个数据进行读写时先进行加锁。其他线程必须等待该操作完成并对数据解锁后,才能进行访问。这样可以保护共享数据,确保线程安全。

1.mutex互斥量

在 C++11 中,互斥量(mutex)是一种用于管理对共享资源访问的同步原语。mutex提供了4种互斥类型:

类型作用
std::mutex互斥量基本的Mutex类
std::recursive_mutex递归互斥量递归互斥量允许同一线程多次锁定同一互斥量
std::timed_mutex定时互斥量定时互斥量允许尝试锁定互斥量,并在指定时间内进行等待
std::recursive_timed_mutex递归定时互斥量递归定时互斥量结合了递归互斥量和定时互斥量的特性

a.构造函数
std::mutex:默认构造函数,创建一个未锁定的互斥量对象

std::mutex mtx;

b.上锁函数
lock:锁定互斥量。如果互斥量已经被锁定,则阻塞当前线程,直到互斥量变为可用。

mtx.lock();

c.解锁函数
unlock:解锁互斥量。如果当前线程没有锁定互斥量,行为未定义。

mtx.unlock();

d.尝试上锁函数
try_lock:尝试锁定互斥量。如果成功锁定,则返回 true;如果互斥量已经被锁定,则立即返回 false。

if (mtx.try_lock()) {
    // 锁定成功
} else {
    // 锁定失败
}

示例

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx; // 定义一个互斥锁
int shared_counter = 0; // 共享数据

void increment(int id) {
    for (int i = 0; i < 100; ++i) {
        mtx.lock(); // 显式加锁
        ++shared_counter;
        std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl;
        mtx.unlock(); // 显式解锁
    }
}

int main() {
    const int num_threads = 10;
    std::vector<std::thread> threads;

    // 创建多个线程
    for (int i = 0; i < num_threads; ++i) {
        threads.push_back(std::thread(increment, i));
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << shared_counter << std::endl;
    return 0;
}

2.使用锁管理类 std::lock_guard 和 std::unique_lock

1.std::lock_guard

std::lock_guard 是一种简单的 RAII(资源获取即初始化)类型,用于在作用域内自动管理互斥量的锁定和解锁

使用std::lock_guard实现自动加锁和解锁。当lock对象创建时,mtx被加锁;当lock对象超出作用域时,mtx自动解锁。这种方式确保了异常安全,防止在函数因异常退出时未能正确解锁的问题。

lock_guard的特点:

  • 1.当构造函数被调用时,该互斥量会被自动锁定
  • 2.当析造函数被调用时,该互斥量会被自动解锁
  • 3.std::lock_guard对象不能复制或移动,因此它只能在局部作用域中使用。

创建 std::lock_guard 对象并锁定互斥量

std::lock_guard<std::mutex> guard(mtx);

示例

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increaseCounter() {
    std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量
    ++counter;
    std::cout << "Counter: " << counter << std::endl;
    // 离开作用域时,lock_guard 自动解锁
}

int main() {
    std::thread t1(increaseCounter);
    std::thread t2(increaseCounter);

    t1.join();
    t2.join();

    return 0;
}

在这个示例中,increaseCounter 函数通过 std::lock_guard 锁定互斥量 mtx。当 increaseCounter 函数结束时,std::lock_guard 对象离开作用域,自动解锁 mtx。

2.std::unique_lock

std::unique_lock 提供了更灵活的锁管理功能,支持延迟锁定、手动解锁和重新锁定。unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。

std::unique_lock 的特点包括:

  • 延迟锁定:在创建 std::unique_lock 对象时,可以通过指定第二个参数为 std::defer_lock 来创建一个未锁定的互斥量对象,允许在需要时再锁定。
  • 灵活性:可以在对象的生命周期内随时进行加锁和解锁操作,提供了更大的灵活性。
  • 自动释放锁:与 std::lock_guard 类似,std::unique_lock 遵循 RAII 规则,在其作用域结束时自动释放锁。
  • 不可复制,可移动std::unique_lock 对象是不可复制的,但可以移动,从而支持在需要时转移锁的所有权。
  • 条件变量支持:在使用条件变量(如 std::condition_variable)时,必须使用 std::unique_lock 作为参数,以确保线程安全的等待和通知机制。

创建 std::unique_lock 对象,可以选择是否立即锁定互斥量

std::unique_lock<std::mutex> lock(mtx); // 使用给定的互斥量mtx进行初始化,并对该互斥量进行加锁操作
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立即锁定
lock.lock(); // 手动锁定

使用示例

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increaseCounter() {
    std::unique_lock<std::mutex> lock(mtx);  // 锁定互斥量
    ++counter;
    std::cout << "Counter: " << counter << std::endl;
    // 离开作用域时,unique_lock 自动解锁
}

int main() {
    std::thread t1(increaseCounter);
    std::thread t2(increaseCounter);

    t1.join();
    t2.join();

    return 0;
}

3.condition_variable

条件变量是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。条件变量始终关联到一个互斥。它提供了一种机制,允许一个或多个线程等待某个条件变量的改变,然后继续执行。这对于解决生产者-消费者问题、实现事件通知等场景非常有用。

std::condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。通过成员函数wait、notify_one、notify_all来进行条件相关的操作。

成员函数作用
wait()阻塞当前线程,直到被通知(通过 notify_one 或 notify_all),并且指定的条件为真
notify_one()通知一个等待该条件变量的线程,使其从 wait 中返回
notify_all()通知所有等待该条件变量的线程,使它们从 wait 中返回

wait函数有以下几种

void wait(std::unique_lock<std::mutex>& lock);


template< class Predicate >
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

作用都是使当前线程进入等待状态,直到条件变量被通知。等待时,互斥锁会被解锁。当线程被唤醒后,互斥锁会被重新锁定。

第二种多了一个pred: 一个谓词(通常是一个函数或 lambda 表达式),返回 true 表示条件满足,线程不再等待。用于防止虚假唤醒,即线程在条件满足前被意外唤醒。这个表达式在每次被唤醒时都会被检查,如果条件不满足,线程会重新进入等待状态

使用步骤:

  • 1.创建一个条件变量对象 std::condition_variable
  • 2.创建一个互斥锁 std::mutex 以保护共享数据。
  • 3.在需要等待条件的线程中使用 std::unique_lock<std::mutex> lock锁定互斥锁,然后调用 wait(lock)
  • 4.在改变共享数据的线程中修改数据并调用 notify_one() 或 notify_all()

示例

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;

void producer(int count) {
    for (int i = 0; i < count; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        dataQueue.push(i);
        std::cout << "Produced: " << i << std::endl;
        cv.notify_one(); // 通知一个等待的消费者
    }
    std::unique_lock<std::mutex> lock(mtx);
    finished = true;
    cv.notify_all(); // 通知所有等待的消费者,生产结束
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !dataQueue.empty() || finished; });
        while (!dataQueue.empty()) {
            int value = dataQueue.front();
            dataQueue.pop();
            std::cout << "Consumed: " << value << std::endl;
        }
        if (finished) break;
    }
}

int main() {
    std::thread prodThread(producer, 10);
    std::thread consThread(consumer);

    prodThread.join();
    consThread.join();

    return 0;
}

生产者线程:向队列中添加数据,每次添加数据后,调用 cv.notify_one() 通知消费者线程有新数据可用。生产完成后,将 finished 标志设置为 true,并调用 cv.notify_all() 通知所有消费者线程生产已完成。

消费者线程:等待数据队列非空或生产结束。使用 cv.wait() 进行等待,并传入一个 lambda 表达式作为条件,当队列非空或生产结束时解除等待,然后消费数据,直到队列为空并且生产结束。

4.atomic原子操作

std::atomic 提供了一组用于执行原子操作的模板和类型,以确保多线程编程中的数据安全。原子操作是不可分割的,不会被其他线程中断,在多线程环境下,多个线程同时对同一个 std::atomic 变量进行操作,可以避免数据竞争和不一致性。

#include <atomic>

std::atomic<int> atomicInt(0); // 声明一个原子整数并初始化为 0

示例

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

std::atomic<int> counter(0); // 声明一个原子计数器并初始化为 0

void incrementCounter(int numIterations) {
    for (int i = 0; i < numIterations; ++i) {
        //counter.fetch_add(1); // 原子加法操作
        counter++;
    }
}

int main() {
    const int numThreads = 10;
    const int numIterations = 1000;
    
    std::vector<std::thread> threads;
    
    // 创建并启动多个线程
    for (int i = 0; i < numThreads; ++i) {
        threads.push_back(std::thread(incrementCounter, numIterations));
    }
    
    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final counter value: " << counter << std::endl;
    
    return 0;
}

三、异步并发

1.async与future

std::async 和 std::future 是 C++11 引入的标准库工具,用于实现异步任务和并发编程。它们提供了一种简单的方式来启动异步任务,并获取任务的结果,而不需要显式地管理线程。

std::async 是一个模板函数,用于启动异步任务。它会启动一个新的线程来执行指定的任务,并返回一个 std::future 对象用于获取任务的结果

template< class Function, class... Args >
std::future< typename std::result_of<Function(Args...)>::type >
async( std::launch policy, Function&& f, Args&&... args );

  • policy:指定任务的启动策略,可以是 std::launch::async(强制异步)或std::launch::deferred(延迟执行)。
  • f:要执行的函数。
  • args:传递给函数的参数。
  • 返回一个 std::future 对象,可以用于获取异步任务的结果。

std::future 是一个模板类,用于访问异步操作的结果。它可以从 std::async、std::promise 或 std::packaged_task 中获取。

主要方法

  • get():获取异步操作的结果,如果结果还未准备好,则阻塞等待
  • wait():等待异步操作完成。
  • valid():检查 std::future 是否有与共享状态相关联。

示例

#include <iostream>
#include <future>
#include <chrono>

// 异步任务函数
int asyncTask(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟长时间任务
    return x * x;
}

int main() {
    // 使用 std::async 启动异步任务, 相当于创建了一个线程,不会阻塞程序
    std::future<int> result = std::async(std::launch::async, asyncTask, 5);
    
    // 主线程可以执行其他工作
    std::cout << "Doing other work in main thread...\n";
    
    // 获取异步任务结果,get()函数会阻塞程序直到异步操作执行完成
    std::cout << "Result from asyncTask: " << result.get() << std::endl;

    return 0;
}

2.packaged_task

std::packaged_task 的主要功能是将可调用对象(如函数、lambda 表达式或函数对象)包装成一个任务,并将其结果传递给一个 std::future 对象。这样,任务可以在不同的线程中执行,并且主线程可以通过 std::future 获取任务的执行结果。

构造函数

// 构造一个包装了可调用对象 f 的 packaged_task。
template< class Callable > explicit packaged_task(Callable&& f)
#include <iostream>
#include <future>
#include <thread>
#include <functional>

// 异步任务函数
int asyncTask(int x) {
    return x * x;
}

int main() {
    // 创建一个包装了异步任务函数的 std::packaged_task
    std::packaged_task<int(int)> task(asyncTask);

    // 获取与任务关联的 std::future 对象
    std::future<int> result = task.get_future();

    // 在一个新线程中执行任务
    std::thread t(std::move(task), 5);

    // 在主线程中获取任务结果
    std::cout << "Result from asyncTask: " << result.get() << std::endl;

    // 等待线程完成
    t.join();

    return 0;
}

std::future get_future()获取与 packaged_task 共享状态相关联的 std::future 对象。

3.promise

std::promise 是 C++11 标准库中的一个类模板,用于在多线程编程中创建异步任务和传递任务结果。std::promise 可以与 std::future 配合使用,提供一种机制来在线程之间传递值或异常

std::promise 提供了一种机制,用于在一个线程中设置异步操作的结果,并允许另一个线程通过与 std::promise 关联的 std::future 对象获取结果。std::promise 主要用于以下场景:

  • 1.在一个线程中设置结果。
  • 2.在另一个线程中等待结果并获取。
成员函数作用
std::future<T> get_future()获取与 std::promise 共享状态相关联的 std::future 对象
void set_value(const T& value)设置共享状态的结果值
void set_value(T&& value)设置共享状态的结果值,使用右值引用
void set_exception(std::exception_ptr p)设置共享状态的异常

以下是一个简单的示例,如何使用 std::promise 和 std::future 在两个线程之间传递结果:

#include <iostream>
#include <thread>
#include <future>

// 异步任务函数
void asyncTask(std::promise<int> promise) {
    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(2));
    int result = 42;  // 假设计算结果是42

    // 设置结果
    promise.set_value(result);
}

int main() {
    // 创建一个 std::promise 对象
    std::promise<int> promise;

    // 获取与 promise 关联的 std::future 对象
    std::future<int> result = promise.get_future();

    // 启动一个线程执行异步任务
    std::thread t(asyncTask, std::move(promise));

    // 在主线程中等待并获取结果
    std::cout << "Waiting for result..." << std::endl;
    int value = result.get();
    std::cout << "Result: " << value << std::endl;

    // 等待线程完成
    t.join();

    return 0;
}

  • std::future:表示异步操作的结果,可以阻塞等待结果完成。
  • std::async:用于启动异步任务,返回一个 std::future 对象。
  • std::promise:用于在线程之间设置和传递异步操作的结果,与 std::future 配合使用。
  • std::packaged_task:用于包装可调用对象,使其异步执行,并通过 std::future 获取结果。

结束语

感谢阅读吾之文章,今已至此次旅程之终站 🛬。

吾望斯文献能供尔以宝贵之信息与知识也 🎉。

学习者之途,若藏于天际之星辰🍥,吾等皆当努力熠熠生辉,持续前行。

然而,如若斯文献有益于尔,何不以三连为礼?点赞、留言、收藏 - 此等皆以证尔对作者之支持与鼓励也 💞。

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Super.Bear

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

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

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

打赏作者

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

抵扣说明:

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

余额充值