当前位置: 首页 > news >正文

C++ 11 【线程库】【包装器】

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:C++修炼之路

🚚代码仓库:C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

 

目录

前言

一、thread类的简单介绍  

get_id

​编辑

构造函数 

​编辑

joinable

 join​编辑

native_handle 

detach 

 std::thread::operator=​编辑

 std::thread::swap​编辑

二、mutex 

 mutex

 recursive_mutex

三、RAII 风格的锁 

 四、条件变量 condition_variable

等待函数:

通知函数:

交替打印数字 

 五、原子类 atomic

 load、store ​编辑

六、包装器

 bind


前言

在C++11之前由于没有线程库,这就导致了在Linux能跑的代码,在windows下就不行,反之也是一样。导致了代码的可移植性差!!!而C++11之后出现了线程库,并行编译时不需要依赖第三方库。而且在原子操作中还引入了原子类的概念。

一、thread类的简单介绍  

类定义

std::thread是C++标准库中的一个类,用于表示和控制线程。它允许你创建、管理、同步线程,并且可以与操作系统的线程进行交互。

成员类型

  • id: 线程的唯一标识符。它是一个类型为std::thread::id的公共成员类型,可以用来比较不同线程是否相同。
  • native_handle_type: 表示线程的原生操作系统句柄。这个类型是平台相关的,可以用来与操作系统的API进行交互。

成员函数

  • 构造函数: 可以以多种方式构造std::thread对象,比如传递一个可调用对象(函数、lambda表达式、函数对象等)作为参数。
  • 析构函数: 当std::thread对象被销毁时,如果它是一个可加入的线程并且没有被joindetach,那么析构函数会调用std::terminate,这将终止程序。
  • 移动赋值操作符 operator=: 允许线程对象之间的所有权转移。移动之后,源线程对象将不再代表一个活跃的线程。
  • get_id: 返回当前线程的唯一标识符。
  • joinable: 检查线程是否可加入。如果线程已经加入或者分离,返回false
  • join: 等待线程结束。如果线程已经结束了,调用join将不会做任何事情。
  • detach: 分离线程,使其在完成时不会自动销毁。分离后的线程必须由操作系统来管理。
  • swap: 交换两个线程对象的内部状态,使得一个线程对象代表另一个线程的执行。
  • native_handle: 返回线程的原生句柄,这可以用来直接与操作系统的线程管理功能交互。
  • hardware_concurrency: 静态成员函数,返回硬件支持的线程并发数量,即可以同时运行的线程数。

非成员函数

  • swap: 一个非成员函数,用于交换两个std::thread对象的线程。它提供了与成员swap函数相同的功能,但是可以用于按值传递线程对象。

我们先来一个传统的写法

#include <iostream>
#include <thread>
using namespace std;
void Func(int n, int num)
{for (int i = 0; i < num; i++){cout <<"线程:"<< n << " " << i << endl;}cout << endl;}
int main()
{thread t1(Func, 1, 20);thread t2(Func, 2, 30);t1.join();t2.join();return 0;
}

线程的ID如何获取?

get_id

在C++标准库中,std::thread::get_id是一个公共成员函数,属于std::thread类。这个函数用于获取与std::thread对象关联的线程的唯一标识符(thread id)。以下是关于std::thread::get_id成员函数的详细信息:

函数原型

id get_id() const noexcept;

功能描述

std::thread::get_id函数返回一个std::thread::id类型的值,该值是线程的唯一标识符。

行为

  • 如果std::thread对象是可加入的(joinable),即它代表了一个活跃的线程,那么get_id函数将返回一个值,该值唯一地标识了这个线程。
  • 如果std::thread对象不是可加入的,例如它是默认构造的或者已经被移动(move)了,那么get_id函数将返回一个默认构造的对象,该对象不代表任何活跃的线程。

参数

无(该函数不接受任何参数)。

返回值

  • 当线程对象是可加入的,返回一个std::thread::id类型的值,它唯一标识了线程。
  • 当线程对象不是可加入的,返回一个默认构造的std::thread::id对象。

异常安全性

get_id函数被声明为noexcept,这意味着它保证不会抛出异常。

 代码示例

#include <iostream>
#include <thread>
using namespace std;
#include <vector>
int main()
{int m, n;cin >> m >> n;vector<int> arr;arr.push_back(m);arr.push_back(n);vector<thread> vthds(m);for (int i = 0; i < arr[0]; i++){vthds[i] = thread([i,&arr,&vthds](){for (int j = 0; j< arr[1]; j++){cout << "线程:" << vthds[i].get_id() << " " << "j:" << j << endl;}cout << endl;});}for (auto& t : vthds){t.join();}return 0;
}

但是我们一般都不会用对象去调用,而是用this_thread

std::this_thread 是 C++ 标准库 <thread> 中定义的一个命名空间,它包含了一组与当前线程相关的函数。这些函数提供了对当前线程的访问和控制,允许开发者执行如休眠当前线程、获取当前线程的ID等操作。以下是 std::this_thread 命名空间中的一些常用功能:

成员函数

  • sleep_for: 使当前线程暂停执行指定的时间长度。例如,std::this_thread::sleep_for(std::chrono::seconds(1)); 会使当前线程休眠一秒。
  • sleep_until: 使当前线程休眠直到达到某个指定的时间点。
  • yield: 暗示调度器当前线程愿意让出对处理器的使用,调度器可以选择另一个线程来运行。
  • get_id: 返回一个标识当前线程的 std::thread::id 类型的唯一标识符。

 原子操作 CAS 是一个不断重复尝试的过程,如果尝试的时间过久,就会影响整体效率,因为此时是在做无用功,而 yield 可以主动让出当前线程的时间片,避免大量重复,把 CPU 资源让出去,从而提高整体效率 具体实现原理可以看陈浩大佬无锁队列的实现 | 酷 壳 - CoolShell

 

