C++11 std::thread

2,250 阅读17分钟

C++ 11增加了标准线程库:std::thread,在语言级别上提供了线程支持,并且是跨平台的。在不同操作系统上,依赖于平台本身的线程库,例如Linux上,底层实现是pthread库。 std::thread禁止了拷贝构造函数和拷贝赋值运算符,所以std::thread对象不能拷贝,但是可以移动。

基础知识

一个最简单的实例:

void func(int arg1, int arg2) {
    cout << "arg1: " << arg1 << ", arg2: " << arg2 << endl;
    cout << "child thread id: " << std::this_thread::get_id() << endl;
}
int main() {
    // 创建并启动子线程
    std::thread thread(func, 10, 100);
    cout << "child thread id: " << thread.get_id() << endl;
    thread.join();

    cout << "main thread id: " << std::this_thread::get_id() << endl;
    cout << "main thread exit" << endl;
    return 0;
}

// 输出
child thread id: 0x70000e841000
arg1: 10, arg2: 100
child thread id: 0x70000e841000
main thread id: 0x10ef98dc0
main thread exit

std::thread线程对象支持的操作如下所示:

  • get_id(): 获取线程ID,类型为std::thread::id
  • join(): 该函数会阻塞当前线程,直到指定线程执行完毕。
  • joinable(): 线程是否可被join,线程有两种状态:joinable和detached,前者必须通过join函数终止线程释放资源,而后者则会在线程执行结束后,自行释放资源。
  • detach(): 使线程从joinable变为detached状态。
  • swap(thread& __t): 交换两个线程对象所代表的底层句柄
  • native_handle(): 返回底层句柄,因为std::thread实现和操作系统相关,因此该函数返回std::thread 底层实现的线程句柄,例如:在Posix标准平台下,就表示Pthread句柄pthread_t
  • static unsigned hardware_concurrency(): 返回当前平台的底层线程实现支持的并发线程数目。

std::this_thread表示当前线程,this_thread实际是一个namespace,支持如下操作:

  • get_id(): 获取线程ID,类型为std::thread::id
  • sleep_for(const chrono::duration<_Rep, _Period>& __d): sleep指定的duration,std::chrono为duration创建了不同时间维度的类型别名,例如:std::chrono::seconds(5)表示5秒;std::chrono::milliseconds(500)表示500毫秒。
  • sleep_until(const chrono::time_point<_Clock, _Duration>& __t): sleep到绝对时间
  • sleep_until(const chrono::time_point<chrono::steady_clock, _Duration>& __t): sleep到绝对时间
  • yield: 当前线程放弃执行,操作系统调度另一线程继续执行。

下面通过sleep_forsleep_until分别实现休眠10秒:


// 当前时间戳
std::time_t timestamp = std::chrono::system_clock::to_time_t(system_clock::now());
// sleep_until实现休眠10秒
std::this_thread::sleep_until(system_clock::from_time_t(timestamp + 10));

// sleep_for实现休眠10秒
std::this_thread::sleep_for(std::chrono::seconds(10));

关于时间的操作可参考time.h文件

// 当前时间戳(秒)
std::time_t now_timestamp = system_clock::to_time_t(system_clock::now());
// struct tm结构体,包含了时、分、秒
struct std::tm *now_tm = std::localtime(&now_timestamp);
std::cout << "Current time: " << now_tm->tm_hour << ":" << now_tm->tm_min << ":"<< now_tm->tm_sec << '\n';
// 未来一分钟
++now_tm->tm_min;
// 未来时间戳
std::time_t future_timestamp = std::mktime(now_tm);
std::cout << "Future time: " << now_tm->tm_hour << ":" << now_tm->tm_min << ":"<< now_tm->tm_sec << '\n';
// time_point格式,可用于std::this_thread::sleep_until
std::chrono::time_point<system_clock> x = system_clock::from_time_t(future_timestamp);

// 输出
Current time: 19:3:22
Future time: 19:4:22

线程锁

互斥锁

mutex(锁)

std::mutex表示互斥锁,不可拷贝(删除了拷贝构造函数和拷贝赋值操作符)。不支持递归锁定,若有此需求,可使用std::recursive_mutex代替。

