C++多线程相关库万字总结【C++11后】

文章目录

std::thread

<thread>中定义。线程std::thread在构造关联的线程对象时立即开始执行,从提供给作为构造函数参数的顶层函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用std::terminate 。

构造函数

构造定义
默认构造函数thread() noexcept;
初始化构造函数template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args);
拷贝构造函数 [deleted]thread(const thread&) = delete;
Move 构造函数thread(thread&& x) noexcept;

初始化构造函数:新产生的线程会调用 Fn 函数,该函数的参数由 args 给出。

Move构造会将控制权转移,之后x将不代表任何线程

可调用对象

第一参数Fn的类型并不是c语言中的函数指针(c语言传递函数都是使用函数指针),在c++11中,增加了可调用对象(Callable Objects)的概念,总的来说,可调用对象可以是以下几种情况:

  1. 函数指针

    // 普通函数 无参
    void function_1() {}
    // 普通函数 1个参数
    void function_2(int i) {}
    // 普通函数 2个参数
    void function_3(int i, std::string m) {}
     
    std::thread t1(function_1);
    std::thread t2(function_2, 1);
    std::thread t3(function_3, 1, "hello");
    
    //注意如果函数存在重载,需要使用额外定义的函数指针进行准确指定
    void fun(){}
    void fun(int i){}
    
    void (*p_fun1)() = fun;
    void (*p_fun2)(int) = fun;
    //std::thread t1(fun);  //重载直接使用原函数名会无法识别使用哪个
    std::thread t2(p_fun1);
    std::thread t3(p_fun2,3);
    
  2. 重载了operator()运算符的类对象,即仿函数

    // 仿函数
    class Fctor {
    public:
        void operator() () {}
    };
    Fctor f;
    
    std::thread t1(f);  
    // std::thread t2(Fctor()); // 编译错误 Fctor()表示创建临时对象
    std::thread t3((Fctor())); // 通过加一个额外的括号来消除歧义
    std::thread t4{Fctor()}; // ok
    //事实上当operator()有参数时,Fctor()就只会解释为临时对象了
    
  3. lambda表达式(匿名函数)

    std::thread t1([](){
        std::cout << "hello" << std::endl;
    });
     
    std::thread t2([](std::string m){
        std::cout << "hello " << m << std::endl;
    }, "world");
    
  4. std::function

    class A{
    public:
        void func1(){}
        void func2(int i){}
        void func3(int i, int j){}
    };
     
    A a;
    std::function<void(void)> f1 = std::bind(&A::func1, &a);
    std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
    std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
    std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
    std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);
     
    std::thread t1(f1);
    std::thread t2(f2);
    std::thread t3(f3, 1);
    std::thread t4(f4, 1);
    std::thread t5(f5, 1, 2);
    

引用传递

线程入口函数Fn的参数是引用类型时,args不应直接传递变量,应使用std::ref

void fun(int& x) {}
int x = 3;
//std::thread t1(fun, x);  //部分编译器会直接报错,就算编译通过也不会改变x的值
std::thread t2(fun, std::ref(x));

但是依然不推荐使用引用传递,这样做会导致多个线程对同一变量的读写

同理,对于Fn本身,其事实上也是拷贝的,因此会有如下一些情况

class A {
public:
    void f(int x, char c) {}
    int g(double x) {return 0;}
    int operator()(int N) {return 0;}
};

A a;
std::thread t1(a, 6); // 调用的是 copy_of_a()
std::thread t2(std::ref(a), 6); // a()
std::thread t3(A(), 6); // 调用的是 临时对象 temp_a()
std::thread t4(&A::f, a, 8, 'w'); // 调用的是 copy_of_a.f()
std::thread t5(&A::f, &a, 8, 'w'); // 调用的是 a.f()
std::thread t6(std::move(a), 6); // 调用的是 a.f(), a不能够再被使用了
// 以上中的拷贝都不会实际改变a中的成员变量

线程对象不可拷贝

void some_function();
void some_other_function();

std::thread t1(some_function);
// std::thread t2 = t1; // 编译错误
std::thread t2 = std::move(t1); //只能移动 移动完成后线程控制权转移,t1不再管理任何线程
t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作
std::thread t3;
t3 = std::move(t2); // t2内部已经没有线程了
t1 = std::move(t3); // 程序将会终止,因为t1内部已经有一个线程在管理了

成员函数

void jion()

阻塞等待该线程执行完成,在thread析构前应当尽可能执行一次(或者detach)

thread::id get_id()

获取线程 ID

bool joinable()

检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。另外,如果某个线程已经执行完任务,但是没有被 join 的话,该线程依然会被认为是一个活动的执行线程,因此也是可以被 join 的。

void detach()

将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。这之后joinable() == false。一旦detach()之后,与这个主线程关联的thread对象就会失去与这个主线程的关联,此时这个子线程就会驻留在后台运行,这个子线程就相当于被c++运行时库接管,当这个子线程执行完成后,由运行时库负责清理该线程相关的资源。

