多线程c++

目录

1.join和detach区别

2.lock_guard和unique_lock 

3.原子操作

4.条件变量condition_variable 

5.call_once

6.future  promise  async

6.1  future

6.2 promise

6.3 async


1.join和detach区别

①不使用join和detach

#include <iostream>
#include <thread>
#include <windows.h>
 
using namespace std;
 
void t1()  //普通的函数,用来执行线程
{
    for (int i = 0; i < 10; ++i)
    {
        cout <<" t1 :"<< i <<"  ";
        _sleep(1);
    }
}
void t2()
{
    for (int i = 11; i < 20; ++i)
    {
        cout <<" t2 :"<< i <<"  ";
        _sleep(1);
    }
}
int main()
{
    thread th1(t1);  //实例化一个线程对象th1,使用函数t1构造,然后该线程就开始执行了(t1())
    thread th2(t2);

    cout << "here is main\n\n";
    system("pause");
    return 0;
}

每次输出的结果都不一样

②使用join

#include <iostream>
#include <thread>
#include <windows.h>
 
using namespace std;
 
void t1()  //普通的函数,用来执行线程
{
    for (int i = 0; i < 10; ++i)
    {
        cout <<" t1 :"<< i <<"  ";
        _sleep(1);
    }
}
void t2()
{
    for (int i = 11; i < 20; ++i)
    {
        cout <<" t2 :"<< i <<"  ";
        _sleep(1);
    }
}
int main()
{
    thread th1(t1);  //实例化一个线程对象th1,使用函数t1构造,然后该线程就开始执行了(t1())
    thread th2(t2);
 
    th1.join(); // 必须将线程join或者detach 等待子线程结束主进程才可以退出
    th2.join(); 
 
    cout << "here is main\n\n";
    system("pause");
    return 0;
}

join会将主线程和子线程th1 th2分离,子线程执行完才会执行主线程。(两个子线程输出每次仍不一样)

③使用detach

#include <iostream>
#include <thread>

#include <windows.h>
 
using namespace std;
 
void t1()  //普通的函数,用来执行线程
{
    for (int i = 0; i < 10; ++i)
    {
        cout <<" t1 :"<< i <<"  ";
        _sleep(1);
    }
   
}
void t2()
{
    for (int i = 11; i < 20; ++i)
    {
        cout <<" t2 :"<< i <<"  ";
        _sleep(1);
    }
}
int main()
{
    thread th1(t1);  //实例化一个线程对象th1,使用函数t1构造,然后该线程就开始执行了(t1())
    thread th2(t2);
 
    th1.detach();
    th2.detach();
 
    cout << "here is main\n\n";
    system("pause");
    return 0;
}

detach也会将主线程和子线程th1 th2分离,但是主线程执行过程子线程仍会执行

总结:

1. join会阻塞当前的线程,直到运行的线程结束。(例如上面第二段代码主线程被阻塞,直到子线程执行完才会执行join之后的主线程代码)

2.detach从线程对象中分离出执行线程,允许线程独立的执行。(上面第三段代码,主线程和两个子线程独立的执行)

参考文章

2.lock_guard和unique_lock 

线程的使用一定需要搭配锁mutex,它是用来保证线程同步的,防止不同的线程同时操作同一个共享数据。

①使用mutex,使用lock函数上锁,unlock解锁

#include <iostream>
#include <thread>
#include <mutex>
#include <stdlib.h>
 
int cnt = 20;
std::mutex m;
void t1()
{
    while (cnt > 0)
    {    
        m.lock();
        if (cnt > 0)
        {
            --cnt;
            std::cout << cnt << std::endl;
        }
        m.unlock();
    }
}
void t2()
{
    while (cnt < 20)
    {
        m.lock();
        if (cnt < 20)
        {
            ++cnt;
            std::cout << cnt << std::endl;
        }
        m.unlock();
    }
}
 
int main(void)
{
    std::thread th1(t1);
    std::thread th2(t2);
 
    th1.join();    //等待t1退出
    th2.join();    //等待t2退出
 
    std::cout << "here is the main()" << std::endl;
    system("pause");
    return 0;
}

②使用lock_guard

使用mutex比较繁琐,需要上锁和解锁,c++提供了lock_guard,它是基于作用域的,能够自解锁,当该对象创建时,它会像m.lock()一样获得互斥锁,当生命周期结束时,它会自动析构(unlock),不会因为某个线程异常退出而影响其他线程。

#include <iostream>
#include <thread>
#include <mutex>
#include <stdlib.h>
 