支持的操作:

  • void lock():锁住mutex,若mutex未被其他线程锁住,则调用线程锁定它;若mutex已经被其他线程锁住,则调用线程将被阻塞,直到其他线程解锁mutex;若mutex已经被调用线程锁定,那么再次锁定同一个mutex,则会产生死锁(未定义的行为),可使用recursive_mutex代替,它允许同一个线程多次锁定同一个recursive_mutex
  • bool try_lock():尝试锁住mutex,若mutex未被其他线程锁住,则调用线程锁定它,并且try_lock返回true;若mutex已经被其他线程锁住,则调用线程无法锁定mutex,并且try_lock直接返回false(不阻塞);若mutex已经被调用线程锁定,那么再次锁定同一个mutex,则会产生死锁(未定义的行为),可使用recursive_mutex代替,它允许同一个线程多次锁定同一个recursive_mutex
  • void unlock():解锁mutex,调用线程释放互斥锁,其他阻塞在lock函数的线程,可以锁住mutex继续执行。

lock和try_lock的差异主要是不能锁定mutex时表现不同,lock函数会一直阻塞调用线程,直到可以锁定mutex为止;而try_lock则不会阻塞调用线程,而是直接返回,并且返回值为false。

recursive_mutex(可重入锁)

recursive_mutexmutex的基础上,允许同一个线程对同一个recursive_mutex多次加锁,表示获得recursive_mutex的多层所有权,同时对recursive_mutex解锁时,也要调用相同次数的unlock,这样调用线程才能彻底释放对recursive_mutex的所有权,其他线程才能锁定recursive_mutex

timed_mutex(时间锁)

timed_mutexmutex基础上,增加了两个成员函数try_lock_fortry_lock_until,表示等待一段时间,尝试获得锁。

  • bool try_lock_for(const chrono::duration<_Rep, _Period>& __d):阻塞一段时间,尝试锁住timed_mutex。若timed_mutex未被其他线程锁住,则调用线程锁定它,并且函数返回true;若timed_mutex已经被其他线程锁住,则调用线程无法锁定timed_mutex,函数会阻塞参数指定的duration,在这期间,若成功锁住timed_mutex(其他线程释放了timed_mutex),则函数直接返回true,否则超时后函数返回false,表示指定的duration内没有获得锁;若timed_mutex已经被调用线程锁定,那么再次锁定同一个timed_mutex,则会产生死锁(未定义的行为),可使用recursive_timed_mutex代替,它允许同一个线程多次锁定同一个recursive_timed_mutex
  • bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time):行为与try_lock_for一致,只不过参数是绝对时间。
std::timed_mutex timed_mutex;
// 尝试2秒内获得锁
if (timed_mutex.try_lock_for(std::chrono::seconds(2))) {   
    // do some thing            
    // 释放锁
    timed_mutex.unlock();
}

// 当前绝对时间戳
std::time_t timestamp = std::chrono::system_clock::to_time_t(system_clock::now());
// 尝试阻塞到未来的绝对时间获得锁
if (timed_mutex.try_lock_until(system_clock::from_time_t(timestamp + 10))) {   
    // do some thing            
    // 释放锁
    timed_mutex.unlock();
}

recursive_timed_mutex(可重入时间锁)

可重入的时间锁,同时具备timed_mutexrecursive_mutex的能力,不再赘述。

🔐锁层级

针对具备不同能力的锁,C++划分了三种层级:

  1. BasicLockable:支持lock和unlock的锁。
  2. Lockable:在BasicLockable基础上,支持try_lock的锁,例如:mutex和recursive_mutex。
  3. TimedLockable:在Lockable基础上,支持try_lock_for和try_lock_until的锁,例如:timed_mutex和recursive_timed_mutex。

Lock模板类

lock_guard

lock_guard是一个管理mutex的对象,不可拷贝(删除了拷贝构造函数和拷贝赋值操作符),除了缺省合成的函数外,没有其他成员函数。构造lock_guard时,mutex被调用线程锁定,销毁lock_guard时,mutex被调用线程解锁。通过这种方式,可以确保程序抛出异常时,也能正确地解锁mutex。这里的mutex可以是四种锁中的任意一种。 lock_guard不会介入mutex生命周期,程序必须保证mutex的生命周期至少延长到持有它的lock_guard销毁为止。

简单来说,构造lock_guard时,获得锁;析构lock_guard时,释放锁。

除了单参数构造函数,lock_guard还有一个包含两个参数的构造函数: lock_guard(mutex_type& __m, adopt_lock_t),表示创建lock_guard时,构造函数不会对mutex加锁,而是由外部程序保证mutex已经被加锁了。

下面是一个典型案例:

// 互斥锁
std::mutex mtx;

void print_even(int x) {
    if (x % 2 == 0) std::cout << x << " is even\n";
    else throw (std::logic_error("not even"));
}

void print_thread_id(int id) {
    try {
        // 使用lock_guard锁定mtx,保证即使是异常逻辑,lock_guard也可以在析构时解锁mtx
        std::lock_guard<std::mutex> lck(mtx);
        print_even(id);
    }
    catch (std::logic_error &) {
        std::cout << "[exception caught]\n";
    }
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_thread_id, i + 1);

    for (auto &th : threads) th.join();

    return 0;
}

// 可能的输出
[exception caught]
6 is even
4 is even
[exception caught]
[exception caught]
2 is even
[exception caught]
8 is even
[exception caught]
10 is even

unique_lock

unique_lock是一个管理mutex的对象,不可拷贝(删除了拷贝构造函数和拷贝赋值操作符),在lock_guard基础上,增加了locktry_locktry_lock_fortry_lock_untilunlock等成员函数(这些函数的作用在上面👆已经介绍过了),更加灵活,但相应的性能会受一些影响。 下面是源码中的类定义:

template <class _Mutex>
class unique_lock
{
public:
    // 模板参数,各类锁
    typedef _Mutex mutex_type;
private:
    // 锁
    mutex_type* __m_;
    // 是否已经获得锁
    bool __owns_;
public:
    // 无参构造函数
    unique_lock() : __m_(nullptr), __owns_(false) {}
    // 单参数构造函数,与lock_guard一样,构造函数中加锁
    explicit unique_lock(mutex_type& __m)
        : __m_(_VSTD::addressof(__m)), __owns_(true) {__m_->lock();}
    // 带defer_lock_t的构造函数(第二个参数可使用编译时常量std::defer_lock),构造函数中不加锁
    unique_lock(mutex_type& __m, defer_lock_t) _NOEXCEPT
        : __m_(_VSTD::addressof(__m)), __owns_(false) {}
    // 带try_to_lock_t的构造函数(第二个参数可使用编译时常量std::try_to_lock),构造函数中尝试加锁
    unique_lock(mutex_type& __m, try_to_lock_t)
        : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock()) {}
    // 带adopt_lock_t的构造函数(第二个参数可使用编译时常量std::adopt_lock),与lock_guard一样,构造函数中不加锁,而是默认外部程序已经加锁了
    unique_lock(mutex_type& __m, adopt_lock_t)
        : __m_(_VSTD::addressof(__m)), __owns_(true) {}
    template <class _Clock, class _Duration>
        // 构造函数中通过try_lock_until尝试加锁
        unique_lock(mutex_type& __m, const chrono::time_point<_Clock, _Duration>& __t)
            : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock_until(__t)) {}
    template <class _Rep, class _Period>
        // 构造函数中通过try_lock_for尝试加锁
        unique_lock(mutex_type& __m, const chrono::duration<_Rep, _Period>& __d)
            : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock_for(__d)) {}
    // 析构时,若加锁了,则释放锁
    ~unique_lock()
    {
        if (__owns_)
            __m_->unlock();
    }

private:
    // 相当于删除了拷贝构造函数和拷贝赋值操作符
    unique_lock(unique_lock const&); // = delete;
    unique_lock& operator=(unique_lock const&); // = delete;

public:
#ifndef _LIBCPP_CXX03_LANG
    // 移动构造函数
    unique_lock(unique_lock&& __u) _NOEXCEPT
        : __m_(__u.__m_), __owns_(__u.__owns_)
        {__u.__m_ = nullptr; __u.__owns_ = false;}
    // 移动赋值操作符
    unique_lock& operator=(unique_lock&& __u) _NOEXCEPT
        {
            if (__owns_)
                __m_->unlock();
            __m_ = __u.__m_;
            __owns_ = __u.__owns_;
            __u.__m_ = nullptr;
            __u.__owns_ = false;
            return *this;
        }