构造函数 

  1. 默认构造函数

    当你使用 std::thread 的默认构造函数时,你得到的是一个没有关联任何线程的线程对象。这相当于一个空壳,没有实际的执行内容。这个构造函数通常用于那些可能但不一定需要与线程关联的情况。
  2. 初始化构造函数

    这种构造函数允许你创建一个新的线程对象,并立即启动一个线程执行指定的函数或任务。你可以传递函数和相应的参数给构造函数,这些参数将被复制或移动到新线程中(根据它们的值类别,即左值或右值)。重要的是,新线程的执行开始于构造函数完成时,这意味着一旦线程对象被创建,它就开始执行指定的任务。
  3. 复制构造函数(已删除):

    C++中的 std::thread 对象不支持复制语义。这意味着你不能通过简单的复制操作来创建线程对象的副本。这是因为复制线程对象可能导致多个对象尝试管理同一个底层线程,这会带来同步和生命周期管理上的复杂性。
  4. 移动构造函数

    移动构造函数允许你将一个线程对象的所有权转移给另一个线程对象。这通常发生在你需要重新分配线程资源时。例如,如果你在一个线程对象中创建了一个线程,但后来决定将其转移给另一个线程对象来管理,你可以使用移动构造函数来实现这一点。转移后,原始线程对象将不再关联任何线程,而新的对象将接管线程的执行。
  5. 销毁

    当一个 std::thread 对象的生命周期结束时,如果它是一个可加入的线程(即它通过初始化构造函数创建并启动了一个线程),它必须被适当地处理。你可以通过调用 join() 方法来等待线程完成其任务,或者通过调用 detach() 方法来分离线程,使其在没有管理的情况下继续执行。如果可加入的线程对象在销毁前既没有被加入也没有被分离,程序将调用 std::terminate,这将导致程序立即终止。

joinable

 std::thread::joinable 是 C++ 标准库 <thread>std::thread 类的一个成员函数,它用于检查线程对象是否可加入。以下是对这个函数的详细解释:

函数原型

bool joinable() const noexcept;

功能描述

joinable() 函数返回一个布尔值,指示线程对象是否可加入。如果线程对象关联了一个线程执行流,并且该线程尚未结束,那么它就是可加入的。

行为

  • 可加入的线程对象:如果线程对象在创建时通过初始化构造函数与一个新线程关联,或者它通过移动构造函数从另一个线程对象那里接管了线程执行,那么它是可加入的。
  • 不可加入的线程对象:如果线程对象是以下情况之一,它将不可加入:
    • 使用默认构造函数创建的,没有关联任何线程执行。
    • 通过移动操作从其他线程对象转移而来,原对象不再关联任何线程。
    • 对象的 join() 或 detach() 成员函数已经被调用过,线程已经完成或分离。

注意事项

  • joinable() 函数被声明为 noexcept,意味着它保证不会抛出异常。
  • 在调用 join() 或 detach() 之前,使用 joinable() 进行检查是一种良好的编程实践,可以避免对已经结束或分离的线程执行非法操作。

#include <iostream>
#include <thread>void thread_function() {// 线程执行的代码
}int main() {std::thread t(thread_function);// 等待线程结束if (t.joinable()) {t.join(); // 等待线程完成} else {std::cout << "Thread is not joinable." << std::endl;}return 0;
}

在这个示例中,我们首先创建了一个线程 t。然后,我们检查它是否可加入。如果是,我们调用 join() 等待线程结束。如果线程不可加入,我们将输出一条消息说明这一点。

总结

std::thread::joinable 是一个重要的成员函数,它提供了一种机制来判断线程对象是否可以安全地调用 join()detach() 函数。正确使用 joinable() 可以帮助避免多线程编程中的常见错误和潜在的资源管理问题。

 join

std::thread::join 是 C++ 标准库中 <thread> 头文件定义的 std::thread 类的一个成员函数,用于等待由 std::thread 对象表示的线程完成其执行。以下是对该函数的详细说明:

函数原型

void join();

功能描述

join() 函数的作用是等待当前 std::thread 对象所关联的线程执行完成。调用此函数的线程(通常是主线程或其他线程)将被阻塞,直到 std::thread 对象所代表的线程终止。

行为

  • 当 join() 被调用时,如果 std::thread 对象所关联的线程尚未结束,调用线程将等待直到该线程完成其所有操作。
  • join() 函数与线程完成的所有操作同步,这意味着一旦 join() 返回,被等待线程中的所有工作都已经完成。

效果

  • 在 join() 函数调用后,std::thread 对象将变为不可加入状态。这意味着你不能再次对同一个线程对象调用 join() 或 detach()
  • 一旦线程对象变为不可加入状态,它就可以被安全地销毁,因为它不再关联任何活跃的线程。

注意事项

  • 如果 std::thread 对象是默认构造的,或者已经被移动到另一个 std::thread 对象,或者已经调用过 join() 或 detach(),则 join() 函数将立即返回,不会产生阻塞效果。
  • 在多线程程序中,合理使用 join() 可以确保线程的执行结果被正确处理,并且线程资源得到适当释放。

native_handle 

std::thread::native_handle() 是 C++ 标准库 <thread>std::thread 类的一个成员函数,它用于获取与线程对象关联的原生线程句柄。以下是对这个函数的详细解释:

函数原型

native_handle_type native_handle();

功能描述

native_handle() 函数返回一个特定于实现的值,这个值提供了对底层操作系统线程表示的访问。这个原生句柄可以用于直接与操作系统的线程管理功能交互,例如查询线程状态或执行特定于平台的线程操作。

行为

  • 此函数只有在库的实现支持时才存在于 std::thread 类中。
  • 它返回一个 thread::native_handle_type 类型的值,这个值是特定于实现的,并且可以用来操作底层线程。

参数

  • 此函数不接受任何参数。

返回值

  • 返回一个 thread::native_handle_type 类型的值,表示线程的原生句柄。

注意事项

  • native_handle() 函数的使用可能会涉及未指定的数据竞争和异常安全性问题。使用此函数时,需要确保对线程的访问是同步的,并且考虑到可能的异常安全问题。
  • 由于 native_handle_type 是特定于实现的,它的具体类型和使用方法将依赖于编译器和操作系统。因此,使用 native_handle() 可能需要特定平台的编程知识。

这个了解就好了。 

detach 

std::thread::detach 是 C++ 标准库 <thread> 头文件中 std::thread 类的一个成员函数,用于分离线程对象。以下是对这个函数的详细解释:

函数原型

void detach();

功能描述

detach() 函数将 std::thread 对象所代表的线程与调用它的线程分离,允许这两个线程独立执行。分离操作意味着两个线程将继续它们的执行,而不会相互阻塞或同步。

