这是一篇C++的新特性汇总的blog, 目的在于整理最近学的知识.
智能指针
shared_ptr
共享智能指针,每个shared_ptr的拷贝指向相同的内存,当最后一个shared_ptr析构后,内存才会被释放。
其原理是shared_ptr有引用计数,每当有新的共享指针指向同一块内存时,引用计数就会+1;每当某共享指针不指向这块内存时,引用计数会减1。当引用计数为0时,会释放掉这块内存。
当两个对象相互使用一个shared_ptr成员变量指向对方,会造成内存泄漏。比如共享链表.
// 赋值的四种方法
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3; p3.reset(new int(1));
auto sp1 = make_shared<int>(100); //最推荐这种方法
std::shared_ptr<int> p = new int(1); //错误的赋值方法, shared_ptr不能通过这种方法赋值
// 获取原始指针
int *p = p1.get(); //比较危险,慎用
// 指定删除器 用shared_ptr管理非new对象或没有析构函数的类,应为其传递合适的删除器
void DeleteIntPtr(int *p);
std::shared_ptr<int> p(new int(1), DeleteIntPtr); //DeleteIntPtr可以是函数,也可以是lambda
std::shared_ptr<int> p1(new int[10], [](int *p) {delete [] p;}); //当用shared_ptr管理动态数组时,必须指定删除器,因为shared_ptr的默认删除器不支持数组对象
使用shared_ptr要注意的问题
// 不要用一个原始指针初始化多个shared_ptr
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //原因是,p1 和 p2 互相不知,有可能导致ptr指向的内存被析构两次
// 不要在函数实参中创建shared_ptr
function(shared_ptr<int>(new int), g()); //函数参数的计算顺序是不确定的,如果是先new int,再调用参g(),但是g()发生了一次异常,而shared_ptr还没有创建,则int内存泄漏了
// 通过shared_from_this()返回this指针
// error example
class A {
...
public:
shared_ptr<A> GetSelf() {
return shared_ptr<A>(this); //会导致重复析构,原因是同一个this构造了两个智能指针
}
...
}
int main() {
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
// right example
class A : public std::enable_shared_from_this<A> {
...
public:
shared_ptr<A> GetSelf() {
return shared_from_this(); //没有问题
}
...
}
int main() {
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
//避免循环引用. 循环引用会导致内存泄漏
class A {
public:
std::shared_ptr<B> bptr;
};
class B {
public:
std::shared_ptr<A> aptr;
};
int main() {
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap; //会导致内存泄漏,解决办法是把A或B的任何一个成员变量改为weak_ptr
}
unique_ptr
独占型指针,不允许其他的智能指针共享其内部的指针,也不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr;但可以转移其指向内存的所有权。
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = std::move(my_ptr);
auto ptr = std::make_unique<Widget>();
和shared_ptr的一些区别
-
默认的unique_ptr可以指向一个数组, 默认的shared_ptr不行,除非指定删除器
-
unique_ptr指定删除器需要确定删除器的类型,不能向shared_ptr那样直接指定删除器
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误 std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //正确
weak_ptr
是一种不控制对象生命周期的智能指针, 它指向一个shared_ptr管理的对象, 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段. weak_ptr设计的目的是为配合shared_ptr而引入的一种智能指针,用来协助shared_ptr工作. 它只能从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少.
weak_ptr是用来解决shared_ptr相互引用时的死锁问题. 它没有重载操作符*和->, 因为它不共享指针,不能操作资源,存粹是作为一个旁观者来监视shared_ptr中管理的资源是否存在.
// 通过use_count()方法获取当前观察资源的引用计数
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
count << wp.use_count() << endl; //结果是1
// 通过expired()判断所观察资源是否已经释放
wp.expired(); // 资源被释放,返回true,否则返回false
// 通过lock()操作资源
std::weak_ptr<int> gw;
void f() {
if (gw.expired()) {
cout << "gw无效, 资源已经释放";
} else {
auto spt = gw.lock(); //如果资源被释放,返回空指针
cout << "gw有效, *spt = " << *spt << endl;
}
}
int main() {
{
auto sp = std::make_shared<int>(42);
gw = sp;
f();
}
f();
return 0;
}
shared_ptr部分中提到不能直接将this指针返回shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回.
weak_ptr解决循环引用问题
将A或B的任意一个成员变量改为weak_ptr.
class A {
public:
std::weak_ptr<B> bptr;
};
class B {
public:
std::shared_ptr<A> aptr;
}
int main() {
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
return 0;
}
weak_ptr使用注意事项
weak_ptr使用前要检查合法性, 对于已经被释放的资源, lock只会返回空指针.
智能指针安全性问题
- shared_ptr本身不是线程安全的, 多线程代码操作的是同一个shared_ptr的对象,此时是不安全的
- 多线程代码操作的不是同一个shared_ptr的对象
右值引用和移动语义
右值是指表达式结束时就不存在的临时对象. 移动是指将内存所有权从一个对象移动给另一个对象的行为.
右值又分为将亡值和纯右值. 将亡值是指即将被销毁并且可以被移动的对象, 纯右值是指不与任何对象关联的临时表达式.
右值引用就是对一个右值进行引用的类型。通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样. 例如:
int && a = 5;
右值引用总结:
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
- auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
- 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。
- 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
移动语义
int str;
int &&str1 = std::move(str); //std::move()是用来将左值转换为右值, 以方便使用右值引用
完美转发
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
int &&a = 10;
int &&b = a; //错误 这时的a本质上是一个左值
int &&b = std::forward<int>(a); //正确 a会被转化为最初的类型--右值
std::forward不是独自运作的,完美转发 = std::forward + 万能引用 + 引用折叠。三者合一才能实现完美转发的效果。
万能引用
-
万能引用只能出现在类型推导的场合;
-
万能引用必须具有T&&的形式, 其中T是一个被推导的类型
-
万能引用的实际类型取决于它的初始化物,如果初始化物是左值,则万能引用是左值引用,如果初始化物是右值,则万能引用是右值引用。
示例:
template <typename T>
void func(T&& a) {}
int b = 5;
func(b); //T为int & 通过引用折叠,最终和左值引用匹配
func(5); //T为int && 通过引用折叠,最终和右值匹配
引用折叠
一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:
- 左值-左值 T& & # 函数定义的形参类型是左值引用,传入的实参是左值引用
- 左值-右值 T& && # 函数定义的形参类型是左值引用,传入的实参是右值引用
- 右值-左值 T&& & # 函数定义的形参类型是右值引用,传入的实参是左值引用
- 右值-右值 T&& && # 函数定义的形参类型是右值引用,传入的实参是右值引用
但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:
所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
完美转发原理
std::forward在libstdc++中的实现是:
template<typename _Tp>
75 constexpr _Tp&&
76 forward(typename std::remove_reference<_Tp>::type& __t) noexcept
77 { return static_cast<_Tp&&>(__t); }
注: forward必须通过显示模板实参调用, 不能依赖函数模板参数推导
有如下三个函数
template<typename T>
void print(T & t) {};
template<typename T>
void print(T && t) {};
template<typename T>
void func(T &&t) {
print(std::forward<T>(t));
}
第一种情况:
int a = 5;
func(a);
根据万能引用的实例化规则,实例化后的函数是:
T = int &
void func(int & && t) {
print(std::forward<int &>(t));
}
根据引用折叠,这段代码等价于:
T = int &
void func(int & t) {
print(std::forward<int &>(t));
}
实例化std::forward
constexpr int & &&
forward(typename std::remove_reference<int &>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int & &&>(__t); }
//折叠后为
constexpr int &
forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &>(__t); }
所以最终版本, 是将参数强制转化成int &
第二种情况:
func(5);
根据万能引用的实例化规则,实例化后的函数是:
T = int
void func(int && t) {
print(std::forward<int>(t));
}
这段代码不需要引用折叠.
实例化std::forward
constexpr int &&
forward(typename std::remove_reference<int>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &&>(__t); }
//折叠后为
constexpr int &&
forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &&>(__t); }
所以最终版本, 是将参数强制转化成int &&
这就是完美转发的原理
右值引用使用技巧:
尝试使用emplace_back代替push_back.
匿名函数lambda
基本语法介绍
[捕获列表] (参数列表) -> 返回类型 {函数体}
示例:
auto add = [](int a, int b)->int {
return a +b;
}
std::cout << add(1, 2) << std::endl;
如果只有一条return语句,编译器无法自动推断出返回类型,所以可以省略函数返回类型,但是如果函数体内有多个return语句时,编译器无法自动推断出返回类型,此时必须指定返回类型.
匿名函数的简写
匿名函数由捕获列表、参数列表、返回类型和函数体组成;可以忽略参数列表(如果没有参数)和返回类型,但不可以忽略捕获列表和函数体,如:
auto f = []{ return 1 + 2; };
Lambda捕获列表
捕获列表是让匿名函数能够使用外部变量.
[] | 空捕获列表,Lambda不能使用所在函数中的变量。 |
---|---|
[names] | names是一个逗号分隔的名字列表,这些名字都是Lambda所在函数的局部变量。默认情况下,这些变量会被拷贝,然后按值传递,名字前面如果使用了&,则按引用传递 |
[&] | 隐式捕获列表,Lambda体内使用的局部变量都按引用方式传递 |
[=] | 隐式捕获列表,Lanbda体内使用的局部变量都按值传递 |
[&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量, 这些变量采用值捕获的方式,其他变量则被隐式捕获,采用引用方式传递identifier_list中的名字前面不能使用&。 |
[=,identifier_list] | identifier_list中的变量采用引用方式捕获,而被隐式捕获的变量都采用按值传递的方式捕获。identifier_list中的名字不能包含this,且这些名字面前必须使用&。 |
例子:
int main() {
int c = 12;
auto Add = [c](int a, int b)->int {...} //捕获的值,按值传递
auto Bdd = [&c](int a, int b)->int {...} //捕获的值,按引用传递
}
STL
STL定义了强大的、基于模板的、可复用的组件,实现了许多通用的数据结构及处理这些数据结构的算
法。其中包含三个关键组件——容器(container,流行的模板数据结构)、迭代器(iterator)和算法
(algorithm)。
容器: 用来管理某一类对象的集合.
迭代器: 用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集
算法: 作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作
容器
STL容器,可将其分为四类:序列容器、有序关联容器、无序关联容器、容器适配器
序列容器:
标准库容器类 | 描述 |
---|---|
vector | 从后部进行快速插入和删除操作,直接访问任意元素 |
array | 固定大小,直接访问任意元素 |
deque | 从前部或后部进行快速插入和删除操作,直接访问任何元素 |
forward_list | 单链表,在任意位置快速插入和删除 |
list | 双向链表,在任意位置进行快速插入和删除操作 |
有序关联容器:
标准库容器类 | 描述 |
---|---|
set | 快速查找,无重复元素 |
multiset | 快速查找,可有重复元素 |
map | 一对一映射,无重复元素,基于键快速查找 |
multimap | 一对一映射,可有重复元素,基于键快速查找 |
无序关联容器:
标准库容器类 | 描述 |
---|---|
unordered_set | 快速查找,无重复元素 |
unordered_multiset | 快速查找,可有重复元素 |
unordered_map | 一对一映射,无重复元素,基于键快速查找 |
unordered_multimap | 一对一映射,可有重复元素,基于键快速查找 |
容器适配器:
标准库容器类 | 描述 |
---|---|
stack | stack 后进先出(LIFO) |
queue | 先进先出(FIFO) |
priority_queue | 优先级最高的元素先出 |
迭代器
迭代器在很多方面与指针类似,也是用于指向首类容器中的元素.
使用一个 iterator 对象来指向一个可以修改的容器元素,使用一个 const_iterator 对象来指向一个不能修改的容器元素。
类型 | 描述 |
---|---|
随机访问迭代器 | 在双向迭代湍基础上增加了直接访问容器中任意元素的功能, 即可以向前或向后跳转任意个元素 |
双向迭代器 | 在前向迭代器基础上增加了向后移动的功能。支持多遍扫描算法 |
前向迭代器 | 综合输入和输出迭代器的功能,并能保持它们在容器中的位置(作为状态信息),可以使用同一个迭代器两次遍历一个容器(称为多遍扫描算法) |
输出迭代器 | 用于将元素写入容器。 输出迭代楛每次只能向前移动一个元索。 输出迭代器只支持一遍扫描算法,不能使用相同的输出迭代器两次遍历一个序列容器 |
输入迭代器 | 用于从容器读取元素。 输入迭代器每次只能向前移动一个元素。 输入迭代器只支持一遍扫描算法,不能使用相同的输入迭代器两次遍历一个序列容器 |
每种容器所支持的迭代器类型决定了这种容器是否可以在指定的 STL 算 法中使用.
各容器支持的迭代器的类型.
容器 | 支持的迭代器类型 | 容器 | 支持的迭代器类型 |
---|---|---|---|
vector | 随机访问迭代器 | set | 双向迭代器 |
array | 随机访问迭代器 | multiset | 双向迭代器 |
deque | 随机访问迭代器 | map | 双向迭代器 |
list | 双向迭代器 | multimap | 双向迭代器 |
forword_list | 前向迭代器 | unordered_set | 双向迭代器 |
stack | 不支持迭代器 | unordered_multiset | 双向迭代器 |
queue | 不支持迭代器 | unordered_map | 双向迭代器 |
priority_queue | 不支持迭代器 | unordered_multimap | 双向迭代器 |
example:
std::vector<int> vec;
auto iter1 = vec.begin(); // 返回普通迭代器
auto iter2 = vec.cbegin(); // 返回const迭代器,这个迭代器不能修改元素
if (iter1 != vec.end()) {...} // vec.end()指向最后一个元素的后面的一个元素,不能用来读写数据,只能用来辅助判断是否遍历完了容器或是否是空容器
算法
STL提供了可以用于多种容器的算法,其中很多算法都是常用的, 比如插入、删除、搜索、排序等.
STL包含了大约70个标准算法,作用在容器元素上的算法只是间接地通过迭代器来实现。另外,还可以使用相似的方法创建自己的算法,这样它们就能和STL容器及迭代器一起使用了。
function 和 bind用法
在设计回调函数的时候,无可避免地会接触到可回调对象。在C++11中,提供了std::function和std::bind两个方法来对可回调对象进行统一和封装。
C++语言中有五种可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
function用法
void printA(int a) {...};
std::function<void(int a)> func = printA; //保存普通函数
std::function<void()> func_1 = [](){cout << "hello world" << endl;}; //保存lambda表达式
class Foo{
public:
Foo(int num) : num_(num){}
void print_add(int i) const {cout << num_ + i << endl;}
int num_;
};
std::function<void(const Foo&,int)> func3 = &Foo::print_add; //保存成员函数 //添加&的作用,是表示该对象是一个成员函数指针,并且需要一个类的实例来调用它
bind用法
bind函数是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般格式是:
auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的
参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参
数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为
newCallable的第一个参数,_2为第二个参数,以此类推。
示例:
void fun_1(int x, int y, int z) {cout << "print: x="<<x<<",y="<<y<<",z"<<z<<endl;}
void fun_2(int &a, int &b) {a++;b++;cout<<"print:a="<<a<<",b="<<b<<endl;}
class A {
public:
void fun_3(int k, int m) {...}
int main {
auto f1 = std::bind(fun_1, 1, 2, 3) //绑定fun_1的第一二三个参数为:1,2,3
f1();
auto f2 = std::bind(fun_1, placeholders::_1, placeholders::_2, 3); //表示绑定fun_1的第三个参数为3, 而fun的第一二个参数分别由调用f2的第一二个参数指定
f2(1, 2);
auto f3 = std::bind(fun_1, placeholders::_2, placeholders::_1, 3); //fun_1的第一二个参数分别由调用f3的第二一个参数指定
f3(1, 2);
int m = 2;
int n = 3;
auto f4 = std::bind(fun_2, placeholders::_1, n); //对于通过std::placeholders传递的参数,通过引用传递,如m; 对于事先绑定的参数,通过值传递.
f4(m);
A a;
auto f5 = std::bind(&A::fun_3, a, placeholder::_1, placeholders::_2);
f5(10, 20); //调用a.fun_3(10, 20);
}
可变参数模板
一个可变参数模板就是一个可以接受可变数目参数的模板函数或模板类. 可变数目的参数被称为参数包. 存在两种参数包: 模板参数包–表示零个或多个模板参数; 函数参数包–表示零个或多个函数参数.
可变参数模板示例如下:
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest) {
auto count1 = sizeof...(Args);
auto count2 = sizeof...(rest);
}
示例中,Args就是模板参数包, rest就是函数参数包, **sizeof…**是求包中有多少元素的运算符
可变模板参数函数
可变参数函数通常是递归的. 例子如下
template<typename T>
ostream &print(ostream &os, const T &t) {
return os << t;
}
template<typeme T>
ostream &print(ostream &os, const T &t, const Args&... rest) {
os << t << ", ";
return print(os, rest...);
}
"rest"是参数包, "rest…"则是包扩展. "const T &"则是模式, 包扩展就是将它分解为构成的元素.
模板特例化
一个特例化版本本质上是一个实例, 不是函数的重载版本
example:
template<typename T> int compare(const T&, const T&) {};
template<> int compare(const char* const &p1, const char* const &p2) {};
当我们定义一个特例化版本时, 函数参数类型必须与一个先前声明的模板中对应的类型匹配.
模板匹配规则:
- 模板函数
- 模板特例化函数
- 普通函数
如果一个函数调用,对这三个函数来说都是精准匹配,那么被匹配的优先级是:3, 2, 1
C++多线程
线程thread
关键成员函数:
get_id() //获取线程ID,返回类型std::thread::id对象。
joinable() //判断线程是否可以加入等待
join() //等该线程执行完成后才返回。
detach() //将本线程从调用线程中分离出来,允许本线程独立执行。
创建线程示例:
#include <iostream>
#include <thread>
using namespace std;
void func1() {
cout << "func1" << endl;
}
void func2(int a, int b) {
cout << "func2" << endl;
}
class A {
public:
static void func3(int a) {
cout << "func3" << endl;
}
};
int main() {
thread t1(func1);
t1.join();
int a = 10;
int b = 20;
thread t2(func2, a, b);
t2.join();
thread t3(A::func3, 1);
if (t3.joinable()) { //确保线程是在活跃状态
t3.join();
}
return 0;
}
std::thread创建的线程对象, 会在构造函数返回后,尽快开始执行,但具体的事件取决于操作系统的调度策略.
一个std::thread对象在销毁之前必须要调用join ()或者detach (),否则会导致程序终止。是因为如果这个对象没有调用join ()或者detach (),那么它的析构函数会调用std::terminate (),这个函数会终止整个进程,而不仅仅是当前线程。这样做是为了避免资源泄漏或者不一致的状态,因为如果一个线程被强制结束,它可能没有执行完所有的析构函数或者清理操作。
如果主线程从main函数返回,或者调用std::exit (),那么整个进程都会终止,并且不会等待其他线程的结束。
互斥量mutex
C++11提供如下4种语义的互斥量(mutex)
- std::mutex, 独占的互斥量, 不能递归使用
- std::time_mutex, 带超时的独占互斥量, 不能递归使用
- std::recursive_mutex, 递归互斥量, 不带超时功能.
- std::recursive_timed_mutex, 带超时的递归互斥量
和普通锁比, 递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
独占互斥量std::mutex
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
主要成员函数:
- 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于unlocked 状态的。
- lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
- unlock(), 解锁,释放对互斥量的所有权, 并唤醒其中一个等待同一个互斥量对象的线程.
- try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
递归互斥量std::recursive_mutex
主要成员函数和std::mutex差不多,但是尽量不要用递归锁,原因是:
- 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩.
- 递归锁比起非递归锁,效率会低;
- 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误。
带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until
- try_lock_for接受一个持续时间作为参数,表示线程最多等待多长时间来获取锁。如果在这段时间内锁可用,线程就锁定互斥对象并返回true。如果在这段时间内锁不可用,线程就放弃获取锁并返回false。
- try_lock_until接受一个时间点作为参数,表示线程最多等待到什么时候来获取锁。如果在这个时间点之前锁可用,线程就锁定互斥对象并返回true。如果到了这个时间点锁还不可用,线程就放弃获取锁并返回false。
std::timed_mutex m;
void task1() {
std::cout << "Task 1 waiting for lock" << std::endl;
if (m.try_lock_for(std::chrono::seconds(5))) {
std::cout << "Task 1 acquired lock" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
m.unlock();
std::cout << "Task 1 released lock" << std::endl;
} else {
std::cout << "Task 1 failed to acquire lock" << std::endl;
}
}
void task2() {
std::cout << "Task 2 waiting for lock" << std::endl;
if (m.try_lock_until(std::chrono::steady_clock::now() + std::chrono::seconds(3))) {
std::cout << "Task 2 acquired lock" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
m.unlock();
std::cout << "Task 2 released lock" << std::endl;
} else {
std::cout << "Task 2 failed to acquire lock" << std::endl;
}
}
int main() {
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
return 0;
}
lock_guard和unique_lock的使用和区别
相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式。这里涉及到unique_lock,lock_guard的使用。
PS: C++相较于C引入了很多新的特性, 比如可以在代码中抛出异常, 如果还是按照以前的加锁解锁的话代码会极为复杂繁琐
unique_lock,lock_guard的区别:
- unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能.
- unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,
lck.lock()进行上锁,而不必等到析构时自动解锁。
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
void fun1() {
while (true) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one();
sleep(10);
}
}
void fun2() {
while (true) {
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){return !q.empty();}); //这里一直休眠到cond.notify_one唤醒
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "thread2 get value form thread1: " << data << std::endl;
}
}
int main() {
std::thread t1(fun1);
std::thread t2(fun2);
t1.join();
t2.join();
return 0;
}
这里必须使用unique_lock的原因是:条件变量在wait时会进行unlock再进入休眠, lock_guard并无该操作接口.
wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则wait函数返回否则释放锁继续休眠.
notify: 进行notify动作并不需要获取锁.
条件变量
互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。
使用条件变量的一般过程:
- 拥有条件变量的线程获取互斥量;
- 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
- 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有等待线程。
成员函数
- wait: 这里必须使用unique_lock,因为wait函数的工作原理:
- 当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不
能使用lock_guard对象。 - 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一
个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程
会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。 - 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某
一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线
程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥
量解锁,然后休眠,如果为true,则进行后面的操作。
- 当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不
- wait_for: 和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。
- wait_until: 与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和wait类似
- notify_one: 解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的。
- notify_all: 解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。
示例:
写一个同步队列:
template<typename T>
class SimpleSyncQueue
{
public:
SimpleSyncQueue(){}
void Put(const T& x) {
std::lock_guard<std::mutex> locker(_mutex);
_queue.push_back(x);
_notEmpty.notify_one();
}
void Take(T& x) {
std::unique_lock<std::mutex> locker(_mutex);
_notEmpty.wait(locker, [this]{return !_queue.empty(); });
x = _queue.front();
_queue.pop_front();
}
bool Empty() {
std::lock_guard<std::mutex> locker(_mutex);
return _queue.empty();
}
size_t Size() {
std::lock_guard<std::mutex> locker(_mutex);
return _queue.size();
}
private:
std::list<T> _queue;
std::mutex _mutex;
std::condition_variable _notEmpty;
};
原子变量
std::atomic是一个用于定义原子类型的对象, 可以在多线程环境下安全地访问和修改.
- 对原子类型的对象的访问不会导致数据竞争,即在不同的线程之间是可见和一致的。
- 对原子类型的对象的操作是不可分割的,即在一个操作完成之前,不会被其他操作干扰或中断。
- 原子类型的对象可以用来实现内存顺序和同步,即指定对其他非原子对象的访问顺序和可见性。
异步操作
std::future
std::future是一个类模板,用来获取异步操作的结果. 它可以使用各种方法查询,等待或提取异步操作的结果, 但是如果异步操作还没有提供结果的话, 这些方法可能会阻塞. 它不可复制.
有两种future, 分别是唯一future(std::future)和共享future(std::shared_future). 前者的实例是仅有的一个指向其关联事件的实例, 后者可以有多个实例指向同一个关联事件.
它的常用的成员函数是:
- 构造函数和赋值操作符:用于创建和移动std::future对象,注意std::future对象是不可复制的,只能移动。
- share:用于将std::future对象转换为std::shared_future对象,这样可以让多个线程共享同一个异步操作的结果。
- get:用于获取异步操作的结果,如果结果还没有就绪,就会阻塞当前线程,直到结果可用。调用get后,std::future对象会变为无效状态,不能再次调用get。
- valid:用于检查std::future对象是否有效,即是否有与之关联的共享状态。
- wait:用于等待异步操作的结果变为可用,不返回结果。
- wait_for:用于等待一段时间或者直到异步操作的结果变为可用,返回等待的状态。
- wait_until:用于等待直到某个时间点或者直到异步操作的结果变为可用,返回等待的状态。
std::async
std::async是C++11中提供的一个非同步函式,它可以让你异步地执行一个可调用对象(如函数、lambda表达式、类成员函数等),并返回一个std::future对象,用于获取执行结果或等待执行完成。
std::async有两种启动策略,分别是std::launch::async和std::launch::deferred。
std::launch::async表示异步模式,即当你调用std::async时,就会立即创建一个新的线程去执行可调用对象,而不会阻塞当前线程。
std::launch::deferred表示延迟模式,即当你调用std::async时,并不会创建新的线程,而是等到你访问std::future对象时,才会在当前线程上执行可调用对象.
如果你不指定启动策略,那么std::async会根据操作系统的情况,自动选择异步模式或延迟模式。
一个示例:
void foo(const string& str) {
cout << "foo: " << str << endl;
}
int main() {
// 异步模式
auto a1 = async(launch::async, foo, "a1"); //a1是std::future类型
cout << "main: --1--" << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "main: --2--" << endl;
a1.get(); // 等待foo("a1")执行完成
cout << "main: --3--" << endl;
// 延迟模式
auto a2 = async(launch::deferred, foo, "a2");
cout << "main: --4--" << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "main: --5--" << endl;
a2.get(); // 在当前线程上执行foo("a2") //开始执行异步任务
cout << "main: --6--" << endl;
return 0;
}
std::packaged_task
std::packaged_task是C++11中提供的一个类模板,它可以将一个可调用对象(如函数、lambda表达式、类成员函数等)包装起来,方便将来作为线程入口函数来调用。
std::packaged_task的模板参数是各种可调用对象的签名,如int(int,int),表示一个接受两个int参数并返回int的可调用对象。
std::packaged_task的构造函数接受一个可调用对象作为参数,并将其保存在内部。
std::packaged_task的重载运算符()可以调用内部保存的可调用对象,并将其返回值传递给一个关联的std::future对象,用于获取执行结果或等待执行完成。
std::packaged_task的get_future()函数可以返回一个与之关联的std::future对象,用于获取执行结果或等待执行完成。
常用的成员方法是:
- 构造函数:可以接受一个可调用对象(如函数、lambda表达式、类成员函数等)作为参数,将其包装为一个异步任务。
- get_future():返回一个与之关联的std::future对象,用于获取执行结果或等待执行完成。
- operator():调用内部保存的可调用对象,并将其返回值传递给关联的std::future对象。
- valid():检查是否有可调用对象被包装,如果没有则返回false。
- reset():重置packaged_task对象,使其可以重新使用同一个可调用对象。
- swap():交换两个packaged_task对象的内容。
示例:
// 一个计算阶乘的函数
int factorial(int n) {
int res = 1;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int main() {
// 创建一个封装了factorial函数的packaged_task对象
std::packaged_task<int(int)> task(factorial);
// 从packaged_task对象中获取关联的future对象
std::future<int> result = task.get_future();
// 创建一个线程,将packaged_task对象移动到其中,并传入参数6
std::thread th(std::move(task), 6);
// 在主线程中等待结果
std::cout << "Waiting for result...\n";
std::cout << "Factorial of 6 is " << result.get() << "\n";
// 等待线程结束
th.join();
return 0;
}
std::promise
std::promise是一个类模板,它可以在一个线程中保存一个值或异常,然后通过一个std::future对象在另一个线程中异步获取。std::promise对象只能使用一次,如果再次设置值或异常,会抛出std::future_error异常。
常用成员方法:
- 构造函数:可以创建一个std::promise对象,并与一个共享状态(通常是std::future)相关联,用于保存一个类型为T的值。
- get_future:可以获取与该std::promise对象相关联的std::future对象,调用该函数之后,两个对象共享相同的共享状态。
- set_value:可以在相关联的共享状态上保存一个类型为T的值,如果共享状态已经有值或异常,则抛出std::future_error异常。
示例:
using namespace std;
void print(std::promise<std::string>& p) {
p.set_value("There is the result whitch you want.");
}
void do_some_other_things() {
std::cout << "Hello World" << std::endl;
}
int main() {
std::promise<std::string> promise;
std::future<std::string> result = promise.get_future();
std::thread t(print, std::ref(promise));
do_some_other_things();
std::cout << result.get() << std::endl;
t.join();
return 0;
}