#endif  // _LIBCPP_CXX03_LANG
    // 主动加锁
    void lock();
    // 尝试加锁
    bool try_lock();

    template <class _Rep, class _Period>
        bool try_lock_for(const chrono::duration<_Rep, _Period>& __d);
    template <class _Clock, class _Duration>
        bool try_lock_until(const chrono::time_point<_Clock, _Duration>& __t);
    // 释放锁
    void unlock();

    // swap实现
    void swap(unique_lock& __u) _NOEXCEPT
    {
        _VSTD::swap(__m_, __u.__m_);
        _VSTD::swap(__owns_, __u.__owns_);
    }
    // 返回持有的mutex,但是该函数不会解锁mutex
    mutex_type* release() _NOEXCEPT
    {
        mutex_type* __m = __m_;
        __m_ = nullptr;
        __owns_ = false;
        return __m;
    }

    // 判断是否已经加锁了
    bool owns_lock() const _NOEXCEPT {return __owns_;}
    // 重载了函数调用符, 判断是否已经加锁了
    operator bool () const _NOEXCEPT {return __owns_;}
    // 获得持有的mutex
    mutex_type* mutex() const _NOEXCEPT {return __m_;}
};

unique_lock的构造函数,实现了不同的加锁策略,具体可见上面的源码和注释。

除非必要,优先使用更高效的lock_guard。

全局函数

std::call_once

call_once是全局函数模板,原型如下所示:

template <class Fn, class... Args>
  void call_once (once_flag& flag, Fn&& fn, Args&&... args);

call_once使用参数args调用fn函数,除非另一个线程已经(或者正在)使用相同的once_flag调用call_once

第一个使用相同once_flag调用call_once的线程,会执行fn函数,同时使其他使用相同once_flag调用call_once的线程进入被动执行状态,即:其他线程不执行fn函数,但是会阻塞到第一个执行fn函数的线程结束。

如果通过call_once执行fn函数的线程抛出了异常,并且存在被动执行的线程,则会从其中选择一个线程使其执行fn函数。

如果已经有线程执行完了call_once,即fn函数返回了,那么当前所有被动执行的线程和将来对call_once的调用(使用相同once_flag)也会立即返回,不会再次执行fn函数。 即多线程环境下,相同once_flagcall_once调用,只会执行一次fn函数

下面看一个实际案例:

int winner;

void set_winner(int x) {
    winner = x;
    std::cout << "set_winner, x = " << x << std::endl;
}

// 多个线程共用一个once_flag
std::once_flag winner_flag;
// 线程函数
void wait_1000ms(int id) {
    // 循环1000次,每次休眠1ms
    for (int i = 0; i < 1000; ++i)
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        // 不同线程通过同一个once_flag,调用call_once,最终只会有一个线程执行对应的set_winner函数
        std::call_once(winner_flag, set_winner, id);
}

int main() {
    std::thread threads[10];
    // 10个线程
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(wait_1000ms, i + 1);

    std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

    for (auto &th : threads) th.join();
    std::cout << "winner thread: " << winner << '\n';

    return 0;
}

// 可能的输出
waiting for the first among 10 threads to count 1000 ms...
set_winner, x = 8
winner thread: 8

可见,虽然运行了10个线程,但是最终只有一个线程执行了set_winner函数。

std::lock

lock是全局函数模板,原型如下所示:

template <class Mutex1, class Mutex2, class... Mutexes>
  void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

函数会锁定所有的mutex,必要时阻塞调用线程。

lock函数以不确定的顺序调用所有mutex的成员函数:lock、try_lock和unlock,以确保函数返回时,所有mutex都被锁定了(而不会产生任何死锁)。

如果lock函数不能锁定所有mutex(例如:其中一个调用抛出了异常),那么在函数失败之前,首先会解锁它成功锁定的所有mutex

下面👇是同时锁定foo和bar的案例:

std::mutex foo, bar;
void task_a() {
    // 若使用foo.lock(); bar.lock(); 而不是std::lock,那么有可能task_a锁定了foo,而task_b锁定了bar,从而导致死锁
    // foo.lock(); bar.lock(); 
    std::lock(foo, bar);
    std::cout << "task a\n";
    foo.unlock();
    bar.unlock();
}

void task_b() {
    // 若使用bar.lock(); foo.lock(); 而不是std::lock,那么有可能task_a锁定了foo,而task_b锁定了bar,从而导致死锁
    // bar.lock(); foo.lock();
    std::lock(bar, foo);
    std::cout << "task b\n";
    bar.unlock();
    foo.unlock();
}

int main() {
    std::thread th1(task_a);
    std::thread th2(task_b);

    th1.join();
    th2.join();

    return 0;
}

std::try_lock

try_lock是全局函数模板,原型如下所示:

// int返回值:若成功锁定了所有mutex,则返回-1;否则返回加锁失败的mutex的索引值
template <class Mutex1, class Mutex2, class... Mutexes>
  int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