int cnt = 20;
std::mutex m;
void t1()
{
    while (cnt > 0)
    {    
        std::lock_guard<std::mutex> lockGuard(m);
        // m.lock();
        if (cnt > 0)
        {
            --cnt;
            std::cout << cnt << std::endl;
        }
        // m.unlock();
    }
}
void t2()
{
    while (cnt < 20)
    {
        std::lock_guard<std::mutex> lockGuard(m);
        // m.lock();
        if (cnt < 20)
        {
            ++cnt;
            std::cout << cnt << std::endl;
        }
        // m.unlock();
    }
}
 
int main(void)
{
    std::thread th1(t1);
    std::thread th2(t2);
 
    th1.join();    //等待t1退出
    th2.join();    //等待t2退出
 
    std::cout << "here is the main()" << std::endl;
    system("pause");
    return 0;
}

③使用unique_lock

lock_guard有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。

因此提出了unique_lock,这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当锁的颗粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。

参考文章

3.原子操作

原子性操作库(atomic)是C++11中新增的标准库,它提供了一种线程安全的方式来访问和修改共享变量,避免了数据竞争的问题。在多线程程序中,如果多个线程同时对同一个变量进行读写操作,就可能会导致数据不一致的问题。原子性操作库通过使用原子操作来保证多个线程同时访问同一个变量时的正确性。

例,在多线程中进行加一减一操作,循环一定次数

#include <iostream>
#include <thread>
#include <atomic>
#include <time.h>
#include <mutex>
using namespace std;
 
#define MAX 100000
#define THREAD_COUNT 20
 
int total = 0;
mutex mt;
 
void thread_task()
{
    for (int i = 0; i < MAX; i++)
    {
        mt.lock();
        total += 1;
        total -= 1;
        mt.unlock();
    }
}
 
int main()
{
    clock_t start = clock();
    thread t[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; ++i)
    {
        t[i] = thread(thread_task);
    }
    for (int i = 0; i < THREAD_COUNT; ++i)
    {
        t[i].join();
    }
    
    clock_t finish = clock();
    // 输出结果
    cout << "result:" << total << endl;
    cout << "duration:" << finish - start << "ms" << endl;
 
    system("pause");
    return 0;
}

从结果来看非常耗时,使用原子atomic,不需要使用mutex,(注意添加头文件)

#include <iostream>
#include <thread>
#include <atomic>
#include <time.h>
#include <mutex>
using namespace std;
 
#define MAX 100000
#define THREAD_COUNT 20
 
//原子操作  也不需要使用互斥锁
// atomic_int total(0);
atomic<int> total;

void thread_task()
{
    for (int i = 0; i < MAX; i++)
    {
        total += 1;
        total -= 1;
    }
}
 
int main()
{
    clock_t start = clock();
    thread t[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; ++i)
    {
        t[i] = thread(thread_task);
    }
    for (int i = 0; i < THREAD_COUNT; ++i)
    {
        t[i].join();
    }
    
    clock_t finish = clock();
    // 输出结果
    cout << "result:" << total << endl;
    cout << "duration:" << finish - start << "ms" << endl;
    
    system("pause");
    return 0;
}

 关于具体函数的使用参考官方文档

4.条件变量condition_variable 

条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者多个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程, 条件变量需要和锁配合使用,这里的锁就是上面的unique_lock。

其中有两个非常重要的接口,wait()和notify_one(),wait()可以让线程陷入休眠状态,notify_one()就是唤醒真正休眠状态的线程。还有notify_all()这个接口,就是唤醒所有正在等待的线程。

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
 
deque<int> q;
mutex mt;
condition_variable cond;
 
void thread_producer()
{
    int count = 10;
    while (count > 0)
    {
        unique_lock<mutex> unique(mt);
        q.push_front(count);
        unique.unlock();    // 解锁才能去唤醒
        cout << "producer a value: " << count << endl;
        cond.notify_one();   // 唤醒wait的线程会阻塞当前线程,
        this_thread::sleep_for(chrono::seconds(1));
        count--;
    }
}
 
void thread_consumer()
{
    int data = 0;
    while (data != 1)
    {
        unique_lock<mutex> unique(mt);
        while (q.empty())
            cond.wait(unique);      //解锁,线程被阻塞处于等待状态 |||| 被唤醒后优先获得互斥锁

        data = q.back();   // 使用 back 函数获取最后一个元素
        q.pop_back();
        cout << "consumer a value: " << data << endl;

        // 下面这行可以不写,unique_lock 离开作用域会调用析构判断是否解锁。
        // unique.unlock();     // 解锁后thread_producer获得互斥锁
    }
}
 