行为

  • 分离线程后,原线程对象不再控制或等待被分离的线程结束。
  • 如果分离的线程结束执行,它的资源将被操作系统自动释放。
  • 如果原线程(调用 detach() 的线程)在分离的线程结束之前结束,分离的线程将继续运行直到它完成执行,然后由操作系统释放它的资源。

效果

  • 调用 detach() 后,std::thread 对象变为不可加入状态,即你不能对这个对象调用 join()
  • 不可加入的线程对象可以被安全地销毁,因为它不再持有对任何活跃线程的引用。

注意事项

  • 分离线程是一个不可逆的操作。一旦调用了 detach(),你将无法再等待或加入这个线程。
  • 如果分离的线程在执行过程中发生异常,调用 detach() 的线程将不会得到通知,异常也不会传播到调用线程。
  • 在分离线程之前,确保线程的执行不会导致资源泄漏或未完成的任务。
#include <iostream>
#include <thread>void thread_function() {std::cout << "线程正在执行工作..." << std::endl;// 模拟一些工作负载std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "线程工作完成。" << std::endl;
}int main() {std::thread t(thread_function);// 分离线程t.detach();// 主线程继续执行,不会等待分离的线程结束std::cout << "主线程继续执行,不会等待线程 " << t.get_id() << " 结束。" << std::endl;// 由于线程已经被分离,这里调用 join() 将无效果// t.join(); // 这行代码将导致未定义行为return 0;
}

 

总结        

std::thread::detach 是一个重要的成员函数,它提供了一种机制来分离线程,使得线程可以独立于创建它的线程执行。使用 detach() 可以避免不必要的同步等待,但也需要谨慎使用,以确保资源得到正确管理。

 std::thread::operator=

功能描述

移动赋值操作符允许一个std::thread对象(通常称为右侧对象,rhs)将其线程执行的所有权转移给另一个std::thread对象(通常称为左侧对象,*this)。这个操作是C++移动语义的一部分,用于高效地重新分配资源。

行为

  • 如果左侧对象(*this)当前不是可加入的(即它不关联任何线程或者已经分离了线程),它将接管右侧对象(rhs)所代表的线程执行。这包括关联的线程和其所有状态。

  • 如果左侧对象是可加入的(即它关联了一个活跃的线程),则调用terminate()函数。这将尝试立即终止线程的执行,这可能导致资源未被正确释放或数据不一致。
  • 移动赋值操作之后,右侧对象(rhs)将不再代表任何线程执行。它就像使用默认构造函数创建的对象一样,不关联任何线程。

注意事项

  • 移动赋值操作符是不可逆的。一旦执行,原对象的状态将被清除,并且不能再用来控制或同步线程。
  • std::thread对象不能被复制,只能被移动。这意味着没有复制构造函数,只有移动构造函数和移动赋值操作符。
  • 在移动赋值之后,原对象应该被视为无效,并且不应该再被使用来控制线程。
#include <iostream>
#include <thread>void thread_function() {std::cout << "线程正在执行工作..." << std::endl;// 模拟一些工作负载std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "线程工作完成。" << std::endl;
}int main() {std::thread t1(thread_function); // 创建并启动线程std::thread t2;                // 创建一个空的线程对象// 移动t1中的线程所有权到t2t2 = std::move(t1); // t1 现在不再代表任何线程// 等待t2中的线程完成if (t2.joinable()) {t2.join();}// 尝试使用已经移动的t1将导致未定义行为// if (t1.joinable()) {//     t1.join(); // 这将是错误的使用// }return 0;
}

 std::thread::swap

函数原型

void swap(std::thread& x);

功能描述

swap 函数交换调用对象(*this)和参数 x 的线程状态。这意味着两个 std::thread 对象关联的线程执行将被交换。

行为

  • 如果调用对象和 x 都是可加入的,它们关联的线程将被交换。
  • 如果其中一个或两个是不可加入的(例如,它们已经被分离或默认构造的),那么交换操作将只影响可加入的线程对象。

参数

  • x:要与当前对象交换状态的 std::thread 对象。

返回值

  • 无(void 类型)。

thread类的成员函数介绍完了 ,最先开始的代码打印错乱。 显示器也是临界资源,两个线程共享临界资源,同时往显示器上打印就会出现错乱的问题。这时候我们就需要用到锁了。

二、mutex 

 

互斥体类型(Mutex Types)

这些是用于保护代码的关键部分以实现互斥访问的可锁类型:

  • mutex:基本的互斥体,提供独占访问。
  • recursive_mutex:递归互斥体,允许同一个线程多次锁定它。
  • timed_mutex:带超时功能的互斥体,尝试锁定时可以指定超时时间。
  • recursive_timed_mutex:递归带超时功能的互斥体,结合了上述两种特性。

锁类型(Locks)

这些对象管理互斥体的锁定状态,并将其与对象的生命周期相关联:

  • lock_guard:当构造时锁定互斥体,在析构时自动解锁。它提供了作用域锁定。
  • unique_lock:与lock_guard类似,但提供了更多的灵活性,例如尝试锁定和锁定超时。

函数

这些函数用于更高级的锁定操作:

  • std::lock:原子地锁定多个互斥体,防止死锁。
  • std::try_lock:尝试锁定一个或多个互斥体,如果无法立即锁定,则可以不阻塞地失败。
  • std::call_once:确保某个函数(通常用于初始化)在程序的生命周期内只被调用一次,即使多次请求也是如此。

上面的锁,我们一步一步的来,先说mutex的lock。

 mutex

#include <vector>
#include <mutex>
int main()
{int m, n;cin >> m >> n;vector<int> arr;mutex mtx;arr.push_back(m);arr.push_back(n);vector<thread> vthds(m);for (int i = 0; i < arr[0]; i++){vthds[i] = thread([i,arr,&mtx](){mtx.lock();for (int j = 0; j< arr[1]; j++){cout << "线程:" << this_thread::get_id()<< " " << "j:" << j << endl;this_thread::sleep_for(chrono::milliseconds(200));}mtx.unlock();cout << endl;});}for (auto& t : vthds){t.join();}return 0;
}