函数尝试通过mutex.try_lock锁定所有的mutex

try_lock函数通过mutex.try_lock成员函数依次为参数中的mutex加锁(首先是a,然后是b,最后是cde),直到所有调用都成功,或者任意一个调用失败(即mutex.try_lock返回false或抛出异常)。

如果try_lock函数由于某个mutex加锁失败而返回,则会解锁所有先前加锁成功的mutex,并且返回那个加锁失败的mutex的索引值。

std::mutex foo, bar;

void task_a() {
    foo.lock();
    std::cout << "task a\n";
    bar.lock();
    // ...
    foo.unlock();
    bar.unlock();
}

void task_b() {
    int x = try_lock(bar, foo);
    if (x == -1) {
        std::cout << "task b\n";
        // ...
        bar.unlock();
        foo.unlock();
    } else {
        std::cout << "[task b failed: mutex " << (x ? "foo" : "bar") << " locked]\n";
    }
}

int main() {
    std::thread th1(task_a);
    std::thread th2(task_b);

    th1.join();
    th2.join();

    return 0;
}

线程同步

condition_variable

condition_variable是一个同步原语,能够阻塞调用线程,直到其他线程通知恢复为止,不可拷贝(删除了拷贝构造函数和拷贝赋值操作符)。 当调用condition_variable任意wait函数时,将使用unique_lock(通过mutex)锁定线程。该线程将一直处于阻塞状态,直到另一个线程调用同一个condition_variable任意notify函数唤醒为止。

condition_variable对象总是使用std::unique_lock<mutex>实现线程阻塞。

condition_variable可用的wait函数:

wait

wait有两个重载版本,如下所示:

void wait (unique_lock<mutex>& lck);
	
template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);

表示阻塞调用线程(调用线程调用wait之前必须已经锁定了unique_lock持有的mutex),直到被其他线程唤醒。 该函数阻塞调用线程的时刻,会自动调用unique_lock.unlock解锁mutex,以允许其他线程加锁同一个mutex继续运行。 一旦被其他线程唤醒,该函数会解除阻塞状态,并且调用unique_lock.lock重新加锁(可能会再次阻塞调用线程),让unique_lock恢复到wait函数被调用时的状态。

通常情况下,其他线程调用condition_variable的notify_one或者notify_all成员函数,来唤醒被阻塞的线程。但是,某些实现可能会在不调用任何notify函数的情况下产生虚假唤醒。因此,使用该函数的程序应该确保其恢复条件得到了满足,所以一般在循环结构中调用wait函数,如下所示,这样即使被虚假唤醒,也会因为条件不满足,再次进入阻塞状态。

while(条件不满足){
    condition_variable.wait
}

包含_Predicate __pred参数的重载版本中,_Predicate是函数模板的参数,表示返回布尔值的函数。如果__pred返回false,则函数会一直阻塞,只有当它返回true时,notify才能唤醒线程。__pred会一直被调用,直到它返回true,非常适合处理虚假唤醒问题,如下所示:

template <class _Predicate>
void
condition_variable::wait(unique_lock<mutex>& __lk, _Predicate __pred)
{
    // __pred()返回false,则一直调用wait阻塞
    while (!__pred())
        wait(__lk);
}

整体来看,wait类函数有三个注意点:

  1. 调用wait类函数时,有一个前提,就是调用线程必须已经锁定了mutex,
  2. wait类函数被调用时,会做两件事:
  • 阻塞调用线程。
  • 调用unique_lock.unlock,解锁调用线程已经锁定的mutex。
  1. wait类函数被唤醒时,也会做两件事:
  • 解除调用线程的阻塞状态。
  • 调用unique_lock.lock,重新为调用线程加锁。

这样,wait类函数在被调用前和被唤醒返回后,可以确保是同一个线程状态。

下面👇看一个案例 案例

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;

bool shipment_available() { return cargo != 0; }

