C++:线程库

news2024/12/29 8:42:06

C++:线程库

    • thread
      • thread
      • this_thread
        • chrono
      • 引用拷贝问题
    • mutex
      • mutex
      • timed_mutex
      • recursive_mutex
      • lock_guard
      • unique_lock
    • atomic
      • atomic
      • CAS
    • condition_variable
      • condition_variable


thread

操作线程需要头文件<thread>,头文件包含线程相关操作,内含两个内容:

  • thread类:操作线程的基本类
  • this_thread命名空间域:用于操作当前线程

thread

thread是用于操作线程的类,其实现了对多平台线程的封装,以统一的面向对象方式完成线程的操作。

  • thread构造:
thread() noexcept;

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

thread (const thread&) = delete;

thread (thread&& x) noexcept;

从以上构造的声明可以得出以下要点:

  1. 可以不传参,直接构造一个空线程对象
  2. 禁止拷贝构造
  3. 支持移动构造

其中第二个声明是最重要的:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

这是一个函数模板,参数如下:

  • fn:一个可调用对象,创建线程后线程执行该函数内容
  • args:可变参数包,用于给fn传参,可以传任意数量的参数

示例:

#include <iostream>
#include <thread>

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    int a = 3;
    int b = 5;

    std::thread t(test, a, b);

    return 0;
}

这就创建了一个线程对象,并且设定好了该线程要执行的函数。

此处最大的便利在于,不论是Windows还是Linux,创建线程时传入的函数,都只允许传入一个参数,而此处可以通过参数包传入任意数量的参数

  • 成员函数:

在这里插入图片描述

  1. operator=

thread线程类的赋值重载,同样禁止拷贝赋值,只允许移动赋值。

还记得我们可以创建一个空线程类吗,没有函数可以给一个空线程类绑定函数,只能通过移动赋值来完成初始化空线程类

示例:

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    std::vector<std::thread> v(100);

    for (auto& th : v)
    {
        th = std::thread(test, 100, 200);
    }

    return 0;
}

当要管理多个线程的时候,可以把多个线程对象放到一个容器内部统一管理,此处用一个vector管理了100个线程对象。但是一开始可能还不能确定线程对象要执行哪一个函数,所以会在容器内部构建空线程对象。需要初始化时,就得用一个匿名对象通过构造函数创建非空线程对象,随后通过operator=完成移动赋值

以上示例中, th = std::thread(test, 100, 200)被放在for循环内部,完成对所有空线程对象的初始化。

  1. get_id

直接获取一个线程的id,其声明如下:

id get_id() const noexcept;

如果仔细观察返回值,你会发现其返回的不是一个整数,而是一个叫做id的类型。其是thread的内部类,由于不同操作系统对线程的标识符不同,比如Linux使用整数tid标识,而Windows使用线程句柄标识不同线程。所以无法统一线程的id,因此C++将不同操作系统的标识符封装为一个类thread::id

thread::id重载了以下操作符:

bool operator== (thread::id lhs, thread::id rhs) noexcept;
bool operator!= (thread::id lhs, thread::id rhs) noexcept;
bool operator< (thread::id lhs, thread::id rhs) noexcept;
bool operator<= (thread::id lhs, thread::id rhs) noexcept;
bool operator> (thread::id lhs, thread::id rhs) noexcept;
bool operator>= (thread::id lhs, thread::id rhs) noexcept;

允许进行比大小,判断相等的操作,因此可以放在容器中作为键值,用于管理线程比如mapset之类的容器。

那么能否作用于unordered_map这样的哈希容器呢?想要将数据放到哈希表中作为键,就需要一个哈希函数将数据转化为一个整数下标。而id是一个复杂的类,不同系统下内部的内容不一样,很难写出一个统一的哈希函数完成转化。而C++对此进行了模板特化,在thread::id作为哈希的键时,C++内部实现了哈希函数,所以thread::id可以直接在哈希表内部使用。

template <class T> struct hash; // 通用模板声明
template<> struct hash<thread::id>; // 对 thread::id 的模板特化