这里从运行结果来看我们打印的结果没有错乱了。但是这些线程是并行在跑,并不是我们想要的。 

我们再来看一段代码

 

void Func(int n)
{// 并行for (int i = 0; i < n; i++){	++x;}
}
int main()
{int n = 10000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << x << endl;return 0;
}

当我们的n较小时,x++为正确,如果x的值比较大又会是什么样? 比如n为100000

 

结果超出我们预期。这里的x是全局变量,和显示器一样都是临界资源。两个线程同时对它++就会出现线程安全的问题,这时我们也是需要加锁。 

  这里就有个问题了如何加锁?     

 

 

理论来说并行是要比串行快的,这里串行快是因为代码比较简单while 循环内只需要进行 ++x 就行了,并行化中频繁加锁、解锁的开销要远大于串行化单纯的进行 while 循环

,如果多来点IO操作,就会发现并行快 。

 #include<mutex>int main()
{int n = 100000;int x = 0;mutex mtx;size_t begin = clock();thread t1([&,n](){mtx.lock();for (int i = 0; i < n; i++){cout << &x << endl;cout << &n << endl;++x;}mtx.unlock();});thread t2([&,n]() {for (int i = 0; i < n; i++){cout << &x << endl;cout << &n << endl;mtx.lock();++x;mtx.unlock();}});t1.join();t2.join();size_t end = clock();cout << x << endl;cout << end - begin << endl;return 0;
}

 

这时我们就加了两条打印语句,两者差距就很接近了。如果还有其他不涉及临界资源的语句,明显并行就快了。因为没有锁的情况,其他线程是可以执行其他语句的。但串行只能拿到锁后才能执行。 

这个大家根据实际的场景来选择加锁的位置。 

 recursive_mutex

 

#include<mutex>int x = 0;
recursive_mutex mtx;void Func(int n)
{if (n == 0)return;mtx.lock();++x;Func(n - 1);mtx.unlock();
}int main()
{thread t1(Func, 10000);thread t2(Func, 20000);t1.join();t2.join();cout << x << endl;return 0;
}

上面这种代码就是典型的死锁问题,即使我们把解锁放在++x后面也会出现栈溢出的问题。所以能写循环的尽量在循环体里面用互斥锁,递归互斥锁尽量不要用。

后面两种不常用,这里就不做过多讲解。

三、RAII 风格的锁 

上面都是需要我们自己手动加锁和解锁,万一我们有时候忘记加锁和解锁,即使我们没有忘记手动加锁和解锁,代码很容易出现死锁的问题。比如后面章节要讲解的异常,代码引入异常处理之后,如果是临界资源出现了问题,代码会跳转至 catch 中 捕捉异常。这时代码异常退出。而unlock还没有执行。锁的资源没有释放,这就导致死锁。

 


int main()
{int n = 100000;int x = 0;mutex mtx;thread t1([&, n]() {try {mtx.lock();for (int i = 0; i < n; i++){if (i % 2 == 0)throw exception("异常");++x;}mtx.unlock();}catch (const exception& e){cout << e.what() << endl;}});thread t2([&, n]() {for (int i = 0; i < n; i++){mtx.lock();++x;mtx.unlock();}});t1.join();t2.join();size_t end = clock();cout << x << endl;return 0;
}

 上面的代码出现的死锁的问题,就导致t2这线程一直在申请锁资源,但是一直申请不到锁。进程卡卡住。

 修改之前的代码,不用自己手动加锁和解锁

 

 文档是关于对lock_guard介绍。

说人话:这里lock_guard利用类的特性,对象实例化时会自动调用构造函数,出对象作用域时会自动调用析构函数。而构造函数就是加锁,析构函数就是解锁。

还有一种  

lock_guardunique_lock都是C++11标准库中的互斥锁管理工具,用于简化互斥锁的使用和管理,它们都遵循RAII(资源获取即初始化)原则,确保在作用域结束时自动释放锁。尽管它们的基本功能相似,但它们之间存在一些关键的区别:

  1. 自动类型转换

    • lock_guard不提供对锁类型的自动转换。它需要在构造时显式指定互斥锁的类型。
    • unique_lock提供了对mutexrecursive_mutex的自动类型转换,允许使用相同的模板代码来锁定不同类型的互斥锁。
  2. 递归锁支持

    • lock_guard不支持递归互斥锁(recursive_mutex),因为它的设计不包括递归锁定的能力。
    • unique_lock可以与递归互斥锁一起使用,允许同一个线程多次锁定同一个递归互斥锁。
  3. 锁所有权转移

    • lock_guard不支持转移锁的所有权。一旦构造,它就会锁定互斥锁,并在销毁时自动解锁。
    • unique_lock允许通过移动语义转移锁的所有权。例如,可以将一个unique_lock对象的锁所有权移动到另一个unique_lock对象。
  4. 锁的尝试与释放

    • lock_guard不支持尝试锁定或手动释放锁。它在构造时锁定互斥锁,并在析构时解锁。
    • unique_lock提供了try_locktry_lock_fortry_lock_until等成员函数来尝试锁定互斥锁,以及release成员函数来手动释放锁,如果已经锁定的话。
  5. 使用场景

    • lock_guard适用于简单的锁管理,当你知道在作用域结束时需要释放锁,并且不需要尝试锁定或转移锁所有权时。
    • unique_lock适用于更复杂的场景,可能需要尝试锁定、定时锁定、递归锁定或转移锁所有权。
  6. 性能

    • 由于unique_lock提供了更多的功能,它可能比lock_guard有更多的运行时开销。然而,如果你需要这些额外的功能,unique_lock是更合适的选择。

总结来说,lock_guard是一个更简单的锁管理工具,适用于不需要额外功能的简单场景。而unique_lock提供了更多的灵活性和控制能力,适用于需要这些高级特性的复杂场景。

 四、条件变量 condition_variable

我们加入锁之后,线程之间互斥了,为了让他们同步,需要用到条件变量。

<condition_variable> 是 C++ 标准库中的一个头文件,它声明了与条件变量相关的类型和函数。条件变量是一种同步机制,用于在多线程编程中,让一个或多个线程等待某个条件为真,直到被另一个线程通知。 