int main()
{
    thread t1(thread_consumer);
    thread t2(thread_producer);
    t1.join();
    t2.join();

    system("pause");
    return 0;
}

thread_consumer 的执行流程如下

1.unique_lock<mutex> unique(mt); - 上锁互斥锁。

2.cond.wait(unique); - 释放互斥锁并等待通知。

3.当被通知后,重新获得互斥锁。

4.继续执行后面的代码,包括 data = q.back(); 和 q.pop_back();。

在上述过程中,unique_lock 会自动处理锁的上锁和解锁操作。当 cond.wait(unique); 执行时,它会将互斥锁解锁,并将线程置于等待状态。当线程被唤醒后,unique_lock 会自动重新对互斥锁上锁。

在 thread_producer 中,它获取互斥锁的步骤如下

1.unique_lock<mutex> unique(mt); - 上锁互斥锁。

2.q.push_front(count); - 操作共享资源。

3.unique.unlock(); - 解锁互斥锁。

4.cond.notify_one(); - 发送通知。

在这个过程中,unique_lock 会在 unique.unlock(); 处解锁互斥锁,以允许其他线程进入相应的临界区。当 cond.notify_one(); 发送通知后,thread_consumer 会被唤醒,并在 unique_lock 重新获取互斥锁后继续执行。

这两个互斥锁操作是同步的,不会引起冲突,因为它们是针对不同的互斥锁对象进行的。thread_consumer 上的互斥锁 mt 与 thread_producer 上的互斥锁 mt 是两个不同的互斥锁对象。

注意:在使用条件变量时,一般要在循环中等待条件,因为线程被唤醒后需要重新检查条件是否真的满足。 

更详细的参考文档

例:在两个线程中交替打印A和B

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

std::mutex mtx;
std::condition_variable cv;
std::string flag("A");