void consume(int n) {
    for (int i = 0; i < n; ++i) {
        std::unique_lock<std::mutex> lck(mtx);
        cv.wait(lck, shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo = 0;
    }
}

int main() {
    std::thread consumer_thread(consume, 10);

    // produce 10 items when needed:
    for (int i = 0; i < 10; ++i) {
        while (shipment_available()) std::this_thread::yield();
        std::unique_lock<std::mutex> lck(mtx);
        cargo = i + 1;
        cv.notify_one();
    }

    consumer_thread.join();

    return 0;
}

// 输出
1
2
3
4
5
6
7
8
9
10

子线程因为cargo等于0,会进入阻塞状态,然后主线程修改cargo,并且唤醒子线程。

wait_for

wait_for有两个重载版本,如下所示:

enum class cv_status{
    no_timeout,
    timeout
}
// 如果因为时间到了停止阻塞,则返回timeout,否则返回no_timeout
cv_status wait_for(unique_lock<mutex>& __lk, const chrono::duration<_Rep, _Period>& __d);
// 返回pred(),而不管是否触发了超时(尽管只有触发超时时,才能为false)。         
bool wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_for函数在wait基础上,增加了阻塞持续时间的能力,所以有两种方式被唤醒:

  • 被其他线程通过notify主动唤醒。
  • 阻塞的duration到了,会主动停止阻塞,与被其他线程唤醒一样。

👇下面看一个实际案例:

std::condition_variable cv;
int value;
void read_value() {
    std::cin >> value;
    cv.notify_one();
}

int main() {
    std::cout << "Please, enter an integer (I'll be printing dots): \n";
    std::thread th(read_value);

    std::mutex mtx;
    std::unique_lock<std::mutex> lck(mtx);
    while (cv.wait_for(lck, std::chrono::seconds(1)) == std::cv_status::timeout){
        std::cout << '.' << std::endl;
    }
    std::cout << "You entered: " << value << '\n';

    th.join();

    return 0;
}

主线程每次阻塞1秒,若wait_for是以超时结束,则打印.,并且再次进入阻塞状态,直到被子线程主动唤醒退出while循环,结束主线程。

wait_until

wait_until有两个重载版本,如下所示:

enum class cv_status{
    no_timeout,
    timeout
}
// 如果因为时间到了停止阻塞,则返回timeout,否则返回no_timeout 
cv_status wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time);
// 返回pred(),而不管是否触发了超时(尽管只有触发超时时,才能为false)。   	
bool wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);

wait_until函数在wait基础上,增加了阻塞到绝对时间的能力,所以有两种方式被唤醒:

  • 被其他线程通过notify主动唤醒。
  • 阻塞的绝对时间到了,会主动停止阻塞,与被其他线程唤醒一样。

notify_all

解除当前正在等待指定condition_variable的所有线程的阻塞状态。如果没有线程在等待,函数将什么也不做。

唤醒所有等待的线程

notify_one

解除当前正在等待指定condition_variable的所有线程中任意一个线程的阻塞状态。如果没有线程在等待,函数将什么也不做。

随机唤醒一个等待的线程

notify_all和notify_one必须在加锁情况下调用吗

并不强制,可以根据具体情况,决定是否需要加锁

通知线程调用notify_all或者notify_one时,不需要提前加锁(与等待线程锁定的同一个mutex)。实际上,先加锁后通知是一种悲观做法,因为被通知的等待线程会立即再次阻塞,等待通知线程释放锁。 然而,一些实现(尤其是pthreads)认识到这种情况,并通过在notify中将等待线程从condition_variable队列直接转移到mutex队列,而不唤醒它,从而避免这种“匆忙等待”的场景。

但是若需要精确的事件调度,那么先加锁后通知是有必要的,例如:等待线程将在满足条件后直接退出程序,这将导致通知线程的condition_variable被销毁,为了不让等待线程立即获得锁,那么在加锁状态下进行通知可能是有必要的。

condition_variable_any

condition_variable的wait/wait_for/wait_until函数只能以unique_lock作为参数,但是condition_variable_any的wait/wait_for/wait_until函数可以以任何Lockable类型的锁作为参数。除此之外,两者的能力完全相同。 下面看一个上面condition_variable.wait

std::mutex mtx;
std::condition_variable_any cv;

int cargo = 0;

bool shipment_available() { return cargo != 0; }

void consume(int n) {
    for (int i = 0; i < n; ++i) {
        mtx.lock();
        cv.wait(mtx, shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo = 0;
        mtx.unlock();
    }
}

int main() {
    std::thread consumer_thread(consume, 10);

    // produce 10 items when needed:
    for (int i = 0; i < 10; ++i) {
        while (shipment_available()) std::this_thread::yield();
        mtx.lock();
        cargo = i + 1;
        cv.notify_one();
        mtx.unlock();
    }

    consumer_thread.join();

    return 0;
}

参考文章

  1. c++ multithreading