等待函数:

  • waitvoid wait(std::unique_lock<std::mutex>& lock);:等待另一个线程的通知。调用此函数的线程必须已经通过 unique_lock 对一个互斥锁进行了加锁。当调用 wait 时,线程将释放互斥锁,并进入等待状态,直到被另一个线程通知。
  • wait_fortemplate <class Rep, class Period> cv_status wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& timeout_duration);:与 wait 类似,但增加了超时时间。如果在超时时间内没有收到通知,线程将退出等待状态。
  • wait_untiltemplate <class Clock, class Duration> cv_status wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& timeout_time);:与 wait_for 类似,但超时条件是指定的时间点,而不是持续时间。

通知函数:

  • notify_onevoid notify_one();:唤醒在该条件变量上等待的一个线程。如果有多个线程在等待,将唤醒其中一个线程。如果没有任何线程在等待,则不执行任何操作。
  • notify_allvoid notify_all();:唤醒所有在该条件变量上等待的线程。

std::condition_variable 对象在使用时,通常与 std::unique_lockstd::mutex 结合使用,以确保在等待和通知过程中对共享资源的访问是互斥的。使用条件变量可以避免竞态条件,并实现更高效的线程间通信

交替打印数字 

题目要求
给你两个线程 t1、t2,要求 t1 打印奇数,t2 打印偶数,数字范围为 [1, 100],两个线程必须交替打印

这里有两个问题:

1 如何确定t1先打印?

这个我们可以利用条件变量的wait函数的特性,如果一个线程等待,会自动释放锁。当x=1时是奇数,这时t1就一定会先运行。

2.如何让另一个线程不打印?

利用条件条件变量 分别让t1、t2满足条件进行阻塞等待。

#include <thread>
#include <condition_variable>
#include <mutex>
int x = 1;
int n = 100;
mutex mtx;
condition_variable cv;
void fucn1(int n)
{while(true){unique_lock<mutex> lck(mtx);if (x >= 100)break;if (x % 2 == 0) //是偶数就阻塞等待cv.wait(lck);cout << "t1:" << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}
}
void fucn2(int n)
{while(true){unique_lock<mutex> lck(mtx);if (x >= 100)break;if (x % 2 != 0) //是奇数就阻塞等待cv.wait(lck);cout << "t2:" << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}}
int main()
{thread t1(fucn1, n);thread t2(fucn2, n);t1.join();t2.join();return 0;
}

 

 五、原子类 atomic

 前面提到频繁的加锁、解锁会导致效率下降,比如上面的代码其实临界资源就只有一个x我们对它++或者-- 再或者位操作。有没有其他办法不用加锁?有的原子操作

<atomic> 是 C++ 标准库中的一个头文件,它提供了一系列用于实现原子操作的模板类和类型定义。原子操作是保证在多线程环境中安全执行的指令,不会出现数据竞争的问题。

类:

  • atomic:原子模板类,封装了一个可以原子地访问的值。这个模板支持多种数据类型,如 intfloatpointer 等,以及它们的无符号和长整型版本。
  • atomic_flag:原子标志类,用于实现低级别的同步操作,如自旋锁。

通用原子操作:

  • is_lock_free:检查原子操作是否无锁(即是否使用非阻塞算法)。
  • store:将一个值存储到原子对象中,可以指定内存顺序。
  • load:从原子对象中读取值,可以指定内存顺序。
  • operator T:获取原子对象中的值的副本。
  • exchange:交换原子对象中的值,并返回旧值。
  • compare_exchange_weak:弱比较并交换操作,用于实现原子条件赋值。
  • compare_exchange_strong:强比较并交换操作,同样用于原子条件赋值。

特定专业化支持的操作(例如整数和/或指针):

  • fetch_add:将一个值加到原子对象上,并返回原始值。
  • fetch_sub:从原子对象中减去一个值,并返回原始值。
  • fetch_and:对原子对象中的值应用按位与操作,并返回原始值。
  • fetch_or:对原子对象中的值应用按位或操作,并返回原始值。
  • fetch_xor:对原子对象中的值应用按位异或操作,并返回原始值。
  • operator++ 和 operator--:递增和递减原子对象中的值。
  • 复合赋值运算符:如 +=-=&=|=^= 等,提供复合赋值操作。

 

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>std::atomic<int> counter(0); // 线程安全的原子计数器int main() {const int num_threads = 4;std::vector<std::thread> threads;// 使用 lambda 表达式创建并启动线程for (int i = 0; i < num_threads; ++i) {threads.push_back(std::thread([=]() {int n = 100; // 每个线程递增的次数for (int j = 0; j < n; ++j) {// 使用 fetch_add 原子地递增 countercounter.fetch_add(1, std::memory_order_relaxed);}}));}// 等待所有线程完成for (auto& th : threads) {if (th.joinable()) {th.join();}}// 输出最终的计数器值std::cout << "Final counter value: " << counter << std::endl;return 0;
}

 这里使用 std::memory_order_relaxed 因为我们不关心操作的内存顺序,只要求操作是原子的。

 

 如果是用printf打印就会编译出错

 load、store 

因为counter是原子类型、而我们的是%d,类型不匹配。这时我们可以用load 

 

#include <atomic>
#include <iostream>int main() {std::atomic<int> atomicInt(0);// 使用 store 写入原子变量atomicInt.store(10, std::memory_order_relaxed); // 写入值10// 使用 load 读取原子变量int value = atomicInt.load(std::memory_order_acquire); // 读取原子变量的值std::cout << "Value of atomicInt: " << value << std::endl;return 0;
}

使用 load 的场景:

  1. 读取共享数据:当需要读取由多个线程共享的原子变量的值时,使用 load 可以确保读取操作的原子性和内存顺序,防止读取过程中其他线程的写入干扰。
  2. 内存顺序要求:当对内存顺序有特定要求,例如需要保证某个操作的内存效果对其他线程可见时,可以使用 load 并指定适当的内存顺序参数,如 std::memory_order_acquire

使用 store 的场景:

  1. 写入共享数据:当需要修改由多个线程共享的原子变量的值时,使用 store 可以确保写入操作的原子性和内存顺序,防止写入过程中其他线程的读取干扰。
  2. 发布操作:在发布-订阅模式中,当一个线程创建了一个对象,并希望其他线程能够安全地访问这个对象时,可以使用 store 并指定 std::memory_order_release 来确保对象的构造和发布操作对其他线程可见。

六、包装器

 在C++中什么可以被调用?函数对象(仿函数)、函数指针、函数名、lambda、这些都是可以被调用的,那这么多类型,我们用模板传参时,可能会导致效率低下。

所以C++11推出了包装器,也叫做适配器C++中的function本质是一个类模板,也是一个包装器。

底层用的还是仿函数。

int f(int a, int b)
{cout << "int f(int a, int b)" << endl;return a + b;
}struct Functor
{
public:int operator() (int a, int b){cout << "int operator() (int a, int b)" << endl;return a + b;}
};

就比如上面我要用map进行封装,可是他们的类型不同,模板参数如何传参?

map<string, >??

这时候我们需要用包装器

 我们来一个简单的用法

int main()
{//int(*pf1)(int,int) = f;//map<string, >function<int(int, int)> f1 = f;function<int(int, int)> f2 = Functor();function<int(int, int)> f3 = [](int a, int b) {cout << "[](int a, int b) {return a + b;}" << endl;return a + b;};cout << f1(1, 2) << endl;cout << f2(10, 20) << endl;cout << f3(100, 200) << endl;return 0;
}

上面代码f1 f2 f3就用functional这个类进行包装了,类型一样,那map就可以进行模板参数传参了。下面在网络用的比较多,指令集。什么指令执行什么任务。

int main()
{//int(*pf1)(int,int) = f;//map<string, >map<string, function<int(int, int)>> opFuncMap;opFuncMap["函数指针"] = f;opFuncMap["仿函数"] = Functor();opFuncMap["lambda"] = [](int a, int b) {cout << "[](int a, int b) {return a + b;}" << endl;return a + b;};cout << opFuncMap["函数指针"](1, 2) << endl;cout<< opFuncMap["仿函数"](1, 2) << endl;cout << opFuncMap["lambda"](1, 2) << endl;return 0;
}

 

 包装器对于类成员函数也有不同比如普通成员函数和静态成员函数的包装就不一样

 


class Plus
{
public:Plus(int rate = 2):_rate(rate){}static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return (a + b)* _rate;}private:int _rate = 2;
};
int main()
{//function<int(int, int)> f1 = &Plus::plusi;function<int(int, int)> f1 = Plus::plusi; //静态成员函数正常包装function<double(Plus, double, double)> f2 = &Plus::plusd; //普通成员函数//模板参数要有this*指针 但是实际上不用,直接传对象,而赋值时要加&不加编译不过cout << f1(1, 2) << endl;cout << f2(Plus(), 20, 20) << endl;Plus pl(3);cout << f2(pl, 20, 20) << endl;return 0;
}

 

 bind

 bind有两个作用:
一是交换参数

二是修个参数个数

基本用法:

std::bind 的基本语法如下:

std::bind<ReturnType>(Function, Args...)(std::placeholders::_1, ...);

其中:

  • ReturnType 是被绑定函数的返回类型。
  • Function 是要绑定的可调用对象。
  • Args... 是要绑定的参数列表。
  • std::placeholders::_1, ... 是用于占位的参数,表示将来调用时需要提供的参数。

 

#include <iostream>
#include <functional>void print(int a, int b) {std::cout << a << " and " << b << std::endl;
}int main() {print(10, 5);// 创建一个 std::function 对象,绑定 print_sum 函数// 但是交换了参数的顺序auto swapped_function = std::bind(print, std::placeholders::_1, std::placeholders::_2);// 调用交换参数后的函数swapped_function(5, 10); // 输出 "The sum of 10 and 5 is 15"return 0;
}

 关于参数交换这个用处不大。

修改参数个数场景还是很多的。比如一个类的成员函数是加减乘除,实际这个类的成员函数的参数是多少?

成员函数参数的个数是n+1个,类还有隐藏的this*啊。

如果用包装器进行包装,比如现在把类和lambda还有仿函数一起包装起来。问题是lambda只有n个参数,这时就需要用bind来修改参数个数。

#include <iostream>
#include <functional>
#include <map>
#include <string>// 一个示例类
class Calculator {
public:// 类成员函数,接受两个参数int add(int a, int b) {return a + b;}
};// 一个自由函数,接受两个参数
int multiply(int a, int b) {return a * b;
}int main() {//用bind直接绑死对象。std::function<int(int, int)> add1 = std::bind(&Calculator::add, Calculator(), std::placeholders::_1, std::placeholders::_2);std::map<std::string, std::function<int(int, int)>> funcMap ={{"*",multiply},{"+",add1 }};//std::cout << funcMap["*"](1, 2) << std::endl;//std::cout << funcMap["+"](10, 30) << std::endl;for (auto& e : funcMap){std::cout << "[" << e.first <<"]: " << e.second(10, 20) << std::endl;}return 0;
}

 

 

 

C++11 的常用内容到这里就讲解完毕了下节预告异常智能指针关注我带你学习更多C++知识。

相关文章:

C++ 11 【线程库】【包装器】

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;C修炼之路⏪   &#x1f69a;代码仓库:C高阶&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多C知识   &#x1f51d;&#x1f51d; 目录 前言 一、thread类的简单介绍 get_id…...

可视化数据科学平台在信贷领域应用系列四:决策树策略挖掘

信贷行业的风控策略挖掘是一个综合过程&#xff0c;需要综合考虑风控规则分析结果、效果评估、线上实时监测和业务管理需求等多个方面&#xff0c;以发现和制定有效的信贷风险管理策略。这些策略可能涉及贷款审批标准的调整、贷款利率的制定、贷款额度的设定等&#xff0c;在贷…...

数据查询深分页优化方案

大家好&#xff0c;我是冰河~~ 最近不少小伙伴在实际工作过程中&#xff0c;遇到了单表大数据量分页的问题&#xff0c;问我怎么优化分页查询。其实&#xff0c;这就是典型的深分页问题。今天趁着周末&#xff0c;给大家整理一些在深分页场景的简单处理方案。 一、普通分页查…...

Redis的主从复制

Redis主从复制是 Redis 内置的⼀种数据冗余和备份⽅式&#xff0c;同时也是分发读查询负载的⼀种⽅法。通过主从复制&#xff0c;可以有多个从服务器&#xff08;Slave &#xff09;复制⼀个主服务器&#xff08;Master &#xff09;的数据。在这个系统中&#xff0c;数据的复制…...