void swap(std::thread)

交换两个线程对象所代表的底层句柄(underlying handles)。

std::this_thread

<thread>库中存在std::this_thread命名空间提供一些全局函数

thread::id get_id()

获取当前运行线程ID

void yield()

当前线程主动让出时间片,操作系统调度另一线程继续执行。

void sleep_until(const std::chrono::time_point &sleep_time )

线程休眠至sleep_time指定的时刻,之后被唤醒

void sleep_for(const chrono::duration &rtime)

线程休眠指定的时间rtime,该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能更长一点。

mutex互斥量库

std::mutex

C++11中在<mutex>中提供了互斥量,共包含四种类型:
std::mutex:最基本的mutex类。
std::recursive_mutex:递归mutex类,能多次锁定而不死锁。
std::time_mutex:定时mutex类,可以锁定一定的时间。
std::recursive_timed_mutex:定时递归mutex类。

其中mutex用于最基本的上锁解锁操作

成员函数

定义说明
void lock()阻塞调用线程,直到线程获取 mutex 的所有权。
bool try_lock()在不阻塞的情况下尝试获取 mutex 的所有权。
void unlock()释放 mutex 的所有权。
// cout不是线程安全的 需要使用锁保护
std::mutex mu;
void shared_print(string msg, int id) {
    mu.lock(); // 上锁
    cout << msg << id << endl;
    mu.unlock(); // 解锁
}

事实上上述函数是存在风险的,如果在unlock()前发生异常,会导致锁没有释放。该问题可以使用std::lock_guard类解决。

std::recursive_mutex

该锁使用时与mutex使用方法一直,为了解决递归导致的自锁现象:

//std::mutex mt;
std::recursive_mutex mt;
void Fun(){
	mt.lock();
	... 
	Fun(); // 发生递归 mutex会导致下次再lock()时被阻塞 recursive_mutex则不会
	mt.unlock();
}

调用方线程在从它成功调用 lock 或 try_lock 开始的时期里占有 recursive_mutex 。此时期间,调用方线程可以多次锁定/解锁互斥元。结束的时候lock与unlock次数匹配正确就行。

std::time_mutex与recursive_timed_mutex

mutex或者recursive_mutex基础上提供计时功能,即允许以一定的时间成本尝试获得锁。其增加了如下两个成员函数:

定义说明
bool try_lock_for(const std::chrono::duration& timeout_duration)timeout_duration时间内尝试获得锁,超时不会阻塞。
bool try_lock_until(const std::chrono::time_point& timeout_time)在时间点timeout_time前尝试获得锁,超时后不会阻塞。

std::lock_guard

为了解决上锁之后,因为出现异常从而导致无法解锁而提出,用于管理一个锁。其使用方法如下:

std::mutex mu;
void shared_print(string msg, int id) {
    //构造的时候帮忙上锁,析构的时候释放锁
    std::lock_guard<std::mutex> guard(mu);
    //mu.lock(); // 上锁
    cout << msg << id << endl;
    //mu.unlock(); // 解锁
}

构造与析构函数

lock_guard存在两种构造函数

构造函数说明
explicit lock_guard(mutex_type& Mtx)在构造的同时进行上锁操作。如果是非递归锁,不要提前调用lock()
lock_guard(mutex_type& Mtx, adopt_lock_t)构造时不再上锁

lock_guard在析构函数中会自动解锁。

std::unique_lock

unique_lock提供了比lock_guard更多的功能。相较于lock_guard只能在析构时解锁,unique_lock可以选择任意时刻解锁,从而降低锁粒度。

构造函数与析构

其拥有的构造函数如下

unique_lock() noexcept; // 部管理任何锁
unique_lock(unique_lock&& Other) noexcept; // 移动构造
explicit unique_lock(mutex_type& Mtx); //构造时使用lock()上锁
unique_lock(mutex_type& Mtx, adopt_lock_t Adopt); // 表示构造前已经上锁了
unique_lock(mutex_type& Mtx, defer_lock_t Defer) noexcept;// 构造时不自动上锁
unique_lock(mutex_type& Mtx, try_to_lock_t Try); // 构造时使用try_lock()上锁
unique_lock(mutex_type& Mtx, const chrono::duration Rel_time);// 构造时使用try_clock_for(Rel_time)上锁
unique_lock(mutex_type& Mtx, const chrono::time_point Abs_time);// 构造时使用try_clock_until(Abs_time)上锁

析构函数会自动解锁

成员函数