void PrintA()
{
    while(true) {
        std::unique_lock<std::mutex> lck(mtx);
        while (flag == "B") {
            cv.wait(lck);
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "A" << std::endl;
        flag = "B";
        cv.notify_all();
    }
}

void PrintB()
{
    while(true) {
        std::unique_lock<std::mutex> lck(mtx);
        while (flag == "A") {
            cv.wait(lck); 
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "B" << std::endl;
        flag = "A";
        cv.notify_all();
    }
}

int main()
{
    std::thread t1(PrintA);
    std::thread t2(PrintB);
    t1.join();
    t2.join();
    return 0;
}

5.call_once

std::call_once来保证某一函数在多线程环境中只调用一次(比如初始化函数只需要执行一次),它需要配合std::once_flag使用

void call_once( std::once_flag& flag, Callable&& f, Args&&... args )
flag      -    对象,对于它只有一个函数得到执行
 f          -    要调用的可调用对象,可以是函数、成员函数、函数对象、lambda函数
args...  -    传递给函数的参数

call_once将CallOnce只执行了一次

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

once_flag onceflag;

mutex mux_;
void CallOnce(){

    lock_guard<mutex> lck(mux_);
    call_once(onceflag,[](){cout<<"callonce"<<endl;});
    cout<<"call back"<<endl;
}

int main(){
 
    thread threads[5];
    for(int i=0; i<5; ++i){
        threads[i] = std::thread(CallOnce);
    }

    for(auto &th:threads){
        th.join();
    }
    system("pause");
    return 0;
}

如果该调用抛出了异常,那么将异常传播给 std::call_once 的调用方,并且不翻转 flag,这样还可以尝试后续调用(称这种对 std::call_once 的调用为异常)。

如果该调用正常返回(称这种对 std::call_once 的调用为返回),那么翻转 flag,并保证以同一 flag 对 std::call_once 的其他调用为消极。

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

std::once_flag flag;

void function_that_may_throw()
{
    std::cout << "Function that may throw\n";
    throw std::exception();  // Simulating an exception
}

void call_once_function(bool do_throw)
{
    try
    {
        std::call_once(flag, [do_throw]() {
            std::cout << "Inside call_once\n";
            if (do_throw) {
                function_that_may_throw();  // May throw an exception
            }
        });
    }
    catch (const std::exception&)
    {
        std::cout << "Caught exception from call_once\n";
    }
}

int main()
{
    std::thread t1(call_once_function, true);
    std::thread t2(call_once_function, false);
    std::thread t3(call_once_function, true);

    t1.join();
    t2.join();
    t3.join();
    return 0;
}

输出结果:

Inside call_once
Function that may throw
Caught exception from call_once
Inside call_once 

第一个线程在 std::call_once调用的代码(lambda函数内部)抛出了异常,异常将传播给std::call_once的调用方(这里是call_once_function函数),然后去catch这个异常,不会翻转flag。第二次线程继续执行std::call_once,传的参数是false不会抛出异常,正常返回翻转flag。第三个线程就不会在执行std::call_once。

6.future  promise  async

关于更详细的参考文档 

6.1  future

为什么使用future:想要从线程中返回异步任务结果,一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。

std::future通过std::promise、std::packaged_task和std::async创建的异步操作。future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,promise用来包装一个值, 将数据和future绑定起来,而packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而future是不可以复制的,如果需要复制放到容器中可以使用shared_future。

 例1:使用packaged_task、async和promise创建future

#include <iostream>
#include <future>
#include <thread>
 
int main()
{
    // 来自 packaged_task 的 future
    std::packaged_task<int()> task([](){ return 7; }); // 包装函数
    std::future<int> f1 = task.get_future();  // 获取 future
    std::thread(std::move(task)).detach(); // 在线程上运行
 
    // 来自 async() 的 future
    std::future<int> f2 = std::async(std::launch::async, [](){ return 8; });
 
    // 来自 promise 的 future
    std::promise<int> p;
    std::future<int> f3 = p.get_future();
    std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
 
    std::cout << "Waiting..." << std::flush;
    f1.wait();
    f2.wait();
    f3.wait();
    std::cout << "Done!\nResults are: "
              << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';
}

#输出结果
Waiting...Done!
Results are: 7 8 9

get和wait区别

调用 get 方法会等待异步任务的完成,并返回任务的结果(如果任务有返回值的话)。

如果异步任务抛出了异常,调用 get 方法会重新引发异常,需要适当的异常处理机制来捕获异常。

get 方法会阻塞当前线程,直到异步任务完成。

调用 wait 方法同样会等待异步任务的完成,但它不返回任务的结果。

 如果异步任务抛出了异常,wait 方法并不会捕获或重新引发异常,异常需要在其他地方进行处理。

wait 方法会阻塞当前线程,直到异步任务完成。


6.2 promise

 promise介绍:promise 对象可以保存某一类型 T 的值,该值可被 future 对象读取(可能在另外一个线程中),因此 std::promise 提供了一种线程同步的手段。

例2:promise创建的future

#include <iostream>       
#include <functional>     
#include <thread>        
#include <future>     // std::promise, std::future

// 这个函数用于从 future中获取共享状态的值,并打印出来。future 提供了对共享状态的异步访问。
void print_int(std::future<int>& fut) {
    int x = fut.get();                    // 获取共享状态的值.
    std::cout << "value: " << x << '\n';  // 打印 value: 10.
}

int main ()
{
    std::promise<int> prom;                    // 生成一个 promise<int> 对象,它允许在某个时间点设置共享状态的值。
    std::future<int> fut = prom.get_future();  // 和 future 关联,fut 将用于在另一个线程中获取共享状态的值。
    std::cout<<"start:"<<std::endl;
    std::thread t(print_int, std::ref(fut));   // 将 future 交给另外一个线程t,线程 t 将能够异步获取共享状态的值。
    std::cout<<"wait:"<<std::endl;
    prom.set_value(10);                        // 设置共享状态的值, 因为 fut 与 prom 相关联,所以 fut.get() 将返回这个设置的值。
    t.join();
    std::cout<<"end!"<<std::endl;
    return 0;
}


#输出结果
start:
wait:
value: 10
end!

例3:如何将promise<int>用作线程间信号

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
 
void accumulate(std::vector<int>::iterator first,
                std::vector<int>::iterator last,
                std::promise<int> accumulate_promise)
{
    int sum = std::accumulate(first, last, 0);    // std::accumulate 是 C++ 标准库 <numeric> 头文件中提供的一个函数,用于计算指定范围内元素的累积值。
    accumulate_promise.set_value(sum);            // 提醒 future
}
 
void do_work(std::promise<void> barrier)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    barrier.set_value();
}
 
int main()
{
    // 演示用 promise<int> 在线程间传递结果。
    std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
    std::promise<int> accumulate_promise;
    std::future<int> accumulate_future = accumulate_promise.get_future();
    std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
                            std::move(accumulate_promise));
 
    // future::get() 将等待直至该 future 拥有合法结果并取得它
    // 无需在 get() 前调用 wait()
    //accumulate_future.wait();  // 等待结果
    std::cout << "result=" << accumulate_future.get() << '\n';
    work_thread.join();  // wait for thread completion
 
    // 演示用 promise<void> 在线程间对状态发信号
    std::promise<void> barrier;
    std::future<void> barrier_future = barrier.get_future();
    std::thread new_work_thread(do_work, std::move(barrier));
    barrier_future.wait();    // 阻塞直至结果变得可用
    // barrier_future.wait() 和 barrier_future.get() 都是等待 barrier 的状态变为可用。
    // 然而,barrier_future.get() 不适用于 std::promise<void>,因为 std::promise<void> 不传递任何值,它只是用于通知或等待一个事件的发生。
    // 具体来说,std::promise<void> 可以在某个线程中设置为"ready",然后其他线程等待这个状态变为"ready"。
    // 但由于 std::promise<void> 不传递值,因此 get() 不返回任何有用的信息,只是等待 barrier 状态的变化。

    new_work_thread.join();

    return 0;
}

 关于为什么使用move:因为 thread 的构造函数要求可调用对象和参数都是可移动的,promise 类是不可拷贝的,只能通过移动来转移所有权。在这个例子中,accumulate_promise的所有权被移动到work_thread线程中,因为std::thread的构造函数要求传递可移动的参数。这也意味着在主线程中不能再使用accumulate_promise,因为它的所有权已经被转移。