网络安全实战基础——实战工具与攻防环境介绍

一、实战集成工具 1. 虚拟机 VMware Workstation&#xff1a;大家熟知的虚拟机 Virtual Box&#xff1a;开源免费、轻量级 2. Kali Linux 工具集 信息收集 Nmap&#xff1a;免费开放的网络扫描和嗅探包&#xff0c;可探测主机是否在线&#xff0c;扫描主机端口和嗅探网络…...

vue2组件封装实战系列之tag组件

作为本系列的第一篇文章&#xff0c;不会过于的繁杂&#xff0c;并且前期的组件都会是比较简单的基础组件&#xff01;但是不要忽视这些基础组件&#xff0c;因为纵观elementui、elementplus还是其他的流行组件库&#xff0c;组件库的封装都是套娃式的&#xff0c;很多复杂组件…...

VBA实战(Excel)(4):实用功能整理

1.后台打开Excel 用于查数据&#xff0c;工作中要打开多个表获取数据再关闭的场景&#xff0c;利用此函数可以将excel表格作为后台数据库查询&#xff0c;快速实现客户要求&#xff0c;缺点是运行效率不够高。 Sub openexcel(exl_name As String)If Dir(addr, 16) Empty Then…...

nginx mirror流量镜像详细介绍以及实战示例

nginx mirror流量镜像详细介绍以及实战示例 1.nginx mirror作用2.nginx安装3.修改配置3.1.nginx.conf3.2.conf.d目录下添加default.conf配置文件3.3.nginx配置注意事项3.3.nginx重启 4.测试 1.nginx mirror作用 为了便于排查问题&#xff0c;可能希望线上的请求能够同步到测试…...

Android14 WMS-窗口添加流程(二)-Server端

Android14 WMS-窗口添加流程(一)-Client端-CSDN博客 本文接着上文"Android14 WMS-窗口添加流程(一)-Client端"往下讲。也就是WindowManagerService#addWindow流程。 目录 一. WindowManagerService#addWindow 标志1&#xff1a;mPolicy.checkAddPermission 标志…...

【传知代码】DETR[端到端目标检测](论文复现)

前言&#xff1a;想象一下&#xff0c;当自动驾驶汽车行驶在繁忙的街道上&#xff0c;DETR能够实时识别出道路上的行人、车辆、交通标志等目标&#xff0c;并准确预测出它们的位置和轨迹。这对于提高自动驾驶的安全性、减少交通事故具有重要意义。同样&#xff0c;在安防监控、…...

Edge浏览器十大常见问题,一次性解决!

Edge曾被称为最好用的浏览器&#xff0c;拳打Chrome脚踢firefox, 可如今却隐藏着像是播放卡顿、下载缓慢、广告繁多等诸多问题&#xff0c;不知道各位还在用吗&#xff1f; 今天小编收集整理了Edge浏览器十大烦人问题&#xff0c;并提供简单有效的解决办法&#xff0c;让你的E…...

lubuntu / ubuntu 配置静态ip

一、查看原始网络配置信息 1、获取网卡名称 ifconfig 2、查询网关IP route -n 二、编辑配置文件 去/etc/netplan目录找到配置文件&#xff0c;配置文件名一般为01-network-manager-all.yaml sudo vim /etc/netplan/01-network-manager-all.yaml文件打开后内容如下 # This …...

15、matlab绘图汇总(图例、标题、坐标轴、线条格式、颜色和散点格式设置)

1、plot()函数默认格式画图 代码&#xff1a; x0:0.1:20;%绘图默认格式 ysin(x); plot(x,y) 2、X轴和Y轴显示范围/axis()函数 代码&#xff1a; x0:0.1:20;%绘图默认格式 ysin(x); plot(x,y) axis([0 21 -1.1 1.1])%设置范围 3、网格显示/grid on函数 代码&#xff1a; …...

调试环境搭建(Redis 6.X 版本)

今儿&#xff0c;我们来搭建一个 Redis 调试环境&#xff0c;目标是&#xff1a; 启动 Redis Server &#xff0c;成功断点调试 Server 的启动过程。使用 redis-cli 启动一个 Client 连接上 Server&#xff0c;并使用 get key 指令&#xff0c;发起一次 key 的读取。 视频可见…...

postgres数据库报错无法写入文件 “base/pgsql_tmp/pgsql_tmp215574.97“: 设备上没有空间

解决思路&#xff1a; base/pgsql_tmp下临时表空间不够 需要新建一个临时表空间指定到根目录之外的其他目录 并且修改默认临时表空间参数 解决方法&#xff1a; select * from pg_settings where name temp_tablespaces;mkdir /home/postgres/tbs_tmp CREATE TABLESPACE tbs_t…...

力扣2762. 不间断子数组

力扣2762. 不间断子数组 multiset法 multiset&#xff1a;元素从小到大排序 begin()返回头指针 (最小)rbegin()返回尾指针 (最大) class Solution {public:long long continuousSubarrays(vector<int>& nums) {int n nums.size();long long res 0;multiset<…...

OpenCV学习(4.8) 图像金字塔

1.目的 在这一章当中&#xff0c; 我们将了解图像金字塔。我们将使用图像金字塔创建一个新的水果&#xff0c;“Orapple”我们将看到这些功能&#xff1a; cv.pyrUp&#xff08;&#xff09; &#xff0c; cv.pyrDown&#xff08;&#xff09; 在通常情况下我们使用大小恒定…...

【TB作品】msp430f5529单片机,dht22,温湿度传感器,OLED显示屏

使用DHT22温湿度传感器和OLED显示屏的单片机项目 博客名称 利用MSP430单片机读取DHT22并显示温湿度 作品功能 本项目利用MSP430单片机读取DHT22温湿度传感器的数据&#xff0c;并将温湿度信息显示在OLED显示屏上。通过这个项目&#xff0c;您可以学习如何使用单片机与传感器…...

Kotlin 异常处理

文章目录 什么是异常抛出异常通过异常信息解决异常捕获异常 什么是异常 我们在运行程序时&#xff0c;如果代码出现了语法问题或逻辑问题&#xff0c;会导致程序编译失败或退出&#xff0c;称为异常。运行结果会给出一个一长串的红色字&#xff0c;通常会给出异常信息&#xf…...

