✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2022 版本 17.6.5
文章目录
- 🌇前言
- 🏙️正文
- 1.lambda表达式
- 1.1.仿函数的使用
- 1.2.lambda表达式的语法
- 1.3.lambda表达式的使用
- 1.4.lambda表达式的原理
- 1.4.lambda表达式的优点及适用场景
- 2.线程库
- 2.1.thread 线程类
- 2.1.1.this_thread 命名空间
- 2.2.mutex 互斥锁类
- 2.2.1.并行与串行的对比
- 2.2.2.其他锁类型
- 2.2.3.RAII 风格的锁
- 2.3.condition_variable 条件变量类
- 2.3.1.交替打印数字
- 2.4.atomic 原子操作类
- 3.包装器
- 3.1.function 包装器
- 3.2.bind 绑定
- 🌆总结
🌇前言
自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码
🏙️正文
1.lambda表达式
lambda
表达式 源于数学中的 λ
演算,λ
演算是一种 基于函数的形式化系统,它由数学家 阿隆佐邱奇 提出,用于研究抽象计算和函数定义。对于编程领域来说,可以使用 lambda
表达式 快速构建函数对象,作为函数中的参数
1.1.仿函数的使用
仿函数 是 C++
中的概念,指借助 类+operator()
重载 创建的函数对象,仿函数 的使用场景如下
创建一个 vector
,通过 sort
函数进行排序,至于结果为升序还是降序,可以通过 仿函数 控制
#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct cmpLess
{bool operator()(int n1, int n2){return n1 < n2;}
};struct cmpGreater
{bool operator()(int n1, int n2){return n1 > n2;}
};int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), cmpLess()); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), cmpGreater()); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}
注:sort
如果不传递函数对象,默认排序结果为升序
结果为正确排序,但这种先创建一个仿函数对象,再调用的传统写法有点麻烦了,如果是直接使用 lambda
表达式 创建函数对象,整体逻辑会清楚很多
使用 lambda
表达式 修改后的代码如下,最大的改变就是 可以直接在传参时直接编写函数对象的代码逻辑
#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 < n2; }); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 > n2; }); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}
最终结果也是正常的
有了 lambda
表达式 之后,程序员不必再通过 仿函数 构建函数对象,并且可以在一定程度上提高代码的可阅读性,比如一眼就可以看出回调函数是在干什么
接下来看看如何理解 lambda
表达式 语法
1.2.lambda表达式的语法
lambda
表达式 分为以下几部分:
[ ]
捕捉列表( )
参数列表mutable
关键字->returntype
返回值类型{ }
函数体
[ ]( ) mutable ->returntype { }
其中,( )
参数列表、mutable
、->returntype
都可以省略
- 省略
( )
参数列表 表示当前是一个无参函数对象 - 省略
mutable
关键字 表示保持捕捉列表中参数的常量属性 - 省略
->returntype
返回值类型 表示具体的返回值类型由函数体决定,编译器会自动推导出返回值类型
注意:
- 捕捉列表 和 函数体 不可省略
- 如果使用了
mutable
关键字 或者->returntype
返回值,就不能省略( )
参数列表,即使为空 - 虽然返回值类型编译器可以推导,但最好还是注明返回值类型
也就是说,最基本的 lambda
表达式 只需书写 [ ]{ }
即可表示,比如这样
int main()
{// 最简单的 lambda表达式[]{};return 0;
}
此时的 lambda
表达式 相当于一个 参数为空、返回值为空、函数体为空 的匿名函数对象
void func()
{}
主要区别在于 lambda
表达式 构建出来的是一个 匿名函数对象,而 func
是一个 有名函数对象,可以直接调用
1.3.lambda表达式的使用
lambda
表达式 构建出的是一个 匿名函数对象,匿名函数对象也可以调用,不过需要在创建后立即调用,否则就会因为越出作用域而被销毁(匿名对象生命周期只有一行)
下面通过 lambda
表达式 构建一个简单的 两整数相加 函数对象并调用
int main()
{int ret = [](int x, int y)->int { return x + y; }(1, 2);cout << ret << endl;return 0;
}
直接使用 lambda
表达式 构建出的 匿名函数对象 比较抽象,一般都是将此 匿名函数对象 作为参数传递(比如 sort
),如果需要显式调用,最好是将创建出来的 匿名函数对象 赋给一个 有名函数对象,调用时逻辑会清晰很多
使用 auto
推导 匿名函数对象 的类型,然后创建 add
函数对象
int main()
{auto add = [](int x, int y)->int { return x + y; };int ret = add(1, 2);cout << ret << endl;return 0;
}
lambda
表达式 还有很多玩法,接下来逐一介绍,顺便学习其他组成部分
利用 lambda
表达式 构建一个交换两个元素的 函数对象
最经典的写法是 函数参数设为引用类型,传入两个元素,在函数体内完成交换
int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [](int& rx, int& ry)->void{auto tmp = rx;rx = ry;ry = tmp;};swap(x, y);cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}
这种经典写法毋庸置疑,肯定能完成两数交换的任务
除此之外,还可以借助 lambda
表达式 中的 捕捉列表 捕获外部变量进行交换
int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}
因为现在 函数对象 是直接捕获外部变量进行操作,调用函数对象时,无需传参
代码写完,编译器立马给出了报错:x
、y
不可修改
这是因为 捕捉列表 中的参数是一个值类型(传值捕捉),此时的捕获的是外部变量的内容,然后赋值到 “x、y
” 中,捕捉列表 中的参数默认具有 常量属性,不能直接修改,但可以添加 mutable
关键字 取消常性
int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]()mutable ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}
但是程序运行结果不尽人意,外部的 x、y
并没有被交换,证明此时 捕捉列表 中的参数 x、y
是独立的值(类似函数中的值传递)
想让外部的 x、y
被真正捕获,需要使用 引用捕捉
int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 引用捕捉auto swap = [&x, &y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}
现在 x、y
被成功交换了
注意: 捕捉列表中的 &x
表示引用捕捉外部的 x
变量,并非取地址(特例)
所以说
mutable
关键字不常用,因为它取消的是值类型的常性,即使修改了,对外部也没有什么意义,如果想修改,直接使用 引用捕捉 就好了
捕捉列表 支持 混合捕捉,同时使用 引用捕捉 + 传值捕捉
int main()
{int x = 1;int y = 2;cout << "调用前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 混合捕捉auto func = [&x, y]()mutable ->void{x = 100;y = 200;};func();cout << "调用后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}
x
被修改了,而 y
没有
除了 混合捕捉 外,捕捉列表 还支持 全部引用捕捉 和 全部传值捕捉
全部引用捕捉
int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [&]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}
无需指定 捕捉列表 中的参数,&
可以一键 引用捕捉 外部所有变量
注:只能捕捉已经定义或声明的变量
全部传值捕捉
int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [=]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}
全部传值捕捉 也能一键捕捉外部变量,不过此时捕获的是外部变量的值,并非变量本身,无法对其进行修改(可以通过 mutable
关键字 取消常性)
注意: [=]
表示全部传值捕捉,[]
表示不进行捕捉,两者不等价
捕捉列表 的使用非常灵活,比如 [&, x]
表示 x
使用 传值捕捉,其他变量使用 引用捕捉; [=, &str]
表示 str
使用 引用捕捉,其他变量使用 传值捕捉
捕捉列表 就像一个 “大师球”,可以直接捕捉到外部的变量,在需要大量使用外部变量的场景中很实用,有效避免了繁琐的参数传递与接收
有没有 全部引用捕捉 + 全部传值捕捉 ?
当然没有,这是相互矛盾的,一个变量不可能同时进行 引用传递 和 值传递,即便传递成功了,编译器在使用时也不知道使用哪一个,存在二义性,所以不被允许
注意: 关于 捕获列表 有以下几点注意事项
- 捕捉列表不允许变量重复传递,否则就会导致编译错误
- 在块作用域以外的
lambda
函数捕捉列表必须为空 - 在块作用域中的
lambda
函数不仅能捕捉父作用域中局部变量,也能捕捉到爷爷作用域中的局部变量
lambda
表达式 还可以完美用作 线程回调函数,比如接下来使用 C++11
中的 thread
线程类,创建一个线程,并使用 lambda
表达式 创建一个线程回调函数对象
int main()
{// 创建线程,并打印线程idthread t([] { cout << "thread running... " << this_thread::get_id() << endl; });t.join();return 0;
}
总之 lambda
表达式 在实际开发中非常好用,关于 thread
类的相关知识放到后面讲解,接下来先看看 lambda
表达式 的实现原理
1.4.lambda表达式的原理
lambda
表达式 生成的函数对象有多大呢?
是像 普通的函数对象指针 一样占 4/8
字节,还是像 仿函数 一样占 1
字节,通过 sizeof
计算大小就可以一探究竟
// 普通函数
int add(int x, int y)
{return x + y;
}// 仿函数
class addFunc
{
public:int operator()(int x, int y){return x + y;}
};int main()
{auto typeA = add;addFunc typeB;auto typeC = [](int x, int y)->int { return x + y; };cout << "普通函数: " << sizeof(typeA) << endl;cout << "仿函数: " << sizeof(typeB) << endl;cout << "lambda表达式: " << sizeof(typeC) << endl;return 0;
}
结果显示,lambda
表达式 生成的函数对象与 仿函数 生成的函数对象大小是一样的,都是 1
字节
仿函数 生成的函数对象大小为 1
字节是因为其生成了一个空类,实际调用时是通过 operator()
重载实现的,比如上面的 addFunc
类,空类因为没有成员变量,所以大小只为 1
字节
由此可以推断 lambda
表达式 本质上也是生成了一个空类,分别查看使用 仿函数 和 lambda
表达式 时的汇编代码
可以看到,这两段汇编代码的内容是一模一样的,都是先 call
一个函数(operator()
重载函数),然后再执行主体逻辑(两数相加),只不过使用 仿函数 需要自己编写一个 空类,而 使用 lambda
表达式 时由编译器生成一个 空类,为了避免这个自动生成的 空类 引发冲突,会将这个 空类 命名为 lambda_uuid
uuid
是 通用唯一标识码,可以生成一个重复率极低的辨识信息,避免类名冲突,这也意味着即便是两个功能完全一样的lambda
表达式,也无法进行赋值,因为lambda_uuid
肯定不一样
所以在编译器看来,lambda
表达式 本质上就是一个 仿函数
1.4.lambda表达式的优点及适用场景
lambda
表达式 作为一种轻量级的匿名函数表示方式,具备以下优点:
- 简洁性: 对于简单的函数操作,无需再手动创建函数、调用,只需要编写一个
lambda
表达式生成函数对象 - 方便些:
lambda
表达式具有 捕捉列表,可以轻松捕获外部的变量,避免繁琐的参数传递与接收 - 函数编程支持:
lambda
表达式可以作为函数的参数、返回值或存储在数据结构中 - 内联定义:
lambda
表达式Lambda表达式可以作为函数的参数、返回值或存储在数据结构中 - 简化代码: 对于一些简单的操作,使用
lambda
表达式可以减少代码的行数,提高代码的可读性
总的来说,lambda
表达式 可以替代一些代码量少的函数,使用起来十分方便,如果 lambda
表达式 编写出来的代码过于复杂时,可以考虑转为普通函数,确保代码的清晰性和可读性
2.线程库
关于 线程 相关操作,Linux
选择使用的是 POSIX
标准,而 Windows
没有选择 POSIX
标准,反而是自己搞了一套 API
和系统调用,称为 Win32 API
,意味着 Linux
与 Windows
存在标准差异,直接导致能在 Linux
中运行的程序未必能在 Windows
中运行
在 C++11
之前,编写多线程相关代码如果保证兼容性,就需要借助 条件编译,分别实现两份代码,根据不同平台编译不同的代码(非常麻烦)
// 确保平台兼容性
#ifdef __WIN_32__CreateThread // Windows 中创建线程的接口// ...
#elsepthread_create // Linux 中创建线程的接口// ...
#endif
在 C++11
中,加入了 线程库 这个标准,其中包含了 线程、互斥锁、条件变量 等常用线程操作,并且无需依赖第三方库,也就意味着使用 线程库 编写的代码既能在 Linux
中运行,也能在 Windows
中运行,保障了代码的可移植性,除此之外,线程库 还新加入了 原子相关操作
2.1.thread 线程类
thread
线程类的概况如下
首先看看 thread
类中的 线程 id
Linux
中的 线程 id
表示每个轻量级进程 TCB
的起始地址,用一个 unsigned long int
表示,理解起来比较费劲;在 thread
类中,直接创建了一个 id
类,也就是这里的 thread::id
,这个类用于标识 线程,同时在类中重载了一系列 operator
函数,用于两个 thread::id
对象的比较
线程创建后,系统会为其分配一个类型为 thread::id
的标识符,也就是该线程的唯一标识符
获取当前线程的 id
,并进行比较
int main()
{thread::id id1 = std::this_thread::get_id();thread::id id2 = std::this_thread::get_id();cout << "id1: " << id1 << " " << "id2: " << id2 << endl;if (id1 == id2)cout << "id 相同" << endl;elsecout << "id 不同" << endl;return 0;
}
注意: thread::id
是一个类,不支持初始化或赋值,用于获取线程 id
至于 thread::native_handle_type
代表一个底层线程的本地(native
)句柄或标识符,本地句柄通常是由操作系统提供的,用于标识和管理线程的底层资源
在绝大多数情况下,使用
C++
标准库提供的高级线程抽象是足够的,而无需直接访问线程的本地句柄。直接使用底层线程句柄通常是为了执行与平台相关的线程操作,这可能包括与操作系统相关的调度、优先级、特定的线程控制等。这样的操作通常是为了满足对底层线程管理的特殊需求,而不是一般性的C++
线程编程。
总结就是 thread::native_handle_type
一般用不上,现阶段不必关心
接下来看看 构造函数 部分
创建 线程类 对象,支持:
- 创建一个参数为空的默认线程对象
- 通过可变参数模板传入回调函数和参数,其中
Fn
表示回调函数对象,Args
是传给回调函数的参数包(可以为空) - 移动构造,根据线程对象(右值)来构造线程对象
注意: thread
类不支持 拷贝构造,因为线程对象拥有自己的独立栈等线程资源,所以这里的 拷贝构造 使用 delete
关键字删除了
使用 thread
类需要包含 thread
这个头文件
#include <iostream>
#include <thread>using namespace std;int main()
{// 参数为空的默认线程对象thread t1; // 传入回调函数及参数thread t2([](int x, int y)->void { while(true)cout << "x + y = " << x + y << endl; }, 1, 2);// 只传入回调函数thread t3([]()->void {while(true)cout << "thread running..." << endl; });//t1.join(); // t1 线程状态为空,不能 join 等待t2.join();t3.join();// 无法拷贝构造//thread t4(t3);return 0;
}
线程回调函数不止可以使用 lambda
表达式,还可以传入 函数指针 或者 函数对象
通过调试可以看到 t2
、t3
线程正在运行中,而 t1
因为没有指定回调函数,所以也就没有完全创建,自然也就没有在运行
其中 17392
、3092
、5964
分别为 主线程、次线程 t2
和 次线程 t3
,而 8460
和 26080
是 ntdll.dll
类型的线程,用于为应用程序加载其他动态库,程序运行大概半分钟后,这两个线程就会自动消失,因为当前处于调试状态,并且程序运行时间较短,所以才会看到这个两个系统级线程
注意: 线程如果没有完全创建,是不能 join
等待的,并且线程不支持拷贝操作
同样的,thread
只支持 移动赋值,不支持 传值赋值
部分构造函数后跟的
noexcept
关键字表示当前函数不会抛出 异常,详细知识放到 『异常』 文章中讲解
当线程对象生命周期结束时,会调用 析构函数 销毁对象
thread
类还提供了一批线程相关接口,比如 获取 id
、等待、分离、交换
除了 joinable
和 swap
,其他功能在 pthread
库中都已经使用过了
get_id
对应pthread_self
join
对应pthread_join
detach
对应pthread_detach
简单使用如下
int main()
{// 创建线程thread t([]()->void { cout << "thread running..." << endl; });// 获取线程 idthread::id id = t.get_id();// 线程剥离// t.detach();cout << "线程 " << id << " 已经创建了" << endl;// 等待线程退出t.join();return 0;
}
注意: 分离线程后,主线程运行结束,整个程序也会随着终止,会导致正在运行中的次线程终止
joinable
是非阻塞版的线程等待函数,等待成功返回 true
,否则返回 false
swap
则是将两个线程的资源进行交换(线程回调函数、线程状态等)
注意: swap
并不会交换 thread::id
,因为这是线程唯一标识符
至于最后两个函数不常用,这里就不介绍了
这些都是线程常见操作,有了 Linux
多线程编程的基础,学习起来会轻松很多,接下来编写一个成员:创建一批线程,并分别打印十次自己的 id
int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 如何获取 id ?cout << "我是线程 " << " 我正在运行..." << endl;}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}
此时面临一个尴尬的问题:如何在回调函数中获取线程 id
?
- 线程
id
目前之前通过线程对象调用get_id
函数获取 - 传入线程吗?不行,因为此时线程还没有完全创建,线程
id
为0
- 传入线程对象?不行,线程还没有完全创建,传入的对象也无法使用,也能通过捕获列表进行引用捕捉,不过同样无法使用
如此一来,想要在 线程回调函数 内获取 线程 id
还不是一件容易的事,好在 C++11
中还提供了一个 this_thread
命名空间,其中提供了获取 线程 id
等函数,可以自由调用
2.1.1.this_thread 命名空间
this_thread
是一个命名空间,其中包含了 获取线程 id
、线程休眠、线程时间片 相关函数
有了 this_thread
命名空间之后,就可以轻松获取 线程 id
int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 获取 idauto id = this_thread::get_id();cout << "我是线程 " << id << " 我正在运行..." << endl;}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}
可以看到,正常获取到了每个线程的 线程 id
注:这里打印错乱很正常,因为显示器也是临界资源,多线程并发访问时,也是需要加锁保护的
this_thread
只是一个命名空间,是如何做到正确调用get_id
函数并获取线程id
的?
this_thread
是std
中的一个子命名空间,其中包含了一些与线程有关的操作,比如get_id
,当线程调用this_thread::get_id
时,实际调用的就是该线程的thread::get_id
,所以才能做到谁调用,就获取谁的线程id
除此之外,this_thread
命名空间中还提供了 线程休眠 的接口:sleep_until
、sleep_for
sleep_util
表示休眠一个 绝对时间,比如线程运行后,休眠至明天 6::00
才接着运行;sleep_for
则是让线程休眠一个 相对时间,比如休眠 3
秒后继续运行,休眠 绝对时间 用的比较少,这里来看看如何休眠 相对时间
相对时间 有很多种:时、分、秒、毫秒、微秒…,这些单位包含于 chrono
类中
比如分别让上面程序中的线程每隔 200
毫秒休眠一次,修改代码如下
int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 获取 idauto id = this_thread::get_id();cout << "我是线程 " << id << " 我正在运行..." << endl;// 休眠 200 毫秒this_thread::sleep_for(chrono::milliseconds(200));}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}
也可以让线程休眠其他单位时间
最后在 this_thread
命名空间中还存在一个特殊的函数:yield
这里的 yield
表示 让步、放弃,带入多线程环境中就表示 主动让出当前的时间片
yield
主要用于 无锁编程(尽量减少使用锁),而无锁编程的实现基于 原子操作 CAS
,关于原子的详细知识放到后面讲解
原子操作 CAS
是一个不断重复尝试的过程,如果尝试的时间过久,就会影响整体效率,因为此时是在做无用功,而 yield
可以主动让出当前线程的时间片,避免大量重复,把 CPU
资源让出去,从而提高整体效率
2.2.mutex 互斥锁类
多线程编程需要确保 线程安全 问题
首先要明白 线程拥有自己独立的栈结构,但对于全局变量等 临界资源,是直接被多个线程共享的
如果想给线程回调函数传递 左值引用 类型的参数,需要使用
ref
引用包装器函数进行包装传递
比如通过以下代码证明 线程独立栈 的存在
int g_val = 0;void Func(int n)
{cout << "&g_val: " << &g_val << " &n: " << &n << endl << endl;
}int main()
{int n = 10;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();return 0;
}
可以看到,全局变量 g_val
的地址是一样,而局部变量 n
的地址相差很远,证明这两个局部变量不处于同一个栈区中,而是分别存在线程的 独立栈
如果多个线程同时对同一个 临界资源 进行操作
- 操作次数较少时,近似原子
- 操作次数多时,有线程安全问题
这里同时对 g_val
进行 n
次 ++
操作
当 n = 100
时,结果还算正常(正确结果为 200
)
int g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 100;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}
但如果将 n
改为 20000
,程序就出问题了(正确结果为 40000
)
n = 20000;
并且几乎每一次运行结果都不一样,这就是由于 线程安全 问题带来的 不确定性 导致的
关于线程安全的更多知识详见 Linux多线程【线程互斥与同步】
确保 线程安全 的手段之一就是 加锁 保护,C++11
中就有一个 mutex
类,其中包含了 互斥锁 的各种常用操作
比如创建一个 mutex
互斥锁 对象,当然 互斥锁也是不支持拷贝的,mutex
互斥锁 类也没有提供移动语义相关的构造函数,因为锁资源一般是不允许被剥夺的
互斥锁 对象的构造很简单,使用也很简单,常用的操作有:加锁、尝试加锁、解锁
lock
对应pthread_mutex_lock
try_lock
对应pthread_mutex_trylock
unlock
对应pthread_mutex_unlock
这些操作使用起来十分简单,对上面的程序进行加锁保护
注:使用 mutex
类需要包含 mutex
这个头文件
int g_val = 0;// 互斥锁对象
mutex mtx;void Func(int n)
{while (n--){mtx.lock();g_val++;mtx.unlock();}
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}
此时无论数据量有多大,最终的结果都是符合预期的
注意: 这里的两个线程只需要一把锁,并且要保证两个线程看到的是同一把锁
2.2.1.并行与串行的对比
互斥锁 的加锁、解锁位置也是有讲究的,比如只把 g_val++
这个操作加锁,此时程序就是 并行化 运行,线程 A
与 线程 B
都可以进入循环,但两者需要在循环中竞争 锁资源,只有抢到 锁资源 的线程才能进行 g_val++
,两个线程同时竞争,相当于同时进行操作
也可以把整个 while
循环加锁,程序就会变成 串行化,线程 A
或者 线程 B
抢到 锁资源 后,就会不断进行 g_val++
,直到循环结束,才会把 锁资源 让出
理论上来说,并行化 要比 串行化 快,实际结果可以通过代码呈现
int main()
{int n = 20000;size_t begin = clock();thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();size_t end = clock();cout << "g_val: " << g_val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}
首先来看看在 n = 20000
的情况下,并行化 耗时
注:测试性能需要在 release
模式下进行
耗时 4ms
,似乎还挺快,接下来看看 串行化 耗时
串行化 只花了 2ms
,比 并行化 还要快
为什么?
因为现在的程序比较简单,while
循环内只需要进行 g_val++
就行了,并行化中频繁加锁、解锁的开销要远大于串行化单纯的进行 while
循环
如果循环中的操作变得复杂,那么 并行化 是要比 串行化 快的,所以加锁时选择 并行化 还是 串行化,需要结合具体的场景进行判断
这里为了让两个线程看到的是同一把锁,将 mutex
对象定义成了一个 全局对象,其实也可以定义为 局部对象,配合 lambda
表达式 的捕捉列表捕获 mutex
对象
int main()
{int n = 20000;int val = 0;mutex mtx; // 局部锁对象size_t begin = clock();thread t1([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});thread t2([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});t1.join();t2.join();size_t end = clock();cout << "val: " << val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}
注意: n
是传值捕捉,如果相对其进行修改,需要使用 mutable
关键字取消常性
2.2.2.其他锁类型
除了最常用的 mutex
互斥锁,C++11
中还提供了其他几种版本
recursive_mutex
递归互斥锁,这把锁主要用来 递归加锁 的场景中,可以看作 mutex
互斥锁 的递归升级版,专门用在递归加锁的场景中
比如在下面的代码中,使用普通的 mutex
互斥锁 会导致 死锁问题,最终程序异常终止
// 普通互斥锁
mutex mtx;void func(int n)
{if (n == 0)return;mtx.lock();n--;func(n);mtx.unlock();
}int main()
{int n = 1000;thread t1(func, n);thread t2(func, n);t1.join();t2.join();return 0;
}
为什么会出现 死锁?
因为当前在进入递归函数前,申请了锁资源,进入递归函数后(还没有释放锁资源),再次申请锁资源,此时就会出现 锁在我手里,但我还申请不到 的现象,也就是 死锁
解决这个 死锁 问题的关键在于 自己在持有锁资源的情况下,不必再申请,此时就要用到 recursive_mutex
递归互斥锁了
// 递归互斥锁
recursive_mutex mtx;
使用 recursive_mutex
递归互斥锁 后,程序正常运行
timed_mutex
时间互斥锁,这把锁中新增了 定时解锁 的功能,可以在程序运行指定时间后,自动解锁(如果还没有解锁的话)
其中的 try_lock_for
是按照 相对时间 进行自动解锁,而 try_lock_until
则是按照 绝对时间 进行自动解锁
比如在下面的程序中,使用 timed_mutex
时间互斥锁,设置为 3
秒后自动解锁,线程获取锁资源后,睡眠 5
秒,即便睡眠时间还没有到,其他线程也可以在 3
秒后获取锁资源,同样进入睡眠
// 时间互斥锁
timed_mutex mtx;void func()
{// 3秒后自动解锁mtx.try_lock_for(chrono::seconds(3));// 睡眠5秒for (int i = 1; i <= 5; i++){this_thread::sleep_for(chrono::seconds(1));cout << "线程 " << this_thread::get_id() << " 已经睡眠了 " << i << " 秒" << endl;}mtx.unlock();
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}
至于最后一个 recursive_timed_mutex
递归时间互斥锁,就是对 timed_mutex
时间互斥锁 做了 递归 方面的升级,使其在面对 递归 场景时,不会出现 死锁
2.2.3.RAII 风格的锁
手动加锁、解锁可能会面临 死锁 问题,比如在引入 异常处理 后,如果在 临界区 内出现了异常,程序会直接跳转至 catch
中捕获异常,这就导致 锁资源 没有被释放,其他线程申请锁资源时,就会出现 死锁 问题
// 死锁
mutex mtx;void func()
{for (int i = 0; i < 2; i++){try{mtx.lock();if (i % 2 == 0)throw exception("抛出异常");mtx.unlock();}catch (const std::exception& msg){cout << msg.what() << endl;}}
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}
这里引发 死锁问题 的关键在于 线程在出现异常后,直接跳转至 catch
代码块中,并且没有释放锁资源
解决方法有两个:
- 在
catch
代码块中手动释放锁资源(不推荐) - 使用
RAII
风格的锁(推荐)
RAII
风格就是 资源获取就是初始化 ,也就是利用对象出了作用域会自动调用析构函数这个特性,来 自动释放锁资源
编写一个 LockGuard
类
// RAII 风格
template<class locktype>
class LockGuard
{
public:LockGuard(locktype& mtx):_mtx(mtx){// 加锁_mtx.lock();}~LockGuard(){// 解锁_mtx.unlock();}private:locktype& _mtx;
};
注意:
- 需要使用模板,因为互斥锁有多个版本
- 成员变量
_mtx
需要使用引用类型,因为所有的锁都不支持拷贝
使用引用类型作为类中的成员变量时,需要在 初始化列表 中进行初始化,以下三种类型需要在初始化列表进行初始化:
- 引用类型
const
修饰- 没有默认构造函数的类型
修改之前的代码,不再手动加锁、解锁
void func()
{for (int i = 0; i < 2; i++){try{LockGuard<mutex> lock(mtx);if (i % 2 == 0)throw exception("抛出异常");}catch (const std::exception& msg){cout << msg.what() << endl;}}
}
此时再次运行,可以发现程序正常运行,证明锁资源被自动释放了
其实库中已经提供了 RAII
风格的类了,分别是 lock_guard
和 unique_lock
其中 lock_guard
和我们自己实现的 LockGuard
几乎一样,功能十分简单(构造时加锁,析构时解锁)
而 unique_lock
在此基础上增加了一些功能,比如 加锁、解锁、赋值、交换 等,因为在某些场景中,需要在临界区内对锁资源进行操作,此时就比较适合使用 unique_lock
在使用 互斥锁 时,推荐使用 lock_guard
或者 unique_lock
进行 自动加锁、解锁,避免 死锁问题
2.3.condition_variable 条件变量类
线程安全 不仅需要 互斥锁,还需要 条件变量,条件变量 主要用来同步各线程间的信息(线程同步),同时可以避免 死锁问题,因为如果线程条件不满足,它就会主动将 锁资源 让出,让其他线程先运行
C++11
提供了一个 condition_variable
条件变量类,其中包含了 构造、析构、等待、唤醒 相关接口
条件变量 也是不支持拷贝的,在 wait
等待时,有两种方式:
- 传统等待,传入一个
unique_lock
对象 - 带仿函数的等待,传入一个
unique_lock
对象,以及一个返回值为bool
的函数对象,可以根据函数对象的返回值判断是否需要等待
为什么要在条件变量
wait
时传入一个unique_lock
对象?
因为条件变量本身不是线程安全的,同时在条件变量进入等待状态时,需要有释放锁资源的能力,否则无法将锁资源让出;当条件满足时,条件变量要有申请锁资源的能力,以确保后续操作的线程安全,所以把互斥锁传给条件变量合情合理
注:使用条件变量需要包含 condition_variable
头文件
int main()
{mutex mtx;condition_variable cond;// unique_lock 对象unique_lock<mutex> lock(mtx);// 传统等待cond.wait(lock);// 带函数对象的等待cond.wait(lock, []()->bool { return true; });return 0;
}
注意: 函数对象返回 true
表示条件为真,不需要等待,返回 false
表示需要等待
至于 wait_for
和 wait_until
就是带时间限制的等待,这里不再细谈
notify_one
表示随机唤醒一个正在等待中的线程,notify_all
表示唤醒所有正在等待中的线程,如果唤醒时,没有线程在等待,那就什么都不会发生
条件变量 的使用看似简单,关键在于如何结合具体场景进行设计
2.3.1.交替打印数字
题目要求
给你两个线程T1
、T1
,要求T1
打印奇数,T2
打印偶数,数字范围为[1, 10]
,两个线程必须交替打印
两个线程交替打印,并且打印的是同一个值,所以需要使用 互斥锁 保护,由于题目要求 T1
打印奇数,T2
打印偶数,可以使用 条件变量 来判断条件是否满足,只有满足才能打印,具体实现代码如下
int main()
{mutex mtx;condition_variable cond;int n = 10;int x = 1; // 从 1 开始// 创建线程thread T1([&, n]()->void {while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情况if (x == n && n % 2 == 0)break;// 不为奇数就等待while (x % 2 != 1)cond.wait(lock); 直接这样写也是可以的//cond.wait(lock, [&]()->bool { return x % 2 == 1; });cout << "T1: " << x++ << endl;// 唤醒其他线程cond.notify_one();}});thread T2([&, n]()->void{while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情况if (x == n && n % 2 == 1)break;// 不为偶数,就等待while (x % 2 != 0)cond.wait(lock); 这样写也是可以的//cond.wait(lock, [&]()->bool {return x % 2 == 0; });cout << "T2: " << x++ << endl;// 唤醒其他线程cond.notify_one();}});T1.join();T2.join();return 0;
}
如何确保两个线程交替打印?
某个线程在打印后,条件必定不满足,只能 wait
等待,在这之前会唤醒另一个线程进行打印,因为数字范围全是正数,即只有奇数和偶数两种状态,所以两个线程可以相互配合、相互唤醒,从而达到交替打印的效果
如何确保打印时不会出现非法情况?
判断待打印的数字是否符合范围,如果不符合就不进行打印,直接 break
结束循环,因为这里是 RAII
风格的锁,所以不必担心死锁问题
2.4.atomic 原子操作类
在学习 atomic
原子操作类 之前,需要先看看什么是 原子操作
原子操作 是一种 “可靠” 的操作,只允许存在 成功 和 失败 两种状态,比如对变量的修改,要么修改成功,要么修改失败,不会存在修改一半被切走的状态(被别人影响)
要想实现 原子操作 就得确保硬件支持 CAS
(compare and swap)硬件同步原语,CAS
简单来说就是 操作前先保存旧值,准备进行操作时,取操作数的值与旧值进行比较,如果相同就进行操作,否则就更新旧值,准备重新操作
结合具体的场景理解,假设现在有一个单链表 list
,线程A 在进行尾插时,线程B 也进行了尾插,并且插入过程比 线程A 快,此时得益于 CAS
,线程A 发现需要连接的节点变了,也就不再进行插入,而是更新尾节点信息,重新尾插
也就是说,基于 CAS
的 原子操作 需要确保待操作数没有发生改变,如果被其他线程更改了,就不能进行之前的操作,而是需要更新信息后重新操作
类似的代码实现如下(基于无锁队列实现的链表)
EnQueue(Q, data) //进队列
{//准备新加入的结点数据n = new node();n->value = data;n->next = NULL;do {p = Q->tail; //取链表尾指针的快照} while( CAS(p->next, NULL, n) != TRUE); //while条件注释:如果没有把结点链在尾指针上,再试CAS(Q->tail, p, n); //置尾结点 tail = n;
}
如果只是单纯的进行 i++
操作,CAS
逻辑可以写成这样
int i = 0;
int old = i; // 保存旧值// 如果 CAS 函数在对 old 和 i 进行比较时,发现两者不相等
// 就会返回 `false`,进入循环更新 `old` 旧值,准备下一次 CAS 判断
// 直到两者相等,才会进行操作,确保整个过程是原子的
while (!CAS(&i, old, old+1))
{old = i;
}// 进行操作
// ...
关于 CAS
的更多详细信息可以看看 陈皓 大佬的这篇文章:《无锁队列的实现》
CAS
操作可以自己手搓,也可以使用库中提供的,比如 C++11
中的 atomic
原子操作类,其中提供了一系列 原子操作,比如 加、减、位运算
借助 atomic
原子操作 类,就可以在不使用锁的情况下,确保整型变量 g_val
的线程安全
注:使用 atomic
原子操作类需要包含 atomic
这个头文件
// 定义为原子变量
atomic<int> g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}
除了整型 int
之外,atomic
还支持定义以下类型为 原子变量
atomic
定义的原子变量类型与普通变量类型并不匹配,比如使用 printf
进行打印时,就无法匹配 %d
这个格式
int main()
{// 定义为原子变量atomic<int> val = 0;printf("%d\n", val);return 0;
}
此时可以借助 atomic
类中的 load
函数,加载该原子类型的普通类型值
此时可以正常匹配
// ...
printf("%d\n", val.load());
// ...
除了 load
之外,还可以使用 store
获取其中的值
// ...
int tmp = 0;
val.store(tmp);
printf("%d\n", tmp);
// ...
线程库中还有一个
future
类,用于 异步编程和数据共享,并不是很常用,这里就不作介绍,使用细节可以看看这篇文章 《C++11中std::future的使用》
3.包装器
包装器 属于 适配器 的一种,正如 栈和队列 可以适配各种符合条件的容器实现一样,包装器 也可以适配各种类型相符的函数对象,有了 包装器 之后,对于相似类型的多个函数的调用会变得十分方便
3.1.function 包装器
现在我们已经学习了多种可调用的函数对象类型
- 普通函数
- 仿函数
lambda
表达式
假设这三种函数对象类型的返回值、参数均一致,用于实现不同的功能,如何将它们用同一个类型来表示?
// 普通函数
void func(int n)
{cout << "void func(int n): " << n << endl;
}// 仿函数
struct Func
{
public:void operator()(int n){cout << "void operator()(int n): " << n << endl;}
};// lambda 表达式
auto lambda = [](int n)->void{cout << "[](int n)->void: " << n << endl;};
如果 C
语言中的指针学的还可以的话,可以试试使用 函数指针 来表示这三个函数对象的类型
遗憾的是,无法直接使用 函数指针 指向 仿函数对象,也无法指向 类对象
int main()
{void(*pf)(int); // 返回值为 void,参数为 int 的函数指针pf = func;pf(10);//Func f;//pf = f(); // 无法赋值pf = lambda;pf(20);return 0;
}
在 C++11
中,增加了 function
包装器 这个语法,专门用来包装函数对象,function
包装器 是基于 可变参数模板 实现的,原型如下
template <class Ret, class... Args>
class function<Ret(Args...)>;
其中 Ret
表示函数返回值,Args
是上文中提到的可变参数包,表示传给函数的参数,function
模板类通过 模板特化 指明了包装的函数对象类型
有了 function
包装器 后,可以轻松包装之前的三个函数对象
注:使用 function
包装器需要包含 functional
头文件
int main()
{// 包装器function<void(int)> f;f = func;f(10);f = Func();f(20);f = lambda;f(30);return 0;
}
包装器 可以结合 哈希表 使用,提前准备一批任务,根据用户发出的不同指令来调用不同的任务,比如下面这个程序,完美地在 指令 与 函数 之间建立了映射关系
int main()
{// 包装了返回值为 void,参数为 void 的函数类型unordered_map<string, function<void(void)>> hash;hash["下载请求"] = []()->void { cout << "正在进行下载任务..." << endl; };hash["SQL查询"] = []()->void { cout << "正在进行SQL查询..." << endl; };hash["日志记录"] = []()->void { cout << "正在记录日志信息..." << endl; };string comm; // 指令while (cin >> comm){if (!hash.count(comm))cout << "该指令不存在,请重新输入" << endl;elsehash[comm](); // 调用函数}return 0;
}
根据给出的指令,调用对应的函数
function
包装器 还可以用在刷题中,比如下面这道题目中,就可以使用 包装器 在 运算符 与 具体操作 之间建立映射关系,使用起来十分方便
150. 逆波兰表达式求值
class Solution
{
public:int evalRPN(vector<string>& tokens) {// 解题思路:操作数入栈,遇到操作符,取两个数计算后,入栈// 建立映射关系unordered_map<string, function<int(int, int)>> hash = {{"+", [](int x, int y)->int { return x + y; } },{"-", [](int x, int y)->int { return x - y; } },{"*", [](int x, int y)->int { return x * y; } },{"/", [](int x, int y)->int { return x / y; } },};stack<int> s;for(auto str : tokens){if(str != "+" && str != "-" && str != "*" && str != "/")s.push(stoi(str));else{// 注意:先获取 y,再获取 xint y = s.top();s.pop();int x = s.top();s.pop();s.push(hash[str](x, y));}}return s.top();}
};
关于这道题的详细题解可以看看这篇文章 《C++题解 | 逆波兰表达式相关》
function
包装器 除了可以包装常规函数对象外,还可用于包装 类内成员函数
包装 静态成员函数 很简单,指明归属于哪个类就行了
class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}private:int _n = 10;
};int main()
{// 包装静态函数function<void(int)> f = Test::funcA;//function<void(int)> f = &Test::funcA; // 这么写也是可以的f(10);return 0;
}
如果包装 非静态成员函数 就有点麻烦了,因为 非静态成员函数 需要借助 对象 或者 对象指针 来进行调用
解决方法是:构建 function
包装器时,指定第一个参数为类,并且包装时需要取地址 &
使用时则需要传入一个 对象,此时传入 匿名对象 或者 普通对象 都行
// 包装非静态函数
function<void(Test, int)> f = &Test::funcB;// 传入匿名对象
f(Test(10), 10);// 传入普通对象
Test t(10);
f(t, 10);
关于包装时的参数设置问题
为什么不能设置为 类的指针,这样能减少对象传递时的开销
因为设置如果设置为指针,后续在进行调用时,就需要传地址,如果是普通对象还好说,可以取到地址,但如果是匿名对象(右值)是无法取地址的,也就无法调用函数了
那能否设置成 类的左值引用 呢?
不行,如果是左值还好,但右值无法被左值引用接收
参数设置为 const
指针 或者 右值引用 又会导致 左值 无法正常传递,所以这里最理想的方案就是单纯设置为 普通类类型,既能接受 左值,也能接受 右值
将参数写成
&&
不是会触发引用折叠机制吗,这样不就既能接收左值,也能接收右值了?
不行,引用折叠(万能引用)是指模板推导类型的行为,普通函数是没有这个概念,如果普通函数既想接收左值,又想接收右值,只能重载出两个参数不同的版本了
3.2.bind 绑定
bind
绑定 是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表
bind
绑定 可以修改参数传递时的位置以及参数个数,生成一个可调用对象,实际调用时根据 修改 规则进行实际的函数调用,具体原型如下
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
fn
是传递的 函数对象,args
是传给函数的 可变参数包,这里使用了 万能引用(引用折叠),使其在进行模板类型推导时,既能引用左值,也能引用右值
使用 bind
绑定 改变参数传递顺序
注:placeholders
是一个命名空间,其中的 _1
、_2
、_N
称为占位符,分别表示函数中的第1、第2、第N个参数,直接使用就行了
void Func(int a, int b)
{cout << "void Func(int a, int b): " << a << " " << b << endl;
}int main()
{// 正常调用Func(10, 20);// 绑定生成一个可调用对象auto RFunc = bind(Func, placeholders::_2, placeholders::_1);RFunc(10, 20);return 0;
}
经过 bind
绑定 后,同样的参数传递,出现了不同的调用结果
bind
的底层也是仿函数,生成一个对应的类,根据用户指定的规则,去调用函数,比如这里经过绑定后,实际调用时,RFunc
中实际在调用Func
传递的参数为20 10
除了使用 auto
自动推导 bind
生成的可调用对象类型外,还可以使用 包装器 来包装出类型
// 使用包装器包装出类型
function<void(int, int)> RFunc = bind(Func, placeholders::_2, placeholders::_1);
bind
绑定 改变参数传递顺序很少使用,只需要简单了解即可
注意: 在使用 bind
绑定改变参数传递顺序时,参与交换的参数类型,至少需要支持隐式类型转换,否则是无法交换传递的
bind
绑定 还可以用来指定参数个数,比如对上面的函数 Func
进行绑定,将参数 1
始终绑定为 100
,后续进行调用时,只需要传递一个参数
int main()
{// 使用包装器包装出类型auto RFunc = bind(Func, 100, placeholders::_1);RFunc(20);RFunc(10, 20);return 0;}
此时如果坚持传递参数,会优先使用绑定的参数,再从函数参数列表中,从左到右选择参数进行传递,直到参数数量符合,比如这里第二次调用虽然传递了 10
和 20
,但实际调用 Func
时,RFunc
会先传递之前绑定的值 100
作为参数1传递,而 10
会作为参数2传递,至于 20
会被丢弃
注意: 无论绑定的是哪一个参数,占位符始终都是从 _1
开始,并且连续设置
绑定普通参数显得没意思,bind
绑定 参数个数用在 类的成员函数 上才舒服,比如对之前 function
包装器 包装 类的成员函数 代码进行优化,直接把 类对象 这个参数绑定,调用时就不需要手动传递 对象 了
class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}void funcC(){}private:int _n = 10;
};int main()
{function<void(int)> RFuncB = bind(&Test::funcB, Test(10), placeholders::_1);RFuncB(10);return 0;
}
除了可以绑定类对象外,也可以直接绑定 val
这个参数,亦或是两者都绑定
// 绑定对象
function<void(Test, int)> f1 = bind(&Test::funcB, placeholders::_1, 10);
f1(Test(), 0);// 两者都绑定
function<void(int)> f2 = bind(&Test::funcB, Test(10), 20);
f2(0);
注意: 虽然参数已经绑定了,但实际调用时,仍然需要传递对应函数的参数,否则无法进行函数匹配调用,当然实际传入的参数是绑定的值,这里传参只是为了进行匹配;并且如果不对类对象进行绑定,需要更改包装器中的类型,调用时也需要传入参数进行匹配
🌆总结
在这C++11系列的收尾文章中,我们深入研究了lambda表达式,为函数对象提供了快速构建的方法。接着,我们学习了标准线程库,包括线程、互斥锁、条件变量等,为跨平台的多线程编程提供了强大工具。最后,通过包装器和绑定工具,我们获得了统一函数对象类型的新手段,使得代码更灵活、可读性更强,为现代C++编程提供了丰富的工具和技巧
相关文章推荐
C++ 进阶知识
C++11『右值引用与移动语义』
C++11『基础新特性』
C++ 哈希的应用【布隆过滤器】
C++ 哈希的应用【位图】
C++【哈希表的完善及封装】
C++【哈希表的模拟实现】
C++【初识哈希】
C++【一棵红黑树封装 set 和 map】