函数名说明
viod lock()阻止调用线程,直到线程获取关联的 mutex 的所有权。
mutex_type *mutex()获得指向关联的 mutex 的存储指针。
bool owns_lock()调用线程是否拥有关联的 mutex。
void release()解除 unique_lock 对象与关联的 mutex 对象的关联。
void swap(unique_lock&) 将关联的 mutex 和所有权状态与指定对象的互换。
bool try_lock()调用锁的try_lock()函数
bool try_lock_for(...)调用锁的try_lock_for()函数
bool try_lock_until(...)调用锁的try_lock_until()函数
void unlock()解锁
explicit operator bool()转换为一个bool,值为owns_lock()结果

std::scoped_lock

C++17引入的一种同时管理多个mutex并防止死锁的包装器,当只有一个时等效于lock_guard

构造函数与析构

scoped_lock存在两种构造函数

构造函数说明
explicit scoped_lock(_MutexTypes&... __m)在构造的同时对所有传入的锁进行上锁操作,并且防止死锁发生。
explicit scoped_lock(adopt_lock_t, _MutexTypes&... __m)构造时不再上锁

析构函数会逆序释放所有锁

全局函数

void call_once(once_flag& Flag, Callable F&&, Args&&... A)

提供在执行期间只调用一次指定的可调用对象的机制。

Flagonce_flag,其应但是一个全局唯一的变量,函数依靠其判断是否被调用过。

F是一个可调用对象,Args是参数

//一种单例的实现方式
class Singlton{
private:
    CSinglton() {} // 私有额构造函数
    ~CSinglton() {}// 在析构函数中释放实例对象 这里省略了    
public:
    static CSinglton* GetInstance(){// 唯一全局访问点
        if (NULL == pInstance) { // 若实例不存在,则尝试创建实例对象
            try { // 使用call_once保证只创建一次
                std::call_once(m_flag, CreateInstance);
            }catch (...) {
                std::cout << "CreateInstance error\n";
            }
        }
        return pInstance;
    }
    static void CreateInstance(){ } // 创建对象

private:
    static CSinglton* pInstance; // 唯一实例对象
    static std::once_flag m_flag; // 保证全局唯一
};
CSinglton* CSinglton::pInstance = NULL;
std::once_flag CSinglton::m_flag;

注意call_once会保证可调用对象正常执行完毕,即在第一次调用还没有完成时,后续其他线程若再次运行到此处则会被阻塞直到第一次调用完成。但是若可调用对象发生异常,会导致后续线程一直阻塞。

void lock(L1&, L2&, L3&...)

尝试在不死锁的情况下锁定所有自变量,会阻塞线程。

int try_lock(L1&, L2&, L3&...)

尝试在不死锁的情况下锁定所有自变量,不会阻塞。成功时返回-1,失败时会返回失败的mutex索引(以0为起始)。

void swap(unique_lock& x, unique_lock& y)

交换两个锁的内容

shared_mutex共享互斥量库

在C++14中引入该库,其提供共享mutex,用于保护多个线程共享访问数据。mutex相当于写锁,只能一个线程访问,而shared_mutex是读写锁,可以允许多个线程读或一个线程写。

std::shared_mutex

该类在C++17引入,是mutex的共享版本,相较于mutex其额外提供了如下函数用于读锁:

函数名说明
void lock_shared()阻塞线程直到获得其读取权限,即共享锁。
bool try_lock_shared()尝试获得共享锁,不会阻塞线程。
void unlock_shared()释放共享锁

当线程只会进行读取操作时,就可以使用共享锁,因为多个线程读取不会发生不安全的现象。

std::shared_timed_mutex

shared_mutex的计时版本,与time_mutex相对于mutex是一样的。提供try_lock_shared_for()try_lock_shared_until()

std::shared_lock

unique_lock的共享版本,必须接收一个共享的mutex,其提供的函数与unique_lock完全一样,只不过全部上锁方式都是共享锁。

condition_variable条件变量库

条件变量是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。相较于互斥量,条件变量的重点在于达成某种条件。事实上互斥量是可以模拟出条件变量,只需要不停循环查询条件,而条件变量可以在条件不满足时阻塞,由别的线程在条件满足时唤醒后再查询条件。

std::condition_variable

针对于unique_lock<mutex>的锁使用的条件变量

构造与析构

没有特殊的构造函数,不可以被复制构造,不可以复制赋值

通知函数

名称描述
void notify_one()通知任意一个等待的线程(一般是按队列顺序)
void notify_all()通知所有等待的线程

通知函数用于去唤醒那些被阻塞的线程进行条件判断。

等待函数

名称描述
void wait(unique_lock& Lck)阻塞线程,直到条件变量被唤醒
void wait(unique_lock& Lck, Predicate Pred)阻塞线程,当条件变量被唤醒时运行可调用对象Pred,返回为true则继续执行,false则继续阻塞。
cv_status wait_for(unique_lock& Lck, const chrono::duration& Rel_time)阻塞线程,当条件变量被唤醒时返回cv_status:: no_timeout,或到Rel_time时限后返回cv_status::timeout
bool wait_for(unique_lock<mutex>& Lck, const chrono::duration& Rel_time, Predicate Pred)阻塞线程,当条件变量被唤醒时对Pred进行判断,或到Rel_time时限返回Pred的结果
cv_status wait_until(unique_lock& Lck, const chrono::time_point& Abs_time)阻塞线程,当条件变量被唤醒时返回cv_status:: no_timeout,或到Abs_time时刻后返回cv_status::timeout
bool wait_until(unique_lock& Lck, const chrono::time_point& Abs_time, Predicate Pred)阻塞线程,当条件变量被唤醒时对Pred进行判断,或到Abs_time时刻返回Pred的结果