nltk下载报错

捣鼓voice_clone时报错&#xff1a; 报错信息&#xff1a; mport nltk nltk.download(‘cmudict’)For more information see: https://www.nltk.org/data.htmlAttempted to load tokenizers/punkt/PY3/english.pickleSearched in: - ‘/home/zhangshuai/nltk_data’ - ‘/hom…...

Vulnhub-DC5

靶机IP:192.168.20.139 kaliIP:192.168.20.128 网络有问题的可以看下搭建Vulnhub靶机网络问题(获取不到IP) 信息收集 nmap扫下端口及版本 dirsearch扫下目录 LinuxphpNginx 环境 我们再去看前端界面&#xff0c;发现在contact界面有能提交的地方&#xff0c;但是经过测试不…...

pytorch 笔记:pytorch 优化内容(更新中)

1 Tensor创建类 1.1 直接创建Tensor&#xff0c;而不是从Python或Numpy中转换 不要使用原生Python或NumPy创建数据&#xff0c;然后将其转换为torch.Tensor直接用torch.Tensor创建或者直接&#xff1a;torch.empty(), torch.zeros(), torch.full(), torch.ones(), torch.…...

vue 创建一个新项目 以及 手动配置选项

【Vue】3.0 项目创建 自定义配置_vue3.0-CSDN博客...

c#快速获取超大文件夹文件名

c#快速获取超大文件夹文件名 枚举集合速度快&#xff1a;(10万个文件) //by txwtech IEnumerable<string> files2 Directory.EnumerateFiles("d:\aa", "*.xml", SearchOption.TopDirectoryOnly);//过滤指定查询xml文件 慢&#xff1a; var fi…...

华为OD技术面试-最小异或-2024手撕代码真题

题目:最小异或 给你两个正整数 num1 和 num2 ,找出满足下述条件的正整数 x : x 的置位数和 num2 相同,且 x XOR num1 的值 最小 注意 XOR 是按位异或运算。 返回整数 x 。题目保证,对于生成的测试用例, x 是 唯一确定 的。 整数的 置位数 是其二进制表示中 1 的数目。 示…...

基于SpringBoot+Vue单位考勤系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝1W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;还…...

Anaconda软件:安装、管理python相关包

Anaconda的作用 一个python环境中需要有一个解释器, 和一个包集合. 解释器&#xff1a; 根据python的版本大概分为2和3. python2和3之间无法互相兼容, 也就是说用python2语法写出来的脚本不一定能在python3的解释器中运行. 包集合&#xff1a;包含了自带的包和第三方包, 第三…...

pinia 重置状态插件

一、前言 测试提出&#xff0c;登出登录后&#xff0c;再次进入页面后。页面的查询项非初始状态。检查后发现&#xff0c;是因为查询项的值存到了store呢&#xff0c;从store中获取&#xff0c;故需要一个重置store的方法 二、pinia 查阅pinia官网后&#xff0c;发现pinia提…...

一千题,No.0049(跟奥巴马一起编程)

美国总统奥巴马不仅呼吁所有人都学习编程&#xff0c;甚至以身作则编写代码&#xff0c;成为美国历史上首位编写计算机代码的总统。2014 年底&#xff0c;为庆祝“计算机科学教育周”正式启动&#xff0c;奥巴马编写了很简单的计算机代码&#xff1a;在屏幕上画一个正方形。现在…...

《python程序语言设计》2018版第5章第46题均值和标准方差-上部(我又一次被作者的出题击倒)

第N次被作者打倒了&#xff0c;第5章46题解题上集的记录 计算均值的代码段 step_num 0num_c 0 pow_c 0 while step_num < 10:a eval(input("Enter number is: "))num_c apow_c pow(a, 2)step_num 1 t2 num_c / 10这个结果和书里的答案差一点。书里写的是…...

php网站后台页面/新闻头条最新消息国家大事

又大一岁了,先祝自己生日快乐,今年是我快乐的一年,最期待的事就是下半年小宝贝的出生,也希望自己今年能在&#xff23;&#xff03;上有更高的成就,虽然我已经不做软件开发了&#xff0c; 但是编程做为一种爱好又何尝不可呢...

如何查看网站用什么代码做的/深圳市企业网站seo

今天早上还是和往常一样&#xff0c;一杯豆奶&#xff0c;可不知怎么地&#xff0c;感觉今天的豆奶有点儿甜呀&#xff01;难道这就是传说中的甘尽苦来&#xff1f;我这好几斤的小心脏。还是说一下最近的想法吧。虽然有点儿过分&#xff0c;但还是值得记录一下&#xff0c;毕竟…...

做一个产品网站要多少钱/seo搜索排名优化公司

文章目录一、今日成绩二、错题总结第一题三、知识查缺题目及解析来源&#xff1a;2023年04月12日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 依据题目画出PERT图如下&#xff1a; 关键路径长度&#xff08;从起点到终点的路径中最长的一条&#x…...

网站上微信引流怎么做的/北京百度搜索排名优化

前段时间研究webrtc&#xff0c;找了很多资料&#xff0c;都不好用。后来找到个skypertc&#xff0c;在上面做了修改&#xff0c;实现了功能。 不知道有多少人对这个感兴趣&#xff0c;并且正在找资料。 有兴趣的话请留言&#xff0c;需要人多的话我再花时间整资料出来。转载于…...

wordpress媒体默认链接/灰色行业推广平台网站

使用 <script setup>组合式 API 的语法糖的时候&#xff0c;defineProps报错&#xff1a; 代码如下&#xff1a; 第一次写vue3的项目&#xff0c;真的是到处都是坑啊&#xff0c;我就不断的百度百度再百度&#xff0c;发现再 module.exports {root: true,env: {node: …...

wordpress 固定链接结构出错/网络优化器下载

距离矢量&#xff08;Distance Vector&#xff09;说法是因为路由是以矢量&#xff08;距离、方向&#xff09;的方式通告出去的&#xff0c;其中距离是根据度量定义的&#xff0c;方向是根据下一跳路由器定义的。因此&#xff0c;满足距离矢量路由选择协议的条件要有两条&…...