另外的,thread::id还支持流输出:

template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);

可以直接用cout这样的流输出对象来输出id

std::thread th(test, 100, 500);
std::cout << th.get_id() << std::endl;
  1. join

用于回收线程,调用该函数后,主线程进入阻塞,直到被join的线程结束,最后回收该线程。

示例:

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    std::thread th(test, 100, 500);
    th.join();

    return 0;
}

只需要线程对象.join()就可以完成线程资源的回收,还是比较方便的。

  1. detatch

用于线程分离,被分离的线程将自己回收自己,无需再join

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    std::thread th(test, 100, 500);
    th.detach();
    
    return 0;
}

但是此处你很有可能会看不到线程的输出结果,因为线程th被分离后,就无需主线程回收了,主线程直接return,线程结束。但是由于主线程退出,同一进程下的所有线程也会推出,导致线程th还没有来得及输出,就被强制退出了。

  1. joinable

用于检测一个线程是否允许被join,如果线程被detach或已经被join了,那么joinable就会返回false,反之返回true


this_thread

std::this_thread 是一个命名空间,用于访问当前线程。它提供了一组函数来操作当前线程。

  1. get_id(): 返回当前线程的 ID。
  2. yield(): 让出 CPU 时间片,让其他线程运行。
  3. sleep_until(): 使当前线程睡眠直到某个时间点。
  4. sleep_for(): 使当前线程睡眠一段时间。

get_idyield都可以直接执行,不用传入参数。而后两个函数与时间相关,要用到C++封装的时间chrono

chrono

<chrono>是一个头文件,内包含chrono命名空间域,该域内部封装了各种时间的相关操作。

  • 时钟 clock
  1. std::chrono::system_clock:系统时钟,表示从 Unix 纪元开始的时间(1970 年 1 月 1 日 00:00:00 UTC)。
  2. std::chrono::steady_clock:稳定时钟,表示从程序启动开始的时间。

这两个时钟都有一个now成员函数,返回当前的时间。但是system_clock会受到系统时钟影响,如果用户调整了系统时间,就有可能造成时间错误,而稳定时钟不受系统时钟影响。

auto t1 = std::chrono::system_clock::now();
auto t2 = std::chrono::steady_clock::now();

这两个函数都返回一个time_point类型,表示当前时间点。

  • 时间段 duration

duration用于表示一个时间段,这个类的用法比较复杂,因此C++为我们封装了一些可以直接使用的类:

  1. std::chrono::nanoseconds (纳秒)
  2. std::chrono::microseconds (微秒)
  3. std::chrono::milliseconds (毫秒)
  4. std::chrono::seconds (秒)
  5. std::chrono::minutes (分钟)
  6. std::chrono::hours (小时)

这些类都是typedef后的duration,如果想要表示一个时间端,直接传数字即可:

auto dur1 = std::chrono::seconds(3); // 3秒
auto dur2 = std::chrono::minutes(5); // 5分钟
  • 时间点 time_point

time_point表示一个时间点,之前时钟返回的now,就是当前的时间点。C++支持了以下运算:

  1. 时间点time_point + 时间段duration = 时间点time_point
  2. 时间段duration - 时间段duration = 时间段duration
  3. 时间点time_point - 时间点time_point = 时间段duration

关于时间类,就简单了解到这里,此处只讲解了最基础的概念,为了讲解线程库的相关接口。

回到this_thread内部的函数:

  1. sleep_until(): 使当前线程睡眠直到某个时间点。
  2. sleep_for(): 使当前线程睡眠一段时间。

sleep_until需要传入一个时间点time_point,比如想要睡眠10秒,就可以用当前时间 + 10秒得到一个时间点,再用sleep_until完成睡眠:

auto t1 = std::chrono::steady_clock::now(); // 获取当前时间	
auto dur = std::chrono::seconds(10); // 获取十秒时间段
auto t2 = t1 + dur; // 时间点 + 时间段 = 时间点,十秒后

std::this_thread::sleep_until(t2); // 睡眠到 10 秒后

sleep_for需要传入一个时间段duration,同样的睡眠十秒:

auto dur = std::chrono::seconds(10); // 获取十秒时间段
std::this_thread::sleep_for(dur); // 睡眠到 10 秒后

引用拷贝问题

再看thread类的声明:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

此处可以发现,其构造参数使用的是&&引用折叠,可以传左值/右值引用。

尝试一下:

void test(int& x, int& y)
{
    x += 100;
    y += 200;
}

int main()
{
    int a = 30;
    int b = 50;

    std::thread t1(test, a, b);

    t1.join();

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    return 0;
}

这段代码无法正常运行,因为线程传参时,无法直接传引用,为什么?

不论是Linux还是Windows系统,创建多线程时,函数都只允许传入一个参数,比如Linux只允许传一个void*的变量。

但是C++封装后,允许传入多个值,最终一定要把这多个参数和函数封装在一个类内部,成为一个可调用对象(仿函数),一起通过一个变量传给线程函数。

而C++为了确保线程拿到的参数是有效的,不会出现线程拿到参数后,参数被主线程销毁了等情况。不论是普通变量,引用还是指针,都会进行一次拷贝

引用一旦经过拷贝,拷贝后的变量和拷贝前的变量,就不是一个变量了。所以此处传入引用会报错,在线程传参时,不能直接传引用

但是指针不怕拷贝,指针拷贝后,依然指向原先的变量。该问题的解决策略有三个:

  1. 使用指针进行传址调用
  2. 使用引用包装器
  3. 使用lambda传引用

引用包装器

引用包装器用于解决引用的拷贝问题,引用拷贝后依然是原先的变量。

引用包装器的原理如下:

template <typename T>
class reference_wrapper
{
public:
    // 构造函数,接受一个引用
    explicit reference_wrapper(T& ref)
        : _ptr(&ref)
    {}

    // 拷贝构造
    reference_wrapper(const reference_wrapper& other)
        : _ptr(other._ptr)
    {}

    operator T& () const
    {
        return *_ptr;
    }

private:
    T* _ptr;
};

引用包装器内部存储一个指针,当包装引用时,_ptr成员会存储引用的指针。当拷贝时,也是对指针进行拷贝,指针拷贝后,依然指向原先的变量。

最核心的是重载了operator T&,也就是隐式类型转化,此时引用包装器可以转化为一个T&引用,所以引用包装器可以当作引用来使用

如果想要将一个引用包装起来,可以使用std::ref()函数,其返回一个引用的引用包装器。另外的,如果是const引用,则使用std::cref()

对于刚才的线程问题,只需要在传递参数时用包装器包装一层即可:

std::thread t1(test, std::ref(a), std::ref(b));

结合lambda

除了以上两种方式,也可以直接使用lambda的捕捉列表,以引用的形式捕捉变量:

int main()
{
    int a = 30;
    int b = 50;

    auto func = [&a, &b]() {
        a += 100;
        b += 200;
        };

    std::thread t1(func);

    t1.join();

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    return 0;
}

lambda的直接捕捉,不用通过std::thread进行参数传递,所以就不会出现引用拷贝的问题。


mutex

既然要进行多线程并发编程,自然少不了线程安全的问题,<mutex>头文件内部,封装了各种锁,用于维护线程安全。

头文件内包含四种锁:

  1. mutex:互斥锁
  2. recursive_muetx:递归锁
  3. timed_mutex:时间锁
  4. recursive_timed_muetx:时间递归锁

以及两种基于ARII的加锁策略:

  1. lock_guard:作用域锁
  2. unique_lock:独占锁

mutex

mutex是最基础的互斥锁,可以对资源进行加锁解锁。

成员函数:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回

可以看出,mutex的使用非常简单。

示例:

int num = 0;

void test(int n)
{
    for (int i = 0; i < n; i++)
    {
        num++;
    }
}