示例与说明

std::mutex m; // 锁
std::condition_variable cv; // 条件变量
bool ready = false; //  main()是否发送数据
bool processed = false; // worker_thread()是否完成数据处理

void worker_thread() {
    std::unique_lock<std::mutex> lk(m); // wait前需要上锁!!!
    cv.wait(lk, [] { return ready; }); // wait
    processed = true;   // 发送数据回 main() 
    lk.unlock(); // 解锁
    cv.notify_one(); // 通知已完成
}

int main() {
    std::thread worker(worker_thread);
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
    } // 使用代码块限制lock_guard作用范围
    cv.notify_one();
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, [] { return processed; });
    }
    worker.join();
}

std::condition_variable_any

相对于condition_variablecondition_variable_any不局限于unique_lock<mutex>,其他拥有lock()unlock()方法的都可以,如std::shared_lock 。提供的方法是相同的。

在C++20之后,std::condition_variable_any中所有wait相关函数都添加了stop_token版本用于取消等待条件:

bool wait( Lock& lock, std::stop_token stoken, Predicate pred )

使得若在给定的 stoken 的关联停止状态上作出停止请求时接收到提醒,从而在被唤醒后不考虑pred直接退出。返回值为pred的结果。

全局函数

void notify_all_at_thread_exit( std::condition_variable& cond, std::unique_lock<std::mutex> lk )

该函数用于线程结束时提醒条件变量的所有其他线程。其效果等价于:

lk.unlock(); cond.notify_all();

atomic原子操作库

原子操作是C++提供的可线程安全的访问指定变量的模板类。

std::atomic<T>

std::atomic 模板可用任何满足可复制构造、可复制赋值、可平凡复制的类型T特化。一般来讲T应当是底层连续的内存空间、存在拷贝构造、不包含虚函数、不包含noexcept的构造函数。

对于基础类型,atomic是有其特化版本,可以提供原本的常见操作,如++、–等操作:

// 指针类型拥有特化版本
std::atomic<U*>;
// 几乎所有的整形都有特化版本,并且有其别名
typedef atomic<bool> atomic_bool;
typedef atomic<char> atomic_char;
typedef atomic<int> atomic_int;
...
// C++20提供了浮点类型的特化
std::atomic<float>;
std::atomic<double>;
// 同时,C++20提供了std::shared_ptr和std::weak_ptr的特化版本
std::atomic<std::shared_ptr>;
std::atomic<std::weak_ptr>;

构造与析构

函数名说明
atomic()默认构造函数。该函数在C++20前采用内存刷0的方式进行初始化;而C++20之后是调用T的默认构造函数,如没有会有编译错误。注意默认构造函数的调用不是原子的。
atomic(T desired)desired的值初始化原子,这个初始化过程不是原子的。

基本成员函数

以下成员函数是所有版本共有的函数

函数名说明
T operator=(T desired)
atomic_flag& operator=(const atomic_flag&) = delete;
向原子对象赋值
bool is_lock_free()检查原子对象是否免锁
void store(T desired)原子地以非原子对象替换原子对象的值。事实上该函数有第二个参数用于控制内存序。
T load()原子地加载并返回原子变量的当前值。该函数有参数用于控制内存序。
operator T()从原子对象加载值
T exchange(T desired)desired赋值给原子对象,并返回旧值
bool compare_exchange_weak(T& expected, T desired)
bool compare_exchange_strong( T& expected, T desired)
比较*thisexpected,而若它们逐位相等,则以desired替换前者。否则,将*this中的实际值加载进expected。该比较直接比较内存,而不是==。该函数主要用于自旋锁,一般weak版本有更好的性能,但偶尔可能会发生假false。
void wait(T old)C++20引入,阻塞线程,直到被唤醒并且其值不等于old,该比较直接对内存进行比较。
void notify_one()C++20引入,唤醒至少一个在原子对象上的等待中阻塞的线程
void notify_all()C++20引入,唤醒所有在原子对象上的等待中阻塞的线程

特化成员函数

