C++多线程、简易线程池

Linux下C语言版多线程: https://xingzhu.top/archives/duo-xian-cheng

创建进程

#include <thread>   // 头文件
thread name (function_name, args..);
  • name:进程名
  • function_name:函数名字,仿函数
  • args:函数传的参数
  • 这个函数是在创建线程的时候就执行这个函数
void print(string str) {  
    cout << "Hello Thread!" << str << endl;  
}  

int main() {  
    // 方式一
    thread t1(print, "hh");   
    // 方式二
    thread t1;
    t1 = thread(print, "hh");
    return 0;
}
  • 这样实际有问题,有可能编译器报错,没报错实际也有问题
  • 因为创建线程后,仿函数就执行了,但是主线程不会等待子线程结束后才往下继续执行,可能仿函数还没执行完,也就是上面的还没打印完,主程序就执行完毕。
// 解决措施一
// t1.join();  主程序等待线程执行完毕
t1.join();    // 打印 Hello Thread!hh

// 解决措施二
// t1.detach(); 分离主线程,子线程在后台持续运行,主线程可以结束,不报错
t1.detach();   // 可能打印空

// 严谨的写法,只有可以调用 join 和 detach 才能写
// t1.joinable()  判断这个线程能否调用 join 或者 detach,返回一个 bool
if(t1.joinable()) {
    t1.join();
}
if(t1.joinable()) {
    t1.detach();
}

易错点

传参为引用型

std:: ref ( ) 这是一个将变量转换为引用类型 传递引用,不管自身是不是引用,都要使用 ref ()

void func(int &x) {  
    x += 1;  
}  

int main()  
{  
    int a = 1;  
    int &b = a;
    thread t1(func, ref(a));  // 正确
    thread t1(func, ref(1));  // 错误,因为 1 是临时变量,执行完就释放,拿不到地址
    thread t1(func, a);       // 错误,虽然 int &x = a 能通过,但是这里还是要转换为 ref
    thread t1(func, b);       // 错误
    func(a);                  // 正确  
}

传递指针或引用指向局部变量的问题

一般就是函数执行完后的局部变量,释放了内存,而 线程还在操作

错误演示 1:

thread t1;
void func(int &x) {  
    x += 1;  
}  
void test() {
    int x = 1;
    t1 = thread(func, ref(x));
}

int main() {
    test();  
}
  • 因为 test 函数执行完,x 内存就释放了,线程在执行 x += 1 非法访问了
  • 当时的疑惑是为啥会非法访问,因为感觉定义的时候就执行函数了,实际不是这样
  • 多线程执行顺序和函数执行顺序不一致,有可能函数结束后才执行线程,因为线程是并发的

错误示范 2

void func1(int *x) {  
    x += 1;  
}  
int main() {  
    int *ptr = new int(1);
    thread t2(func1, ptr);
    delete ptr;  // 正确的是放在 join 函数后
    t.join();
}
  • 潜在的问题,虽然没报错,但是问题仍在
  • delete ptr; 正确的是放在 join 函数后,也就是等待子线程完成之后,因为有可能还未执行完成,就通过 delete ptr 把内存释放了,就非法访问了

类成员函数调用的问题

错误示范

class MyClass {
public:
    void func(int &x) {
        x += 1;
    }
};

int main() {
    int a = 1;
    MyClass obj;

    // 创建线程,传递成员函数和对象实例
    // 传递函数指针,一般函数就是函数名,而成员函数需加作用域
    // 光有这个指针还不够,因为它不知道要在哪个对象上调用这个函数,所以传参数 &obj
    thread t1(&MyClass::func, &obj, ref(a));
    // 潜在的问题就是 obj 提前释放,报错访问了被释放的对象,这个使用智能指针解决
    return 0;
}
// 智能指针 shared_ptr -----> 不需要手动释放内存,生命周期结束后自动释放,封装好了
#include <iostream>
#include <thread>
#include <memory>
#include <chrono>
using namespace std;
class MyClass {
public:
    void memberFunction(int &x) {
        this_thread::sleep_for(chrono::seconds(2));
        x += 1;
        cout << "Thread is modifying x to: " << x << endl;
    }
};