std::promise<int> prom;                 
std::future<int> fut = prom.get_future();  
std::thread t(print_int, std::ref(fut));  

std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(), std::move(accumulate_promise)); 

6.3 async

 async介绍:​ 它是基于任务的异步操作,通过async可以直接创建异步的任务, 返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦。

函数模板 std::async 异步地运行函数 f(有可能在可能是线程池一部分的分离线程中),并返回最终将保有该函数调用结果的 std::future。

async( std::launch policy, Function&& f, Args&&... args );

参数

policy  -  位掩码值,每个单独位控制允许的执行方法

                 std::launch::async    表示任务执行在另一线程

                 std::launch::deferred 表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程

                 如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度 器(线程池),会根据实际情况决定采用哪种策略。

 f         -   要调用的可调用对象

args... -   传递给 f 的参数

返回值

指代此次调用 std::async 所创建的共享状态的 std::future

例4:

#include <algorithm>
#include <future>
#include <iostream>
#include <mutex>
#include <numeric>
#include <string>
#include <vector>
 
std::mutex m;
 
struct X
{
    void foo(int i, const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << ' ' << i << '\n';
    }
 
    void bar(const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << '\n';
    }
 
    int operator()(int i)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << i << '\n';
        return i + 10;
    }
};
 
template<typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
    auto len = end - beg;
    if (len < 1000)
        return std::accumulate(beg, end, 0);
 
    RandomIt mid = beg + len / 2;

    // 创建一个异步任务,使用 std::async 启动一个新线程(或线程池中的线程),调用 parallel_sum 函数处理范围 [mid, end)。
    // std::launch::async 是启动一个异步任务的标志,表示使用新线程执行异步任务。
    auto handle = std::async(std::launch::async, parallel_sum<RandomIt>, mid, end);
    // 递归调用 parallel_sum 处理范围 [beg, mid)
    int sum = parallel_sum(beg, mid);
    // 获取异步任务的结果,将递归调用的结果(beg,mid)与异步任务的结果(mid,end)相加,最终得到整个范围 [beg, end) 的并行求和结果。
    return sum + handle.get();
}
 
int main()
{
    std::vector<int> v(10000, 1);
    std::cout << "和为 " << parallel_sum(v.begin(), v.end()) << '\n';
 
    X x;
    // 使用的是默认的异步启动策略,即std::launch::async | std::launch::deferred  调用 x.foo(42, "Hello")   
    std::future<void> a1 = std::async(&X::foo, &x, 42, "Hello"); 

    // 以 deferred 策略  调用 x.bar("world!")  只有当调用 a2.get() 或 a2.wait() 时打印 "world!"
    std::future<void> a2 = std::async(std::launch::deferred, &X::bar, &x, "world!");

    // 以 async 策略调用 X()(43)  同时打印 "43"
    // std::future<int> a3 = std::async(std::launch::async, X(), 43);
    std::future<int> a3 = std::async(std::launch::async, &X::operator(), &x, 43);


    a2.wait();        
    
    std::cout << a3.get() << '\n'; // 打印 "53"
} 


 

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值