函数名说明
T fetch_add(T arg)
T* fetch_add(std::ptrdiff_t arg)
整形、浮点型(C++20)特化函数,加arg,返回旧值。
指针特化,指针地址增加,返回旧值。
T fetch_sub(T arg)
T* fetch_sub(std::ptrdiff_t arg)
同上,进行相减操作
T fetch_and(T arg)整形特化函数,按位与arg,返回旧值。
T fetch_or(T arg)同上,按位或
T fetch_xor(T arg)同上,按位异或
operator++
operator--
整形、指针特化函数,操作结果与对应类型操作一致。
operator+=
operator-=
整形、浮点型(C++20)、指针特化函数,操作结果与对应类型操作一致。
operator&=
`operator
=<br/>operator^=`

示例

std::thread aThreads[100];
int main() {
    std::atomic<int> atomicValue(0); // 定义一个整形原子
    auto f = [&](const char *name, int idx) {
        for(int i = 0; i<10000; i++) {
            ++atomicValue; // 原子自增
            std::this_thread::sleep_for(chrono::microseconds(5)); // 使多个线程相互切换
            printf("%s%d: atomicValue %d\n", name, idx, atomicValue.load());
        }
    };
    
    for(int i = 0; i<100; i++) {
        aThreads[i] = std::thread(f, "thread", i);
    }
    
    for(int j = 0; j<100; j++) {
        aThreads[j].join();
    }
    
    printf("atomicValue: %d\n", atomicValue.load());
    return 0;
}

std::atomic_flag

std::atomic_flag是原子布尔类型。不同于所有std::atomic的特化,它保证是免锁的。不同于std::atomic<bool>std::atomic_flag不提供加载或存储操作。atomic_flag可以用于实现自旋互斥。

构造与析构

函数名说明
atomic_flag()默认构造。在C++20前该构造无法确定其值;C++20后默认构造会始终是清除状态(false)。

在C++20前使用std::atomic_flag v = ATOMIC_FLAG_INIT;来保证初始化为清除状态,C++20后依然兼容这种写法。

成员函数

函数名说明
atomic_flag& operator=(const atomic_flag&) = delete;atomic_flag是不可进行赋值的
void clear()设置标志为 false
bool test_and_set()设置标志为 true 并返回旧值
bool test()C++20引入,返回标志的值
void wait(T old)C++20引入,阻塞线程,直到被唤醒并且其值不等于old,该比较直接对内存进行比较。
void notify_one()C++20引入,唤醒至少一个在原子对象上的等待中阻塞的线程
void notify_all()C++20引入,唤醒所有在原子对象上的等待中阻塞的线程

std::atomic_ref

该类在C++20中引入,用于将一个外部对象封装为一个原子。该模板可以用在任何可平凡复制的类型 T(包括 bool)。

构造函数

函数名说明
atomic_ref( T& obj )构造引用对象 objatomic_ref 对象。
atomic_ref(const atomic_ref& ref)构造引用 ref 所引用对象的 atomic_ref 对象

成员函数

atomic_ref的成员函数与atomic一致

全局函数

提供类中的全部函数的全局版本,一般第一个参数为原子类型的指针。

std::memory_order

指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序。在没有任何制约的多处理器系统上,多个线程同时读或写数个变量时,一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实,更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现,因为内存模型允许编译器变换。

// C++20前
typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;
// C++20后
enum class memory_order {
    relaxed, consume, acquire, release, acq_rel, seq_cst
};
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release;
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;
inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;

详细请见

future

提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常。

std::future

提供访问异步操作结果的机制

构造与析构

函数名说明
future()默认构造,构造后valid() == false
future(future&& other)移动构造,构造后 other.valid() == false

不可以拷贝构造,在析构时会放弃其拥有的共享状态,若是最后一个会释放共享状态。

事实上future不会主动进行构造,一般是由std::asyncstd::packaged_taskstd::promise创建。

成员函数

函数名说明
future& operator=(future&& other)移动赋值,赋值后other.valid() == false
std::shared_future<T> share()转移共享状态到 std::shared_future 对象,之后valid() == false
T get()
T& get()
void get()
阻塞直至future拥有合法结果并获取它。调用此函数前valid()应为true,调用此方法后valid()==false 。若future所引用的共享状态中存储异常,则抛出该异常。
后面两个分别为T&void的特化版本
bool valid()检查future是否拥有共享状态。
void wait()阻塞直至结果变得可用。
std::future_status wait_for(const std::chrono::duration& timeout_duration)阻塞结果变得可用,或经过指定的timeout_duration的时间,返回值future_status::ready:共享状态就绪
future_status::timeout:共享状态在经过指定的等待时间内仍未就绪
std::future_status wait_until(const std::chrono::time_point& timeout_time)阻塞结果变得可用,或到达指定的timeout_time的时间点。返回值同上。

valid()返回值为false,则上述函数基本都是无意义的。

std::promise

std::promise提供存储值或异常的设施,之后通过std::promise对象所创建的 std::future 对象异步获得结果。

构造与析构

函数名说明
promise()默认构造,共享状态为空
promise(future&& other)移动构造,构造完毕后,other无共享状态

在析构时,若共享状态未就绪,则存储以 std::future_errc::broken_promiseerror_conditionstd::future_error 类型异常对象,令共享状态就绪再释放它。