int main()
{
    int a = 1;
    auto obj = make_shared<MyClass>(); // 使用智能指针
    thread t1(&MyClass::memberFunction, obj, ref(a));
    t1.join(); // 等待线程完成
    cout << "Final value of a: " << a << endl;
    return 0;
}

互斥量

常见问题

#include <mutex>

mutex mt1;
int a = 0;  
void func() {  
    for(int i = 1; i <= 100; i++) {  
        // mt1.lock();
        a += 1;  
        // mt1.unlock();
    }  
}  
int main()  
{  
    thread t1(func);  
    thread t2(func);  
    t1.join();  
    t2.join();  
    return 0;  
}
  • 按理说结果为 200,但是这里存在线程同时访问情况,也就是都拿到 a,进行操作,那么结果比 200 小或者等
  • 现在编译器输出一直等于 200,应该是编辑器的操作,进行了强制加锁
  • 实际此时线程不安全,那么应该加个互斥信号量,也就是上述代码中的注释,在加操作前后加锁和解锁

互斥量处理不周到导致的死锁情况 一个情景是 t1 先获取 mt1 锁控制,t2 获取 mt2 锁控制,然后再各自获取 mt2, mt1 下面就出现了循环等待的情况,死锁

错误示范

mutex mt1, mt2;  
void func1() {  
    mt1.lock();  
    _sleep(100);  // 加这个测试,防止出现 t1 获取速度过快把 mt1 mt2都获取了
    mt2.lock();  
    mt2.unlock();  
    mt1.unlock();  
}  
  
void func2() {  
    mt2.lock();  
    mt1.lock();  
    mt1.unlock();  
    mt2.unlock();  
}  
  
int main()  
{  
    thread t1(func1);  
    thread t2(func2);  
    t1.join();  
    t2.join();  
    cout << "over" << endl;  
    return 0;  
}

// 解决方案:使其获取锁的顺序相同,都先 mt1,再 mt2

类型

lock_guard

std:: lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个 线程同时访问同一资源而导致的数据竞争问题

std:: lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定
  • 当析构函数被调用时,该互斥量会被自动解锁
  • std:: lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用
void func() {  
    for(int i = 1; i <= 100; i++) {  
        lock_guard<mutex> lg(mt1); // 自动加锁,在这个局部作用域结束后,自动调用析构解锁
        a += 1;    
    }  
}
// 需要指定锁的类型,比如这里的 mutex,因为源码有 explict ,不支持隐式转换

unique lock

主要特点:

  • 可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等
  • 自动加锁和解锁

成员函数:

  • lock ():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁
  • try_lock ():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true
  • try_lock_for (const std::chrono::duration<Rep, Period>& rel_time):加锁,加的延迟锁,如果超过了这个时间,直接返回,不必阻塞了
  • try_lock_until (const std::chrono:: time point<Clock, Duration>& abs_time) :加时间点锁,超过了这个时间点,直接返回,不阻塞了
// 一般情况
void func() {  
    for(int i = 1; i <= 100; i++) {  
        unique_lock<mutex> lg(mt1);  
        a += 1;    
    }  
}

// 手动加锁
#include <mutex>
timed_mutex mt1;    // 加时间锁,这里互斥量需要改变成 timed_mutex
int a = 0;  
void func() {  
    for(int i = 1; i <= 2; i++) {  
        unique_lock<timed_mutex> lg(mt1, defer_lock);   
        // 只有能够加锁,才执行 if 里的执行体,所以有个 if 判断
        // 等待 2s,如果还不能加锁,就返回 false;
        if(lg.try_lock_for(chrono::seconds(2))) {  
            this_thread::sleep_for(chrono::seconds(1));  // 休眠 1s
            a += 1;  
        }  
    }  
}  
int main()  
{  
    thread t1(func);  
    thread t2(func);  
    t1.join();  
    t2.join();  
    cout << a;   // 执行结果不定,可能为 3
    return 0;  
}