int main()
{
    std::mutex mtx;

    std::thread t1(test, 2000);
    std::thread t2(test, 2000);

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

该代码中,两个线程一起对同一个num++,每个线程2000次,但是最后输出的num很有可能比2000少,因为num++不是原子性的。此时对其加锁:

int num = 0;

void test(int n, std::mutex& mtx)
{
    for (int i = 0; i < n; i++)
    {
        mtx.lock();
        num++;
        mtx.unlock();
    }
}

int main()
{
    std::mutex mtx;

    std::thread t1(test, 2000, std::ref(mtx));
    std::thread t2(test, 2000, std::ref(mtx));

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

通过std::mutex mtx定义了一个名为mtx的锁,随后通过std::thread将锁作为参数传入线程,在每次num++前加锁,后释放锁。


timed_mutex

时间锁就是限定每次申请锁的时长,如果超过一定时间没有申请到锁,就返回。

成员函数:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回
  • try_lock_until:如果到指定时间还没申请到锁就返回false,申请到锁返回true
  • try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true

通过之前的经验,可以猜出try_lock_until要传入一个time_point时间点,而try_lock_for要传入一个时间段duration

try_lock_for为例:

int num = 0;

void test(int n, std::timed_mutex& mtx)
{
    while (n)
    {
        bool ret = mtx.try_lock_for(std::chrono::microseconds(1));
        if (ret)
        {
            num++;
            n--;
            mtx.unlock();
        }
        else
        {
            std::cout << "加锁超时" << std::endl;
        }
    }
}

int main()
{
    std::timed_mutex mtx;

    std::thread t1(test, 2000, std::ref(mtx));
    std::thread t2(test, 2000, std::ref(mtx));

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

此处每次申请锁不超过一微秒,如果try_lock_for返回true说明抢到锁了,进行num++,反之则输出加锁超时


recursive_mutex

递归锁用于解决函数的递归造成的死锁,比如这样:

int num = 0;

void test(int n, std::mutex& mtx)
{
    if (n <= 0)
        return;

    mtx.lock();

    test(n - 1, mtx);

    mtx.unlock();
}

int main()
{
    std::mutex mtx;
    std::thread t1(test, 2000, std::ref(mtx));

    t1.join();
    std::cout << "num = " << num << std::endl;

    return 0;
}

函数test中会产生死锁,第一次递归对mtx加锁,第二次递归时由于自己已经占有锁了,再次申请锁就会阻塞,导致死锁。

递归锁就是用于解决这样的自己与自己造成的死锁局面。

int num = 0;

void test(int n, std::recursive_mutex& mtx)
{
    if (n <= 0)
        return;

    mtx.lock();

    test(n - 1, mtx);

    mtx.unlock();
}

int main()
{
    std::recursive_mutexmtx;
    std::thread t1(test, 2000, std::ref(mtx));

    t1.join();
    std::cout << "num = " << num << std::endl;

    return 0;
}

recursive_mutexmutex的用法完全一致,以上代码中只需要把mutex换为recursive_mutex就可以避免死锁。

因为mutex.lock()时,如果申请不到锁,不论是谁占有这把锁,都会陷入阻塞,直到锁被释放。recursive_mutex则会记录是谁占有这把锁,recursive_mutex.lock()时,会检查申请锁的线程和占有锁的线程是不是同一个,如果是同一个,则直接申请成功,因此可以避免递归死锁。


lock_guard

在使用锁的过程中,最忌讳的就是忘记解锁,这就会导致一个线程一直持有锁,其他线程无法访问到资源。但是难道每次用完锁后解锁,就可以保证锁被释放吗?

看到这个例子:

void test(int n, std::mutex& mtx)
{
    mtx.lock();
    // 抛异常
    mtx.unlock();
}

C++作为一门面向对象语言,带有异常机制,一旦抛出异常,就会直接结束函数栈帧,一直跳转到cache。以上代码中,如果抛异常了,那么mtx.unlock()根本就不会执行,导致锁没法释放。

因此C++引入了RAII机制来管理锁,利用对象的生命周期来实现加锁和解锁,原理如下:

template <typename mutex_type>
class LockGuard
{
public:
    LockGuard(mutex_type& lock)
        : _lk(lock)
    {
        _lk.lock();
    }

    ~LockGuard()
    {
        _lk.unlock();
    }

private:
    mutex_type& _lk;
};

void test(int n, std::mutex& mtx)
{
    LockGuard<std::mutex> guard(mtx);
    // 抛异常
}

LockGuard这个类,在构造时接受一个锁,随后加锁,在析构时自动解锁。那么加锁的时间就与对象的生命周期绑定了。而就算经过异常退出,对象也会正常析构,从而加锁。

标准库std::lock_guard就是这个原理,其接收一个锁类型的模板参数,在构造中调用lock,析构中调用unlock,完成对锁的自动管理。

void test(int n, std::mutex& mtx)
{
    std::lock_guard<std::mutex> guard(mtx);
    // 抛异常
}

使用了lock_guard后,就不用再自己手动解锁了。


unique_lock

lock_guard的可操作性很低,只有构造和析构两个函数,也就是只有自动释放锁的能力。而unique_lock功能更加丰富,而且可以自由操作锁。

unique_lock在构造时,可以传入一把锁,在构造的同时会对该锁进行加锁。在unique_lock析构时,判断当前的锁有没有加锁,如果加锁了就先释放锁,后销毁对象。

而在构造与析构之间,也就是整个unique_lock的生命周期,可以自由的加锁解锁:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回
  • try_lock_until:如果到指定时间还没申请到锁就返回false,申请到锁返回true
  • try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true

提供了以上五个接口,也就是说可以作用于前面的任何一款锁。另外的unique_lcok还允许赋值operator=调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok,自己不再持有锁


atomic

在多线程情况下要加锁,就是因为很多操作不是原子性的。但是有一些简单的操作,比如num++,每次都加锁解锁,性能必然会降低。因此C++又提供了原子库<atomic>,其实现了简单操作的原子化,一些简单的++、--等都实现了原子化,可以不加锁也没有线程安全,需要头文件<atomic>

atomic

支持的类型如下:

  1. 可以通过简单的拷贝完成复制,而不需要调用构造函数与拷贝构造等
  2. 类型的大小不超过 std::atomic 的内部实现所支持的最大大小(通常是与机器字大小相同)

最常见的满足以上要求的类型就是内置类型,比如char,各种整型,浮点型,以及指针。

使用起来也很简单:

atomic<类型> 变量名;

这样即可定义一个原子的类型。

示例:

std::atomic<int> num = 0;

void test(int n)
{
    for (int i = 0; i < n; i++)
    {
        num++;
    }
}

int main()
{
    std::thread t1(test, 2000);
    std::thread t2(test, 2000);

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

以上是一个多线程代码,但是我们并没有加锁,却是线程安全的,因为num是一个atomic<int> 类型的变量,num++是一个原子操作。

atomic类的成员函数如下:

在这里插入图片描述

首先就是实现了operator++operator--,自增自减的操作是原子的。

再比如说fetch_add,用于实现对一个原子类型增加指定值,该过程也是原子的。

std::atomic<int> num = 3;
num.fetch_add(5);

以上代码完成了3 + 5的计算,且过程是原子性的,其余操作也是类似的:

  • fetch_add:原子性,增加指定的值
  • fetch_sub:原子性,减少指定的值
  • fetch_and:原子性,与指定值按位与
  • fetch_or:原子性,与指定值按位或
  • fetch_xor:原子性,与指定值按位异或

还有一些其它接口:

在这里插入图片描述

store用于设定原子类型为指定值:

std::atomic<int> num = 3;
num.store(100);

num.store(100)相当于num = 100,但是过程是原子性的。

load用于获取原子类型当前的值,也是原子的。

operator T是隐式类型转换,也就是从atomic<T>转化为T类型,此时就可以把原子类型当作一般类型来使用了,不过要注意的是,隐式转换后就是一般类型,不再具有原子性了


CAS

C++之所以可以实现变量的原子操作,是基于CAS的原子操作,这是一个硬件级别的操作,其涉及三个操作数:

  1. 内存位置
  2. 预期值
  3. 更新值

操作流程为:读取内存位置的当前值,判断是否与预期值相等,如果相等,将其变为更新值,如果不相等,返回当前值

比如在gcc编译器中,内置了函数__sync_bool_compare_and_swap,其用于进行CAS操作:

bool __sync_bool_compare_and_swap(type* ptr, type oldval, type newval);
  1. ptr:内存位置,指向要操作的变量
  2. oldval:预期值,即预计该变量原先的值
  3. newval:更新值,希望把这个变量设置的值

返回值:如果修改成功返回true,修改失败返回false

比如通过CAS实现一个原子的自增:

while(__sync_bool_compare_and_swap(&x, x, x + 1) == false);

这样短短一行代码就可以实现原子自增,首先读取&x,获取x的地址,随后传入变量x的当前值,预期值传入x + 1,表示自增。

比如说在&x后读取到了x的当前值为5,的此时另一个线程打断了操作,修改x = 10。进入函数__sync_bool_compare_and_swap后,发现预期值 = 5,而当前x = 10,说明被其他线程修改了,直接返回false表示修改失败,进入下一轮while循环。

也就是说基于CAS实现的原子性,不是真的原子性,而是检测到在修改变量的过程中,有其它人来修改了变量,就终止操作防止线程安全错误。

这个函数在C/C++里面是没法直接使用的,而是内置在编译器中,这是因为这个函数绕过了操作系统,直接与处理器的指令交互,因为CAS操作要非常迅速,否则就会出现相互打断的问题。直接通过编译器与处理器的原子指令交互,比通过操作系统内核要快得多。


condition_variable

谈到锁,自然也要谈条件变量,这是线程同步的重要手段,C++将条件变量放在头文件<condition_variable>中。

condition_variable

condition_variable只有一个无参的构造函数,且删除了拷贝构造,不允许拷贝。

  • 等待:
  1. wait:进入条件变量的等待队列
  2. wait_for:进入条件变量的等待队列,一定时间后如果没有被唤醒,则不再等待返回false
  3. wait_until:进入条件变量的等待队列,到指定时间后如果没有被唤醒,则不再等待返回false

第一个wait需要传入一把锁unique_lock<mutex>,此处要求必须使用unique_lock<mutex>。而后续两个与时间相关的等待,分别要传入时间段duration和时间点time_point。至于为什么要传入一把锁,这属于并发编程的知识,就不在博客中讲解了。

  • 唤醒:
  1. notify_one:唤醒等待队列的第一个线程
  2. notify_all:唤醒等待队列的所有线程

示例:让两个线程从0 - 100,轮流输出奇数偶数。

int n = 0;
bool flag = true;

std::mutex mtx;
std::condition_variable cv; // 条件变量

void func(bool run) // run用于标识是否轮到当前线程输出
{
    while (n < 100)
    {
        std::unique_lock<std::mutex> lock(mtx);

        while (flag != run) // 使用while代替if,防止伪唤醒
            cv.wait(lock);  // 没轮到当前线程,进入条件变量等待

        std::cout << n << std::endl;
        n++;
        flag = !flag;
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(func, true);  // falg == true 输出偶数
    std::thread t2(func, false); // falg == false 输出奇数

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

    return 0;
}

这就是一个简单的,两个线程通过条件变量相互制约的案例,展示了condition_variable的基础用法。

再回到wait函数,wait的声明如下:

void wait (unique_lock<mutex>& lck);

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

其有两个重载,第一个只有一个参数,也就是我刚刚提到的只要传入一个unique_lock<mutex>。第二个重载允许传入第二个参数pred,这是一个可调用对象,用于作为条件变量的判断值。

wait的第二个参数要求是一个可调用对象,返回值类型伪bool,作用如下:

  • 返回true:表示条件成立,wait直接返回,不进入等待队列
  • 返回false:表示条件不成立,wait阻塞,进入等待队列直到被唤醒

在刚刚的案例中,以下语句负责控制条件变量:

while (flag != run)
	cv.wait(lock); 

实际上在condition_variable中,无需这样写判断语句,而是可以通过可调用对象传入条件变量内部:

void func(bool run)
{
    auto cond_func = [&](){
        return flag == run;
        };

    while (n < 100)
    {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, cond_func);

        std::cout << n << std::endl;
        n++;
        flag = !flag;
        cv.notify_one();
    }
}

此处给wait函数第二个参数传入了一个lambda表达式cond_func,其返回一个boolflag == run,当这个值为true说明轮到当前线程执行,也就是条件成立,当前线程不会进入等待队列。

另外的,在等待队列的线程被唤醒后,也会触发一次该函数的条件判断,防止伪唤醒


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2135360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

上班炒股会被开除吗?公司是如何发现员工上班炒股?一文告诉你答案!

随着互联网金融的发展&#xff0c;股票交易变得越来越便捷&#xff0c;不少上班族选择利用工作之余的时间来进行股票投资。 然而&#xff0c;这种行为是否合规&#xff1f;公司又是如何发现并处理这种情况的呢&#xff1f;本文将为您解答这些问题。 一、上班炒股是否合规&…

JAVA毕业设计175—基于Java+Springboot+vue3的医院预约挂号管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvue3的医院预约挂号管理系统(源代码数据库)175 一、系统介绍 本项目前后端分离(可以改为ssm版本)&#xff0c;分为用户、医生、管理员三种角色 1、用户&#x…

交换机最常用的网络变压器分为DIP和SM

华强盛电子导读&#xff1a;交换机通用网络变压器插件48PIN最为常见 您好&#xff01;今天我要给您介绍一款真正能为您的工业生产带来变革的产品——华强盛工业滤波器。在如今这个高度数字化的工业时代&#xff0c;可靠的网络连接至关重要&#xff0c;而华强盛工业网络变压器就…

Smartbi体验中心新增系列Demo,用户体验更丰富

为进一步提升用户体验&#xff0c;让大家更直观地了解Smartbi产品在数据分析方面的功能优势&#xff0c;Smartbi体验中心近期新增了一系列Demo。这些更新旨在优化产品操作流程&#xff0c;并为用户提供更多真实场景下的应用参考。接下来&#xff0c;我们一起简要浏览此次体验中…

KEIL仿真时弹窗 “Cannot access target.”

现象 仿真时&#xff0c;点击暂停就会弹出下图窗口。 Cannot access target. Shutting down debug session. 解决方法 开启STM32的Debug&#xff0c;如下图。

基于WIFI的开关控制器设计与实现

本设计基于STC89C52RC单片机设计的WiFi开关器系统&#xff0c;旨在通过软硬件设计实现按键和手机APP远程同步控制4个继电器驱动3个LED灯、1个风扇&#xff0c;并且具备时间显示、开关状态显示、定时驱动&#xff0c;时间设定及保存等功能。该设计在硬件方面采用STC89C52单片机作…

软考中项(第三版) 项目成本管理总结

前言 系统集成项目管理工程师考试&#xff08;简称软考中项&#xff09;&#xff0c;其中案例分析也是很大一部分考试内容&#xff0c;目前正在学习中&#xff0c;现总结一些可能会考到的知识点供大家参考。 1.1、项目成本管理总线索 1、项目成本失控的原因 &#xff08;1&a…

python库安装失败问题

pip install XXXX 报错信息如下 D:\Dev>pip install D:\Dev\robotlib-0.0.33.tar.gz DEPRECATION: Loading egg at d:\app\dev\python\lib\site-packages\fs11a3_package-1.3-py3.11.egg is deprecated. pip 24.3 will enforce this behaviour change. A possible replace…

【机器学习】使用Numpy实现神经网络训练全流程

文章目录 网络搭建前向传播反向传播损失计算完整代码 曾经在面试一家大模型公司时遇到的面试真题&#xff0c;当时费力写了一个小时才写出来&#xff0c;自然面试也挂了。后来复盘&#xff0c;发现反向传播掌握程度还是太差&#xff0c;甚至连梯度链式传播法则都没有弄明白。 网…

solidity-19-fallback

接收ETH receive和fallback receive和callback是solidity中两个特殊的回调函数&#xff0c;一个处理接收ETH,一个处理不存在的函数调用。本质上就是吧fallback拆成了两个回调函数。我暂时不知道什么是fallback fallback调用不存在的函数时会被调用也就是这个函数是不是等价于…

视频转音频,分享这六种转换操作

视频转音频&#xff0c;随着多媒体技术的发展&#xff0c;人们越来越频繁地需要将视频中的音频部分提取出来单独使用。无论是为了制作播客、获取音乐片段还是其他需求&#xff0c;视频转音频都是一项非常实用的技能。为了让你轻松应对各种场合的需求&#xff0c;下文将为你详细…

day-55 不同路径

思路 动态规划&#xff1a;因为只能向右或向下移动&#xff0c;可以得出状态转换方程&#xff1a;dp[i][j]dp[i-1][j]dp[i][j-1] 解题过程 直接令第一行和第一列全为1&#xff0c;然后通过状态转换方程进行计算&#xff0c;返回dp[m-1][n-1]即可 Code class Solution {publi…

Centos挂载和删除nfs

一、Centos挂载nfs 1、安装NFS客户端软件 sudo yum install nfs-utils 2、 创建一个挂载点目录 mkdir -p /mnt/nfs 注意:目录可以随意创建 3、永久挂载nfs 即系统在每次启动后自动挂载NFS共享 (1)编辑 /etc/fstab vim /etc/fstab (2)添加nfs <nfs_server_ip&…

AI算法盒如何精准守护你的安全区域

在当今智能化时代&#xff0c;安全防范已成为社会各个领域的核心需求之一。万物AI算法盒&#xff0c;作为前沿科技的集大成者&#xff0c;其内置的区域人员入侵检测视觉算法&#xff0c;以卓越的性能和广泛的应用场景&#xff0c;为各行各业提供了高效、精准的安全解决方案。 核…

使用iperf3测试局域网服务器之间带宽

文章目录 一、下载安装1、windows2、centos 二、使用0、参数详解1、centos 一、下载安装 1、windows https://iperf.fr/iperf-download.php 拉到最下面选最新版&#xff1a; 2、centos yum install iperf3二、使用 0、参数详解 服务器或客户端&#xff1a; -p, --port #…

喧嚣漫天之际,重新审视以太坊的定位与路线图

价值捕获很重要&#xff0c;但现在讨论为时尚早。 作者&#xff1a;Mike Neuder&#xff08;以太坊基金会研究员&#xff09;&#xff1b;译者&#xff1a;Azuma&#xff1b;编辑&#xff1a;郝方舟 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 编者按…

Centos7通过Docker安装openGauss5.0.2并配置用户供Navicat连接使用

下载镜像 [rootiZ2ze3qc9ouxm10ykn3cvdZ ~]# docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/enmotech/opengauss:5.0.2 5.0.2: Pulling from ddn-k8s/docker.io/enmotech/opengauss 2ec76a50fe7c: Pull complete e48b50219b49: Pull complete 512e203af4…

万龙觉醒免费脚本,自动打金挂机!VMOS云手机辅助攻略!

《万龙觉醒》作为一款策略类手游&#xff0c;玩家需要在多个方面进行资源管理和战斗部署。为了更加高效地进行游戏&#xff0c;推荐使用VMOS云手机。通过VMOS云手机&#xff0c;你可以体验到游戏专属定制版的云手机&#xff0c;它内置游戏安装包&#xff0c;省去了重新下载安装…

102.WEB渗透测试-信息收集-FOFA语法(2)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;101.WEB渗透测试-信息收集-FOFA语法&#xff08;1&#xff09; FOFA使用实例 • title&q…

关于若依flowable的安装

有个项目要使用工作流功能&#xff0c;在网上看了flowable的各种资料&#xff0c;最后选择用若依RuoYi-Vue-Flowable这个项目来迁移整合。 一、下载项目代码&#xff1a; 官方项目地址&#xff1a;https://gitee.com/shenzhanwang/Ruoyi-flowable/ 二、新建数据库&#xff…