成员函数

函数名说明
promise& operator=(promise&& other)移动赋值
void swap(promise& other)交换两个 std::promise 对象的内容
std::future<T> get_future()返回关联同一状态的future对象,若无共享状态或已经调用过该函数,则会抛出异常。
void set_value(const R& value)
void set_value(R&& value)
void set_value(R& value)
void set_value()
原子地存储 value 到共享状态,并令状态就绪。
后两个分别为R&void的特化版本。void不存储值,只会令状态就绪。
void set_value_at_thread_exit(const R& value)
...
原子地存储 value 到共享状态,并在线程结束时使状态就绪。
同上,其也有四个版本。
void set_exception(std::exception_ptr p)存储异常指针 p 到共享状态中,并令状态就绪。
void set_exception_at_thread_exit( std::exception_ptr p)存储异常指针 p 到共享状态中,而不立即使状态就绪。在当前线程退出时,销毁所有拥有线程局域存储期的变量后,再零状态就绪

注意,上述的最后四类set方法,在对象生命周期中只能选择其中一种调用一次。

示例

void add1(int x, std::promise<int> accumulate_promise) {
    accumulate_promise.set_value(x+1);  // future 该类方法只能使用一次
}

int main() {
    // 演示用 promise<int> 在线程间传递结果。
    std::promise<int> apromise;
    std::future<int> afuture = apromise.get_future();
    std::thread work_thread(add1, 5,
    		std::move(apromise));// 注意promise不能拷贝,只能移动
    // future::get() 将等待直至该 future 拥有合法结果并取得它
    std::cout<<"result="<<afuture.get()<<'\n';
    work_thread.join();  // 等待线程结束
}

std::packaged_task

std::packaged_task包装任何可调用对象,使得能异步调用它。其返回值或所抛异常被存储于能通过std::future对象访问的共享状态中。

构造与析构

函数名说明
packaged_task()构造无任务且无共享状态的对象
explicit packaged_task(F&& f)构造拥有共享状态和任务副本的 std::packaged_task 对象,以 std::forward<F>(f) 初始化副本。
packaged_task(packaged_task&& rhs)rhs之前所占有的共享状态和任务构造 std::packaged_task,令rhs留在无共享状态且拥有被移动后的任务的状态。

成员函数

函数名说明
packaged_task& operator=(packaged_task&& other)移动赋值
bool valid()是否拥有共享状态
void swap( packaged_task& other )other 交换的共享状态和存储的任务。
std::future<R> get_future()返回共享同一共享状态的 future
get_future 只能对每个 packaged_task 调用一次。
void operator()(ArgTypes... args)调用存储的任务 f。任务返回值或任何抛出的异常被存储于共享状态。令共享状态就绪,并解除阻塞任何等待此操作的线程。
void make_ready_at_thread_exit( ArgTypes... args)调用存储的任务 f。任务返回值或任何抛出的异常被存储于共享状态。只有在在当前线程退出,并销毁所有线程局域存储期对象后,共享状态才会就绪。
void reset()重置状态,抛弃先前执行的结果。等价于*this = packaged_task(std::move(f))

std::shared_future

std::shared_future 提供访问异步操作结果的机制,类似std::future,除了允许多个线程等候同一共享状态。不同于仅可移动的std::future(故只有一个实例能指代任何特定的异步结果),std::shared_future 可复制而且多个 shared_future 对象能指代同一共享状态。

若每个线程通过其自身的 shared_future 对象副本访问,则从多个线程访问同一共享状态是安全的。

构造与析构

函数名说明
shared_future()默认构造函数。构造空的 shared_future,它不指代共享状态,即 valid() == false
shared_future(const shared_future& other)构造与 other 指代同一共享状态的 shared_future,若有共享状态。
shared_future(std::future<T>&& other)转移other所保有的共享状态给*this。构造后, other.valid() == false
shared_future(shared_future&& other)同上

在析构时,若*this是指代共享状态的最后一个对象,则销毁共享状态,否则不做任何事。

成员函数

shared_future的成员函数与future是相同的,但是如下函数会有所区别:

函数名说明
shared_future& operator=( const shared_future& other )拷贝赋值。shared_future是提供拷贝的,future则没有
const T& get()shared_future只能提供共享状态值的常引用,并且调用get()后,不会使valid()false
T&void的特化版本是没有区别的。

std::async

这是在<future>中提供的全局函数,其定义如下:

std::future<R> async(Function&& f, Args&&... args)

std::future<R> async(std::launch policy, Function&& f, Args&&... args)

std::async 异步地运行函数f(有可能在可能是线程池一部分的分离线程中),并返回最终将保有该函数调用结果的 std::future。其中模板R推导较为复杂,一般推荐直接使用auto接收函数返回值。