解释可能为 3 的一种情况由来

  • 假设 t1 先执行,然后休眠 1s,然后 t1 又执行,t2 总共等 2s 都没等到,然后返回 false,不执行当前 if 语句内的内容,i++ 然后执行下一次的 for 循环
  • 一次对于 t2 来说就只进行了一次 +1 操作,t1 执行两次,所以返回 3

条件变量

condition_variable

  • std::condition_variable 只能与 std::unique_lock<std::mutex> 配合使用

  • 也就是只能和独占锁一起使用

  • 常用的独占锁:mutextimed_mutex

  • 条件变量 condition_variable 类的 wait() 还有一个重载的方法,可以接受一个条件,这个条件也可以是一个返回值为布尔类型的函数

  • 条件变量会先检查判断这个条件是否满足,如果满足条件(布尔值为 true),则当前线程重新获得互斥锁的所有权,结束阻塞,继续向下执行;如果不满足条件(布尔值为 false),当前线程会释放互斥锁(解锁)同时被阻塞,等待被唤醒

  • 普通的 wait 也是阻塞后都会释放互斥锁,唤醒后就会重新抢占互斥锁,然后执行

condition_variable_any

  • std::condition_variable_any 可以与任何满足 BasicLockable 概念的锁类型一起使用,这包括自定义的锁类型,或其他标准库中的锁(如 std::shared_mutex
  • 比如读写锁,递归锁,信号量等等,都是一些存在共享的锁

上述的阻塞后是用 notify_one()notify_all() 唤醒

生产者消费者问题

void wait(std::unique_lock<std::mutex>& lock
  • 该方法该方法使调用线程进入等待状态,并且自动把传给他的锁解开
  • 当被唤醒时,它会重新锁定互斥锁,然后继续执行
template <class Predicate> void wait(std::unique_lock<std::mutex>& lock, Predicate pred)
  • 这个方法除了执行上述功能外,还会在等待之前和被唤醒之后检查传入的谓词 pred 是否为真
  • 如果谓词为假,线程会继续等待;如果谓词为真,线程会退出等待
  • 这里就可以传一个 bool 值,可以用 lambda 格式写
void notify_one()
void notify_all()
  • 唤醒一个正在等待该条件变量的线程,如果没有线程在等待,该调用会被忽略
  • 唤醒所有正在等待该条件变量的线程,如果没有线程在等待,该调用会被忽略

例子

#include <mutex>  
#include <condition_variable>  
  
queue<int> q;  
condition_variable g_cv;  
mutex mt1;  
bool done = false;  // 用于指示生产者是否完成  
  
void Producer() {  
    for(int i = 1; i <= 10; i++) {  
        // 注意这个大括号不能省略,这表示了锁的局部作用域
        {  
            unique_lock<mutex> lock(mt1);  
            q.push(i);  
            g_cv.notify_one();  
        }  
        this_thread::sleep_for(chrono::microseconds(100));  
    }  
    // 生产完成  
    {  
        unique_lock<mutex> lock(mt1);  
        done = true;  
        g_cv.notify_all(); // 通知所有等待的消费者  
    }  
}  
  
void Consumer() {  
    while(true) {  
        unique_lock<mutex> lock(mt1);  
        g_cv.wait(lock, []() {  
            return !q.empty() || done; // 当队列不为空或者生产完成时退出等待  
        });  
  
        if (!q.empty()) {  
            int value = q.front();  
            q.pop();  
            cout << "Consumed: " << value << endl; // 输出消费的值  
        }  
  
        // 如果生产已经完成并且队列为空,退出循环  
        if (done && q.empty()) {  
            break;  
        }  
    }  
}  
  
int main() 
{  
    thread t1(Producer);  
    thread t2(Consumer);  
    t1.join();  
    t2.join();  
    return 0;  
}

原子变量

  • 可以指定 boolcharintlong指针 等类型作为模板参数(不支持浮点类型和复合类型)
  • 设置了一个变量为原子变量后,就不需要互斥锁的实现了,因为内部实现了冲突的解决,也会等待,但是比互斥锁要好,它本质是一种原子操作
  • 原子操作指的是不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换,而互斥锁使用需要大量上下文切换,所以性能比互斥锁好很多

构造函数

// ①
atomic() noexcept = default;
// ②
constexpr atomic( T desired ) noexcept;
// ③
atomic( const atomic& ) = delete;

构造函数①:默认无参构造函数。 构造函数②:使用 desired 初始化原子变量的值。 构造函数③:使用=delete 显示删除拷贝构造函数, 不允许进行对象之间的拷贝

常用成员

别名原始类型定义
atomic_bool (C++11)std::atomic<bool>
atomic_char (C++11)std::atomic<char>
atomic_schar (C++11)std::atomic<signed char>
atomic_uchar (C++11)std::atomic<unsigned char>
atomic_short (C++11)std::atomic<short>
atomic_ushort (C++11)std::atomic<unsigned short>
atomic_int (C++11)std::atomic<int>
atomic_uint (C++11)std::atomic<unsigned int>
atomic_long (C++11)std::atomic<long>
atomic_ulong (C++11)std::atomic<unsigned long>
atomic_llong (C++11)std::atomic<long long>
atomic_ullong (C++11)std::atomic<unsigned long long>
atomic_char8_t (C++20)std::atomic<char8_t>
atomic_char16_t (C++11)std::atomic<char16_t>
atomic_char32_t (C++11)std::atomic<char32_t>
atomic_int m_value = 0;
int temp = m_value.load();   // 获取原子变量的值
m_value.store(10);           // 修改原子变量的值

// 但是原子变量内部也封装了操作符
// ++, += -- -=
m_value++, m_value--, m_value += 10;

// 但是其他复杂操作就需要提取出来这个值操作

使用

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

struct Counter {
    void increment()
    {
        for (int i = 0; i < 10; ++i)
        {
            int current_value = m_value.load(); // 显式加载当前值
            if (current_value < 5) // 如果当前值小于 5,则递增
            {
                m_value.store(current_value + 1); // 显式存储新的值
                cout << "increment to: " << m_value.load()
                     << ", threadID: " << this_thread::get_id() << endl;
            }
            this_thread::sleep_for(chrono::milliseconds(500));
        }
    }

    void decrement()
    {
        for (int i = 0; i < 10; ++i)
        {
            int current_value = m_value.load(); // 显式加载当前值
            if (current_value > -5) // 如果当前值大于 -5,则递减
            {
                m_value.store(current_value - 1); // 显式存储新的值
                cout << "decrement to: " << m_value.load()
                     << ", threadID: " << this_thread::get_id() << endl;
            }
            this_thread::sleep_for(chrono::milliseconds(500));
        }
    }
    
    atomic_int m_value = 0;
};

int main()
{
    Counter c;
    thread t1(&Counter::increment, &c);
    thread t2(&Counter::decrement, &c);
    t1.join();
    t2.join();
    return 0;
}

call_once( )

在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用 std::call_once() 来保证函数在多线程环境下只能被调用一次。使用 call_once() 的时候,需要一个 once_flag 作为 call_once() 的传入参数

// 定义于头文件 <mutex>
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

flagonce_flag 类型的对象,要保证这个对象能够被多个线程同时访问到 f:回调函数,可以传递一个有名函数地址,也可以指定一个匿名函数 args:作为实参传递给回调函数

示例

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

once_flag g_flag;
void do_once(int a, string b)
{
    cout << "name: " << b << ", age: " << a << endl;
}

void do_something(int age, string name)
{
    static int num = 1;
    call_once(g_flag, do_once, 19, "luffy");
    cout << "do_something() function num = " << num++ << endl;
}

int main()
{
    thread t1(do_something, 20, "ace");
    thread t2(do_something, 20, "sabo");
    thread t3(do_something, 19, "luffy");
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

输出:

name: luffy, age: 19
do_something () function num = 1
do_something () function num = 2
do_something () function num = 3

线程池

注意:这个是线程池简略版本,详细版本: https://xingzhu.top/archives/c-xian-cheng-chi-tong-bu

  • 线程池是提前开辟好的多个线程组成的
  • 任务是开启后,等着任务进来执行任务
  • 因为线程的开辟和销毁十分耗时间

因此线程池需要具备一个线程数组,一个任务队列 生产者往里面加任务,线程池维护线程数组去任务队列取任务

// 完整代码,无解析版本
// 解析版本在下面
#include<vector>  
#include<functional>  
#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <queue>  

class ThreadPool{  
public:  
    ThreadPool(int numThreads) :stop(false) {  
        for (int i = 0; i < numThreads; i++) {  
            threads.emplace_back([this] {  
                while (true) {  
                    std::unique_lock <std::mutex> lock(mtx);  
                    condition.wait(lock, [this] {  
                        return !tasks.empty() || stop;  
                        });  
  
                    if (stop && tasks.empty())  
                        return;  
                    std::function<void()> task(std::move(tasks.front()));  
                    tasks.pop();  
                    lock.unlock();  
                    task();  
                }  
            });  
        }  
    }  
    ~ThreadPool(){  
        {  
            std::unique_lock<std::mutex> lock(mtx);  
            stop = true;  
        }  
        condition.notify_all();  
        for(auto  &t : threads) {  
            t.join();  
        }  
    }  
    template<class F, class... Args>  
  
    void enqueue(F &&f, Args&&... args){  
        std::function<void()>task =  
                std::bind(std::forward<F>(f), std::forward<Args>(args)...);  
        {  
            std::unique_lock<std::mutex> lock(mtx);  
            tasks.emplace(std::move(task));  
        }  
        condition.notify_one();  
    }  
  
private:  
    std::vector<std::thread> threads;  // 线程数组
    std::queue<std::function<void()>> tasks;  // 任务队列,包装的任务函数
    std::mutex mtx;      // 互斥信号量,互斥访问任务队列
    std::condition_variable condition;  
    bool stop;  
};  

int main()  
{  
    ThreadPool pool(4);  
    for(int i = 0; i < 10; i++) {  
        pool.enqueue([i] {  
            std::cout << "task : " << i <<" " << "is running" << std::endl;  
            std::this_thread::sleep_for(std::chrono::seconds(1));  
            std::cout << "task : " << i <<" " << "is over" << std::endl;  
        });  
    }  
}

详细解析版本的线程池

// 有详细解析版本的线程池
#include<vector>  
#include<functional>  
#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
#include <queue>  
  
class ThreadPool{  
public:  
    // numThreads 是线程数量,由用户指定,因此作为传参
    ThreadPool(int numThreads) :stop(false) {  
        for (int i = 0; i < numThreads; i++) {  
            // emplace_back 比 push_back 更节省资源,一致的作用
            // 因为 vector 包裹的函数,这里使用 lambda 表达式实现 
            threads.emplace_back([this] {  
                while (true) {  
                    std::unique_lock <std::mutex> lock(mtx);  
                    // 决定什么时候去任务,使用条件变量
                    // false 就阻塞在这
                    condition.wait(lock, [this] {  
                        // 队列不空或者线程已经被终止了,就不阻塞了
                        return !tasks.empty() || stop;  
                        });  
  
                    if (stop && tasks.empty())  
                        return;  
                    // 这里是移动语义,将任务队列头部的任务使用权交给 tash
                    // 这里使用 move 是为了节省资源,使用赋值拷贝会多申请空间
                    std::function<void()> task(std::move(tasks.front()));  
                    tasks.pop();  
                    // 手动解锁,虽然这个作用域结束会自动解锁
                    // 但是不知道 task() 执行多久,防止其他线程迟迟拿不到任务队列的任务
                    // 这样手动解锁,其他线程就可以取任务队列的任务了 
                    lock.unlock(); 
                    task();  
                }  
            });  
        }  
    }  
    ~ThreadPool(){  
        {  
            // 加锁是因为 stop 是所有线程的共享变量
            std::unique_lock<std::mutex> lock(mtx);  
            stop = true;  
        }  
        // 通知所有线程,把任务队列里的任务取完
        // 因为线程对象析构,说明线程生命周期结束,简洁说明加任务的执行部分结束
        // 比如下面的 main 函数执行完,对象销毁,此时加任务的 for 循环也执行完毕
        condition.notify_all();  
        // 等待所有的线程执行完毕
        for(auto  &t : threads) {  
            t.join();  
        }  
    }  
    // 任务队列的实现
    template<class T, class... Args>  
    // Args&&... args 是可变列表,就是任意数量各种类型参数
    // 如果传来的参数是右值,就是右值引用,如果是左值,就是左值引用
    // std::function<返回值类型(参数类型列表)> fun_name = 可调用对象,这是语法规则
    // 由于是可变列表,不知道参数类型和个数,使用 bind 将其和 F &&f 绑定,返回值就是 function 包装器
    // auto f = std::bind(可调用对象地址, 绑定的参数/占位符); bind 语法
    // 原本是 bind(f, args),但是考虑一个问题
    // 就是 F&&f 会将传来的不管是左值,还是右值,经过 F &&f 处理后,f 都变成了左值引用
    // 这不是我们想看到的,这就使用 forward 完美转发
    // std::forward<T>(t);  语法规则
    //  当 T 为左值引用类型时,t 将被转换为 T 类型的左值
    // 当 T 不是左值引用类型时,t 将被转换为 T 类型的右值
    void enqueue(T &&f, Args&&... args){  
        std::function<void()>task =  
                std::bind(std::forward<T>(f), std::forward<Args>(args)...); 
        {  
            // 放任务加锁,这个大括号就是局部作用域,结束后就解锁
            std::unique_lock<std::mutex> lock(mtx);  
            tasks.emplace(std::move(task));  
        }  
        condition.notify_one();    
    }  
  
private:   
    std::vector<std::thread> threads;      // 线程数组
    std::queue<std::function<void()>> tasks;  // 任务队列,包装的任务函数
    std::mutex mtx;                        // 互斥信号量,互斥访问任务队列
    std::condition_variable condition;    // 条件变量,通知哪些线程完成任务
    bool stop;        // 线程终止
};  
  
int main()  
{  
    // 使用线程池
    ThreadPool pool(4);  
    for(int i = 0; i < 10; i++) {  
        // lambda 表达式表示任务函数,自动类型推导类型 T,无需指定
        pool.enqueue([i] {  
            std::cout << "task : " << i <<" " << "is running" << std::endl;  
            std::this_thread::sleep_for(std::chrono::seconds(1));  
            std::cout << "task : " << i <<" " << "is over" << std::endl;  
        });  
    }  
}

异步并发

异步就是不会出现死等情况,发起任务后,干其他事情去了,这个任务执行后,通知进程

future

想要在主线中得到某个子线程任务函数返回的结果

// ①
future() noexcept;
// ②
future( future&& other ) noexcept;
// ③
future( const future& other ) = delete;
  • 构造函数①:默认无参构造函数
  • 构造函数②:移动构造函数,转移资源的所有权
  • 构造函数③:使用=delete 显示删除拷贝构造函数, 不允许进行对象之间的拷贝

成员函数

T get();
T& get();
void get();

void wait() const;

template< class Rep, class Period >
std::future_status wait_for( const std::chrono::duration<Rep,Period>& timeout_duration ) const;

template< class Clock, class Duration >
std::future_status wait_until( const std::chrono::time_point<Clock,Duration>& timeout_time ) const;

wait_until()wait_for() 函数返回之后,并不能确定子线程当前的状态,因此我们需要判断函数的返回值,这样就能知道子线程当前的状态了:

常量解释
future_status::deferred子线程中的任务函仍未启动
future_status::ready子线程中的任务已经执行完毕,结果已就绪
future_status::timeout子线程中的任务正在执行中,指定等待时长已用完

以下实现中使用 ref() 原因是都删除了拷贝构造,因此只能传引用或者移动构造

promise

成员函数

// 获取内部管理的 future 对象
std::future<T> get_future();

存储要传出的 value 值,并立即让状态就绪,这样数据被传出其它线程就可以得到这个数据了

void set_value( const R& value );
void set_value( R&& value );
void set_value( R& value );
void set_value();   // 为 promise<void>; 准备的

存储要传出的 value 值,但是不立即令状态就绪。在当前线程退出时,子线程资源被销毁,再令状态就绪

void set_value_at_thread_exit( const R& value );
void set_value_at_thread_exit( R&& value );
void set_value_at_thread_exit( R& value );
void set_value_at_thread_exit();

使用

通过 promise 传递数据的过程一共分为 5步:

  1. 在主线程中创建 std::promise 对象
  2. 将这个 std::promise 对象通过引用的方式传递给子线程的任务函数
  3. 在子线程任务函数中给 std::promise 对象赋值
  4. 在主线程中通过 std::promise 对象取出绑定的 future 实例对象
  5. 通过得到的 future 对象取出子线程任务函数中返回的值

用于在一个线程中产生一个值,并在另一个线程中获取这个值

void func(std::promise<int> &f) {  
    f.set_value(1000);  
}  
int main() {  
    std::promise<int> f;  
    auto future_result = f.get_future();  
    std::thread t1(func, ref(f));  
    t1.join();  
    cout << future_result.get();  
    return 0;  
}
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main()
{
    promise<int> pr;
    thread t1([](promise<int> &p) {
        p.set_value_at_thread_exit(100);
        this_thread::sleep_for(chrono::seconds(3));
        cout << "睡醒了...." << endl;
    }, ref(pr));

    future<int> f = pr.get_future();
    int value = f.get();
    cout << "value: " << value << endl;
    t1.join();
    return 0;
}

packaged_task

  • std::packaged_task 类包装了一个可调用对象包装器类对象(可调用对象包装器包装的是可调用对象,可调用对象都可以作为函数来使用)
  • 这个类可以将内部包装的函数和 future 类绑定到一起,以便进行后续的异步调用,它和 std::promise 有点类似,std::promise 内部保存一个共享状态的值,而 std::packaged_task 保存的是一个函数。

构造函数

// ①
packaged_task() noexcept;
// ②
template <class F>
explicit packaged_task( F&& f );
// ③
packaged_task( const packaged_task& ) = delete;
// ④
packaged_task( packaged_task&& rhs ) noexcept;
  • 构造函数①:无参构造,构造一个无任务的空对象
  • 构造函数②:通过一个可调用对象,构造一个任务对象
  • 构造函数③:显示删除,不允许通过拷贝构造函数进行对象的拷贝
  • 构造函数④:移动构造函数

使用

packaged_task 其实就是对子线程要执行的任务函数进行了包装,和可调用对象包装器的使用方法相同,包装完毕之后直接将包装得到的任务对象传递给线程对象就可以了

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

int main()
{
    packaged_task<int(int)> task([](int x) {
        return x += 100;
    });

    thread t1(ref(task), 100);

    future<int> f = task.get_future();
    int value = f.get();
    cout << "value: " << value << endl;

    t1.join();
    return 0;
}
  • std::thread 可以直接使用 packaged_task
  • 在这个例子中,std::thread 的构造函数可以直接接受一个可调用对象,包括 packaged_task
  • thread 构造时,将 task 和参数 100 直接传给了 std::thread,并且 taskthread t1(ref(task), 100) 中直接被调用
  • std::thread 可以直接接受 packaged_task,因为线程构造函数知道如何调用这个可调用对象,并且会立即执行它
  • 但是在线程池中,如果任务队列是个可调用对象包装器,就不能直接使用这个,需要先用 lambda 包装成为一个匿名仿函数,再传入任务队列中

也可使用移动语义

#include <future>

int func() {  
    int res = 0;  
    for(int i = 0; i < 1000; i++) {  
        res ++;  
    }  
    return res;  
}  
int main() 
{  
    std::packaged_task<int()> task(func);  
    auto future_result = task.get_future();  
  
    std::thread t1(std::move(task));  
    cout << func()<< endl;  
    t1.join();  
    cout << future_result.get() << endl;  
}

async

  • std::async 函数比前面提到的 std::promisepackaged_task 更高级一些,因为通过这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数
  • 异步任务执行完成返回的结果也是存储到一个 future 对象中,当需要获取异步任务的结果时,只需要调用 future 类的 get() 方法即可
  • 如果不关注异步任务的结果,只是简单地等待任务完成的话,可以调用 future 类的 wait() 或者 wait_for() 方法

构造函数

async( Function&& f, Args&&... args );
async( std::launch policy, Function&& f, Args&&... args );
// 返回值是一个 future 对象
  • f:可调用对象,这个对象在子线程中被作为任务函数使用
  • Args:传递给 f 的参数(实参)
  • policy:可调用对象 f 的执行策略
策略说明
std::launch:: async调用 async 函数时创建新的线程执行任务函数
std::launch::deferred调用 async 函数时不执行任务函数,直到调用了 future 的 get()或者 wait()时才执行任务(这种方式不会创建新的线程)

使用

调用 async()函数直接创建线程执行任务

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

int main()
{
    cout << "主线程ID: " << this_thread::get_id() << endl;
    // 调用函数直接创建线程执行任务
    future<int> f = async([](int x) {
        cout << "子线程ID: " << this_thread::get_id() << endl;
        this_thread::sleep_for(chrono::seconds(5));
        return x += 100;
    }, 100);

    future_status status;
    do {
        status = f.wait_for(chrono::seconds(1));
        if (status == future_status::deferred)
        {
            cout << "线程还没有执行..." << endl;
            f.wait();
        }
        else if (status == future_status::ready)
        {
            cout << "子线程返回值: " << f.get() << endl;
        }
        else if (status == future_status::timeout)
        {
            cout << "任务还未执行完毕, 继续等待..." << endl;
        }
    } while (status != future_status::ready);

    return 0;
}
  • 其实直接调用 f.get() 就能得到子线程的返回值
  • 这里演示一下 wait_for 使用

输出

主线程ID: 8904
子线程ID: 25036
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
子线程返回值: 200

调用 async()函数不创建线程执行任务

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

int main()
{
    cout << "主线程ID: " << this_thread::get_id() << endl;
    // 调用函数直接创建线程执行任务
    future<int> f = async(launch::deferred, [](int x) {
        cout << "子线程ID: " << this_thread::get_id() << endl;
        return x += 100;
    }, 100);

    this_thread::sleep_for(chrono::seconds(5));
    cout << f.get();
    return 0;
}
主线程ID: 24760
主线程开始休眠5...
子线程ID: 24760
200

总结

  • 使用 async() 函数,是多线程操作中最简单的一种方式,不需要自己创建线程对象,并且可以得到子线程函数的返回值
  • 使用 std::promise 类,在子线程中可以传出返回值也可以传出其他数据,并且可选择在什么时机将数据从子线程中传递出来,使用起来更灵活
  • 使用 std::packaged_task 类,可以将子线程的任务函数进行包装,并且可以得到子线程的返回值

说明: 参考学习: https://subingwen.cn/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值