参数std::launch policy是用于指定启动策略,其可选掩码如下:

  1. std::launch::async:异步调用。如果policy & std::launch::asyn != 0,函数会异步调用。

  2. std::launch::deferred:惰性求值。如果policy & std::launch::deferred != 0,函数求值会延后到 std::future 上首次调用非定时等待函数,在当前线程(不一定是最初调用 std::async 的线程)中求值。

    实验表明,在gcc中std::launch::async的策略会掩盖std::launch::deferred

semaphore信号量库

该库在C++20引入。信号量 (semaphore) 是一种轻量的同步原件,用于制约对共享资源的并发访问。在可以使用两者时,信号量能比条件变量更有效率。

std::counting_semaphore<LeastMaxValue>

counting_semaphore 是一个轻量同步元件,能控制对共享资源的访问。不同于std::mutexcounting_semaphore 允许同一资源有多于一个同时访问,至少允许 LeastMaxValue 个同时的访问。

构造与析构

explicit counting_semaphore(std::ptrdiff_t desired)

构造一个 std::counting_semaphore 类型对象,初始化其计数器的值为 desired

析构时应保证counting_semaphore没有在使用,尤其是没有线程阻塞在其中。

成员函数

函数名说明
void release(std::ptrdiff_t update = 1)内部计数器的值增加 update。任何等待计数器大于 0 的线程都会因此来竞争信号量
void acquire()若内部计数器大于0则尝试将它减少1,否则阻塞直至内部计数器大于0且能成功减少。
bool try_acquire()内部计数器大于0则尝试将它减少1,该过程不会被阻塞。返回是否成功减少。
bool try_acquire_for(const std::chrono::duration& rel_time)若内部计数器大于0则尝试将它减少1,否则阻塞直至它大于0且能成功地减少,或已经经过rel_time的时间。返回是否成功减少。
bool try_acquire_until(const std::chrono::time_point& abs_time)若内部计数器大于0则尝试将它减少1,否则阻塞直至它大于0且能成功地减少,或已经超出abs_time时间点。返回是否成功减少。
staic std::ptrdiff_t max()返回内部计数器的最大可能值,它大于或等于 LeastMaxValue

std::binary_semaphore

这是一个二元信号量,使用方法与counting_semaphore一致:

using binary_semaphore = std::counting_semaphore<1>;

latchbarrier

线程屏障库,允许任何数量的线程阻塞直至期待数量的线程到达该屏障。

std::latch

<latch> 中定义,latchstd::ptrdiff_t 类型的向下计数器,它能用于同步线程。在创建时初始化计数器的值。不同于 std::barrier ,参与线程能减少 std::latch 多于一次。

构造与析构

explicit latch(std::ptrdiff_t expected)

构造 latch 并初始化其内部计数器。expected应大于0。

析构期间不应再调用其任何成员函数。

成员函数

函数名说明
void count_down(std::ptrdiff_t n = 1)内部计数器减少n而不阻塞。n不应大于其内部计数器的值且大于0。
void wait()阻塞调用方线程直至内部计数器抵达0。若它已为零,则立即返回。
bool try_wait()若内部计数器抵达零 则返回 true。
void arrive_and_wait(std::ptrdiff_t n = 1)内部计数器减少n(若需要)并阻塞调用方线程直至计数器抵达零。
staic std::ptrdiff_t max()返回实现所支持的内部计数器的最大值。

std::barrier

类模板 std::barrier 提供允许至多为期待数量的线程阻塞直至期待数量的线程到达该屏障。不同于 std::latchbarrier是可重用的,一旦数量达到解除阻塞之后,其内部计数会重置。

构造与析构

explicit barrier(std::ptrdiff_t expected, CompletionFunction f = CompletionFunction())

设置每初始期待计数为 expected ,以 std::move(f) 初始化完成函数对象,然后开始第一阶段。f为每次达到计数后自动调用的。注意f应当是noexcept的。

成员函数

函数名说明
arrival_token arrive(std::ptrdiff_t n = 1)构造一个于当前阶段的阶段同步点关联的 arrival_token 对象,然后将期待计数减少n
void wait(arrival_token&& arrival)arrival 与当前阶段的阶段同步点关联,则在与 arrival 关联的阶段同步点阻塞直至该同步点的阶段完成步骤运行。否则,若 arrival 与前一阶段关联,则立即返回。
void arrive_and_wait()将期待计数减少 1 ,然后在当前阶段的同步点阻塞直至运行当前阶段的阶段完成步骤。
void arrive_and_drop()将所有后继阶段的初始期待计数减少一,然后将当前阶段的期待计数减少一。
staic std::ptrdiff_t max()返回实现所支持的内部计数器的最大值

stop_token

C++20引入,提供异步停止信息相关的类。

std::stop_token

提供是否已经或能否对其所关联的std::stop_source对象作出停止请求的线程安全的方法。一般不会独立构造 stop_token 对象,而是从 std::jthreadstd::stop_source 取得它。这使它共享与 std::jthreadstd::stop_source 的相同的关联停止状态。

构造与析构

stop_token 独立构造是没有意义的。但其提供拷贝构造和移动构造,拷贝构造会绑定到相同的停止状态,而移动构造会使自己获得旧对象的状态,然后旧对象置空。

成员函数

函数名说明
bool stop_requested()若该 stop_token 对象拥有关联停止状态且该状态已收到停止请求则返回true,否则为false
bool stop_possible()stop_token 对象无关联停止状态,或仍未收到停止请求且无关联的std::stop_source对象则返回false,否则为true

std::stop_source

stop_source类提供发出停止请求的方式。对一个stop_source对象作出的停止请求对所有拥有同一关联状态的stop_sourcestd::stop_token可见;调用任何对关联std::stop_token注册的std::stop_callback;并且唤醒任何在关联std::stop_token上等待的std::condition_variable_any对象。

构造与析构

函数名说明
stop_source()构造拥有新停止状态的 stop_source
explicit stop_source(std::nostopstate_t nss)构造无关联停止状态的空 stop_source
stop_source(const stop_source& other)拷贝构造函数。构造拥有与other相同的关联停止状态的stop_source
stop_source(stop_source&& other)移动构造函数。构造拥有与other相同的关联停止状态的stop_source,将 other 设为空。

成员函数

函数名说明
bool request_stop()stop_source 对象拥有停止状态且未被请求停止,则发出停止请求给停止状态。
std::stop_token get_token()stop_source 拥有停止状态,则返回与 stop_source 的停止状态关联的 stop_token 对象;否则返回空的 stop_token
bool stop_requested()若该 stop_token 对象拥有关联停止状态且该状态已收到停止请求则返回true,否则为false
bool stop_possible()stop_token 对象无关联停止状态,或仍未收到停止请求且无关联的std::stop_source对象则返回false,否则为true

std::stop_callback

stop_callback 类模板提供对关联的std::stop_token对象注册回调函数的 RAII 对象类型,使得将在std::stop_token被请求停止时调用回调函数。在对该 stop_callback 的关联std::stop_tokenstd::stop_source成功调用request_stop()的同一线程调用经由 stop_callback 构造函数注册的回调函数;或若在构造函数的注册前已请求停止,则在构造 stop_callback 的线程调用回调函数。

构造与析构

函数名说明
explicit stop_callback(const std::stop_token& st, C&& cb)对给定的st(复制),给定可调用回调函数cb构造 stop_callback
explicit stop_callback(std::stop_token&& st, C&& cb)对给定的st(移动),给定可调用回调函数cb构造stop_callback

不允许拷贝构造。

析构时会取消绑定,但是如果其在析构时其他线程正在调用回调函数,则析构会被阻塞直到回调函数调用完成。

std::jthread

jthread在C++20引入进<thread>库中,是有自动合并和取消支持的std::thread。自动合并是指jthread在析构时会自动调用jion()(如果需要);取消支持是指其可以从主线程向内部线程发送一个终止信号,使内部线程对其做出响应。

std::jthread是可以直接当成std::thread来使用的,没有任何区别。但其额外提供一些函数函数,同时向其传递的可调用对象也可以有特殊参数。

构造与析构

jthread的构造方法与thread没有区别,但是其中的可调用对象可以接收一个std::stop_token类型的参数,该参数如果有必须是第一个参数,jthread会自动进行参数传递。

jthread在析构时会自动调用request_stop()join()(如果需要)。

成员函数

相比于threadjthread增加了如下几个成员函数:

函数名说明
std::stop_source get_stop_source()返回std::stop_source,拥有与 jthread 对象内部所保有者相同的共享停止状态。
std::stop_token get_stop_token()返回std::stop_token,与 jthread 对象内部保有的同一共享停止状态关联。
bool request_stop()若内部停止状态尚未被请求停止,则对它发出停止请求。返回是否发出请求。

示例

void thread_proc(std::stop_token st, int x) {
    while(!st.stop_requested()) {
        std::cout<<"working"<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout<<"thread_proc:Thread "<<std::this_thread::get_id()<<" exit"<<std::endl;
}

int main() {
    std::cout<<"main:Thread id "<<std::this_thread::get_id()<<std::endl;
    std::jthread thr(&thread_proc, 3); // 创建线程
    std::stop_callback sc(thr.get_stop_token(), []{
        std::cout<<"stop_callback:Thread "<<std::this_thread::get_id()<<" call stop_callback"<<std::endl;
    });
    std::this_thread::sleep_for(std::chrono::seconds(4));
    thr.request_stop(); // request_stop()也可以省略,但是因为stop_callback是局部的,会在jthread之前析构,所以这里显式调用了。
    return 0; // jthread可以不手动join()
}

参考

【C++】C++多线程资料总目录
微软C++ 标准库参考 (STL)
C++参考手册

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值