目录
- 九、可变参数模板
- 十、lambda表达式
- 10.1 C++98中的一个例子
- 10.2 lambda表达式
- 10.3 lambda表达式语法
- 10.3.1 lambda表达式各部分说明
- 10.3.2 捕获列表说明
- 10.4 函数对象与lambda表达式
- 十一、包装器
- 11.1 function包装器
- 11.2 bind
- 十二、线程库
- 12.1 线程
- 12.1.1 thread类的简单介绍
- 12.1.2 thread类中常用函数
- 12.1.2.1 [构造函数](https://legacy.cplusplus.com/reference/thread/thread/thread/)
- 12.1.2.2 [移动拷贝](https://legacy.cplusplus.com/reference/thread/thread/operator=/)
- 12.1.2.3 [thread::get_id函数](https://legacy.cplusplus.com/reference/thread/thread/get_id/) 与 [this_thread::get_id函数](https://legacy.cplusplus.com/reference/thread/thread/get_id/)
- 12.1.2.4 [joinable函数](https://legacy.cplusplus.com/reference/thread/thread/joinable/)
- 12.1.2.5 [join函数](https://legacy.cplusplus.com/reference/thread/thread/join/)
- 12.1.2.6 [detach函数](https://legacy.cplusplus.com/reference/thread/thread/detach/)
- 12.1.2.7 [swap函数](https://legacy.cplusplus.com/reference/thread/thread/swap/)
- 12.2 锁
- 12.2.1 锁的简单介绍
- 12.2.2 mutex中常用的接口及应用
- 12.2.2.1 [lock函数](https://legacy.cplusplus.com/reference/mutex/mutex/lock/)
- 12.2.2.2 [try_lock函数](https://legacy.cplusplus.com/reference/mutex/mutex/try_lock/)
- 12.2.2.3 [unlock函数](https://legacy.cplusplus.com/reference/mutex/mutex/unlock/)
- 12.2.2.4 接口的使用
- 12.2.3 [lock_guard 类](https://legacy.cplusplus.com/reference/mutex/lock_guard/)
- 12.2.4 [unique_lock 类](https://legacy.cplusplus.com/reference/mutex/unique_lock/)
- 12.3 条件变量
- 12.3.1 条件变量的简单介绍
- 12.3.2 condition_variable的中常用的接口
- 12.3.2.1 [wait函数](https://legacy.cplusplus.com/reference/condition_variable/condition_variable/wait/)
- 12.3.2.2 [notify_one函数](https://legacy.cplusplus.com/reference/condition_variable/condition_variable/notify_one/)
- 12.4 综合应用(支持两个线程交替打印,一个打印奇数,一个打印偶数)
- 12.5 原子操作
- 12.5.1 atomic类 的简单介绍
- 12.5.2 atomic类 的简单使用
- 结尾
上一篇文章中我讲述统一的列表初始化、声明、范围for、智能指针、STL中的一些变化、右值引用、移动语义以及新的类功能,本篇文章我将要讲述可变参数模板、lambda表达式、包装器以及线程库。C++11(一)
九、可变参数模板
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
递归函数方式展开参数包
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList("string");
ShowList("string", "vector");
ShowList("string", "vector", '6');
return 0;
}
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。
我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0)
,也是按照这个执行顺序,先执行printarg(args)
,再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}
将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... )
,最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]
。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList("string");
ShowList("string", "vector");
ShowList("string", "vector", '6');
return 0;
}
template <class T>
int PrintArg(T&& t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args&&... args)
{
// 要初始化arr,强行让解析参数包,参数包有一个参数,PrintArg就依次推演生成几个
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList("string");
ShowList("string", "vector");
ShowList("string", "vector", '6');
return 0;
}
STL容器中的empalce相关接口函数:
https://legacy.cplusplus.com/reference/vector/vector/emplace_back/
https://legacy.cplusplus.com/reference/list/list/emplace_back/
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?
int main()
{
list<pair<string, string> > l;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
l.emplace_back("love", "爱");
l.emplace_back("want", "想要");
l.emplace_back(make_pair("miss", "想念"));
l.push_back(make_pair("I", "我"));
l.push_back({ "You", "你"});
for (auto e : l)
cout << e.first << ":" << e.second << endl;
return 0;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的aj::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,
// push_back 是先构造,再移动构造,其实也还好。
list<pair<string, aj::string>> l;
l.emplace_back("love", "爱");
l.emplace_back(make_pair("miss", "想念"));
cout << endl;
l.push_back(make_pair("I", "我"));
l.push_back({ "You", "你" });
cout << endl;
return 0;
}
总结:对于深拷贝的类emplace系列接口相比于insert系列接口效率略微提高一些但是移动构造的成本也足够低。对于浅拷贝的类emplace系列接口相比于insert系列接口效率提高的更多一些。
十、lambda表达式
10.1 C++98中的一个例子
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用sort方法。
#include <algorithm>
#include <functional>
int main()
{
int array[] = { 9,2,6,4,7,1,5,3,0 };
// 默认按照小于比较,排出来结果是升序
sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
#include <algorithm>
#include <functional>
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
10.2 lambda表达式
#include <algorithm>
#include <functional>
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(),
[](const Goods& gl, const Goods& gr) {return gl._price < gr._price; });
sort(v.begin(), v.end(),
[](const Goods& gl, const Goods& gr) {return gl._price > gr._price; });
return 0;
}
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。
10.3 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
10.3.1 lambda表达式各部分说明
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
// 最简单的lambda表达式,没有任何意义
[] {};
// 省略参数列表和返回值类型
int x = 2, y = 5;
[=] {return x + 6; };
// 省略返回值类型,并且这里没有返回值类型
cout << "改变之前-->" << "x:" << x << " y:" << y << endl;
auto func1 = [&](int z) { x = y + z; };
func1(100);
cout << "改变之后-->" << "x:" << x << " y:" << y << endl << endl;
// 各部分都很完善的lambda函数
cout << "改变之前-->" << "x:" << x << " y:" << y << endl;
auto func2 = [=, &y](int z)->int {return y += x + z; };
func2(520);
cout << "改变之后-->" << "x:" << x << " y:" << y << endl << endl;
// 复制捕捉x
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
10.3.2 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]
:表示值传递方式捕捉变量var[=]
:表示值传递方式捕获所有父作用域中的变量(包括this)[&var]
:表示引用传递捕捉变量var[&]
:表示引用传递捕捉所有父作用域中的变量(包括this)[this]
:表示值传递方式捕捉当前的this指针
注意:
-
父作用域指包含lambda函数的语句块.
-
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
[=, &a, &b]
:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]
:值传递方式捕捉变量a和this,引用方式捕捉其他变量 -
捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]
:=已经以值传递方式捕捉了所有变量,捕捉a重复
-
在块作用域以外的lambda函数捕捉列表必须为空。
-
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
-
lambda表达式之间不能相互赋值,即使看起来类型相同
int main() { auto func1 = [] {cout << "Chinsese" << endl; }; auto func2 = [] {cout << "Chinsese" << endl; }; cout << typeid(func1).name() << endl; cout << typeid(func2).name() << endl; return 0; }
void (*PF)();
int main()
{
auto f1 = [] {cout << "Chinsese" << endl; };
auto f2 = [] {cout << "Chinsese" << endl; };
// f1 = f2; // 编译失败--->E0349 没有与这些操作数匹配的 "=" 运算符
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
cout << typeid(f2).name() << endl;
cout << typeid(f3).name() << endl;
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
10.4 函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
struct TotalPrice
{
double operator()(int num, double unit_price)
{
return num * unit_price;
}
};
int main()
{
auto TP = [](int num, double unit_price)
{return num * unit_price; };
TotalPrice()(50, 5.2);
TP(50, 5.2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
。
十一、包装器
11.1 function包装器
function包装器 也叫作适配器。
C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
// ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?
// 也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
通过上面的程序验证,我们会发现useF函数模板实例化了三份。
包装器可以很好的解决上面的问题
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
下面使用包装器包装成员函数时需要加取地址并且突破类域取到该函数,若取的是类中的静态成员则可以不加取地址只突破类域即可取得该函数,但是最好养成都加取地址的习惯。
取类的成员函数时需要多加一个参数,可以是类对象的指针或是类对象,但是使用类对象会更加的方便,因为使用类对象的时候传参可以使用匿名对象,但是使用类对象的指针时就必须定义一个类对象然后再取它的地址作为参数。
class ADD
{
public:
double addd(double x, double y)
{
return x + y;
}
};
int main()
{
// 传类对象的指针
function<double(ADD*, double, double)> func1 = &ADD::addd;
ADD a;
cout << "func1(&a, 5, 10)-->" << func1(&a, 5, 10) << endl;
// 传对象
function<double(ADD, double, double)> func2 = &ADD::addd;
cout << "func2(ADD(), 5, 10)-->" << func2(ADD(), 5, 10) << endl;
return 0;
}
#include<functional>
using namespace std;
int add(int x, int y)
{
return x + y;
}
struct Add
{
int operator()(int x, int y)
{
return x + y;
}
};
class ADD
{
public:
static int addi(int x, int y)
{
return x + y;
}
double addd(double x, double y)
{
return x + y;
}
};
int main()
{
// 函数名(函数指针)
function<int(int, int)> func1 = add;
cout << func1(5, 10) << endl;
// 函数对象
function<int(int, int)> func2 = Add();
cout << func2(5, 10) << endl;
// lambda表达式
function<int(int, int)> func3 =
[](int x, int y)->int {return x + y; };
cout << func3(5, 10) << endl;
// 类的成员函数(静态函数)
function<int(int, int)> func4 = &ADD::addi;
cout << func4(5, 10) << endl;
// 类的成员函数
function<double(ADD, double, double)> func5 = &ADD::addd;
cout << func5(ADD(),5, 10) << endl;
return 0;
}
有了包装器,如何解决模板的效率低下,实例化多份的问题呢?
#include<functional>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
11.2 bind
bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用bind函数还可以实现参数顺序调整等操作。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将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为第二个参数,以此类推。
#include<functional>
using namespace std;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
struct Add
{
int operator()(int x, int y)
{
return x + y;
}
};
class ADD
{
public:
static int addi(int x, int y)
{
return x + y;
}
double addd(double x, double y)
{
return x + y;
}
};
int main()
{
// 表示绑定函数add 参数分别由调用 func1 的第一,二个参数指定
function<int(int, int)> func1 = bind(add, placeholders::_1 , placeholders::_2);
cout << func1(5, 10) << endl;
// 参数调换顺序
function<int(int, int)> func2 = bind(sub, placeholders::_1, placeholders::_2);
cout << func2(5, 10) << endl;
function<int(int, int)> func3 = bind(sub, placeholders::_2, placeholders::_1);
cout << func3(5, 10) << endl;
// 绑定类中静态函数
function<int(int, int)> func4 = bind(ADD::addi, placeholders::_2, placeholders::_1);
cout << func4(5, 10) << endl;
// 绑定类的成员函数
function<double(double, double)> func5 = bind(&ADD::addd, ADD(),placeholders::_2, placeholders::_1);
cout << func5(5, 10) << endl;
return 0;
}
十二、线程库
12.1 线程
有关于线程的内容,例如线程、锁、条件变量等内容,在Linux中的多线程这篇文章有更详细的讲解,有兴趣的可以去看一下那篇问题。
12.1.1 thread类的简单介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。C++11线程类
12.1.2 thread类中常用函数
12.1.2.1 构造函数
无参构造函数
thread() noexcept;
这个构造函数创建了一个没有关联的线程对象。也就是说,这个thread对象不表示任何正在执行的线程。这种类型的对象通常被称为“joinable”的否定,即它不能被join或detach。
有参构造函数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
这个模板构造函数用于创建一个新的线程,该线程将执行指定的函数fn,该函数可以接受任意数量的参数args。这里Fn和Args是模板参数,分别代表函数和参数的类型。Fn&&和Args&&…表示函数和参数都被完美转发,允许传递左值或右值。参数fn为可执行对象,是线程需要执行的方法,可以为函数指针、仿函数、lambda表达式和包装器。
使用有参构造函数创建线程时,通常使用lambda表达式作为可执行对象,下面这段代码中就使用lambda表达式作为参数创建了两个线程,但是下面这段代码中有由于多线程同时对x进行++操作,所以存在线程安全问题,下面的锁和原子操作就会讲到如何解决这个问题。
移动构造函数
thread (thread&& x) noexcept;
这个构造函数允许将一个thread对象x的资源(即它所代表的线程)移动到新创建的thread对象中。这是通过右值引用thread&& x实现的,表示x是一个将要被移动的对象。移动后,x将不再拥有线程(即它变为非joinable状态),而新创建的thread对象将拥有该线程。
下面演示一下如何让一个线程将资源移动到新线程中,首先创建一个线程th1,然后使用移动构造将另一个线程th2,将th1使用move函数从左值转变为右值作为th2的参数,就可以将th1的资源移动给th2了。
12.1.2.2 移动拷贝
thread& operator= (thread&& rhs) noexcept;
thread 类的移动赋值运算符允许将一个 hread 对象(rhs)的线程所有权转移到另一个thread 对象中。这个操作是“移动”而不是“拷贝”,意味着资源(在这里是线程的执行)从一个对象转移到另一个对象,而不是被复制。
如果是我们创建的线程都是使用无参构造的,而C++线程库中又没有start函数能够让线程启动,那么应该如何让这些线程启动呢?
在下面的代码中,我们使用无参构造在vector中创建了三个线程,我们可以使用移动赋值将一个右值的线程拷贝给这些线程,就可以让这三个线程启动起来 了,移动赋值的参数需要右值的线程,我们可以创建临时对象作为右值,也可以使用move将一个左值线程转换为右值线程。
12.1.2.3 thread::get_id函数 与 this_thread::get_id函数
id get_id() const noexcept;
thread 类中的成员函数get_id用于获取与该线程对象关联的线程的唯一标识符。这个标识符在线程的整个生命周期内是唯一的,并且即使线程已经终止,标识符仍然有效且唯一。
我们可以通过线程对象获取到线程的id,但是进入到了线程的内部,这个方法就行不通了,在线程内部可以使用this_thread::get_id函数来获取到线程的id。
12.1.2.4 joinable函数
bool joinable() const noexcept;
thread 类中的成员函数joinable用于检查线程对象是否关联了一个可加入的(joinable)线程。一个线程对象是 joinable 的,意味着它代表了一个正在执行或尚未终止的线程,并且这个线程还没有被 join(等待完成)或 detach(分离,使其独立执行)。
12.1.2.5 join函数
void join();
thread 类中成员函数join()用于等待与其关联的线程完成其执行。调用 join 的线程(通常是主线程)会被阻塞,直到被 join 的线程执行完毕。
12.1.2.6 detach函数
void detach();
thread 类中成员函数detach设计用来将线程从其关联的thread 对象中分离出来,使其成为一个在后台独立运行的线程。一旦线程被分离,程序就不再拥有对该线程的直接控制权,也无法再与之进行同步(如使用 join)。
12.1.2.7 swap函数
void swap (thread& x) noexcept;
thread 类中成员函数swap用于交换两个thread对象的状态。
上面我们使用了移动构造和移动拷贝来转移线程的资源,实际上还可以使用swap函数来转移线程对象的状态。
12.2 锁
12.2.1 锁的简单介绍
在C++中,mutex(互斥量)是用于多线程编程中的一种同步机制,用于保护共享数据,防止多个线程同时访问同一资源而导致数据竞争或条件竞争。mutex提供了一种简单而有效的方式来确保在同一时间只有一个线程可以访问某个特定的代码段或资源。mutex介绍文档
C++中除了互斥锁以外,还有递归锁(recursive_mutex)、时间锁(timed_mutex)和递归时间锁(recursive_timed_mutex),但是最常用的还是互斥锁,大家如果感兴趣的话,可以去了解一下上述的其他锁。
12.2.2 mutex中常用的接口及应用
12.2.2.1 lock函数
void lock();
当你调用 mutex 对象的 lock() 成员函数时,系统首先检查互斥锁是否当前被任何线程持有。如果锁是空闲的,则当前线程会成功获取锁,并继续执行后续代码。如果锁已经被其他线程持有,则当前线程会被阻塞,直到持有锁的线程释放锁为止。一旦锁被释放,系统会尝试再次获取锁,如果成功,则当前线程继续执行。一旦线程成功获取锁,它就“拥有”了这个锁,直到它显式地调用 unlock() 成员函数释放锁为止。
12.2.2.2 try_lock函数
bool try_lock();
try_lock函数用于尝试获取互斥锁,而不会阻塞调用线程去等待锁的释放。如果锁当前未被其他线程持有,则 try_lock 会成功获取锁并返回 true;如果锁已被其他线程持有,则 try_lock 会立即返回 false,表示未能获取锁。
12.2.2.3 unlock函数
void unlock();
当你调用一个 mutex 对象的 unlock() 成员函数时,如果当前线程持有该 mutex,则 unlock() 会释放这个锁,使得其他被阻塞的、尝试获取该锁的线程可以继续执行。一旦锁被释放,它就不再由当前线程持有。此时,任何其他线程都可以尝试获取这个锁。
注意:只有持有锁的线程才能调用 unlock()。如果未持有锁的线程尝试调用 unlock(),则行为是未定义的(通常是未指定的行为,可能导致程序崩溃或产生不可预测的结果)。
为了避免忘记解锁或异常导致的死锁,通常建议使用RAII技术来管理锁的生命周期。这可以通过使用 std::lock_guard 或 std::unique_lock 等类来实现,它们会在对象销毁时自动调用 unlock()。
12.2.2.4 接口的使用
互斥锁mutex是不支持拷贝的,所以在下面的代码中,我们使用引用的方式传递mutex,但是这样会报错,因为创建线程的参数实际上是先传给构造函数的,又因为底层的原因,构造函数中会创建一个对象,实际上引用接收的并不是mtx,而是mtx的拷贝,所以需要使用ref将mtx包裹起来,这里的x也是同样的道理。这里的底层非常的复杂,我也只是了解到了一点皮毛,不建议大家去了解C++11的底层,大家只要记住,给线程传递左值引用参数需要用ref包裹起来即可。实际上这里还可以通过传递指针的方式解决传递的问题。
下面的代码中,我们使用了ref包裹了mtx和x,然而实际上的代码还存在线程安全的问题。两个线程分别对x进行10000次++操作,但是x的值最后却不是20000,这是因为两个线程可能同时对x进行++,最终导致了数据不一致的问题,所以需要对公共区域进行加锁。
在下面的代码中,我们对线程访问的公共区域进行加锁后,无论两个线程进行多少次++操作,最终的结果都会是正确的。
上面的代码中,当给线程传递左值引用时,需要使用ref包裹,但是如果我们使用lambda表达式就可以完全避免这个问题了,因为lambda表达式不需要传参,而是通过捕获的方式获取到变量。
12.2.3 lock_guard 类
lock_guard 是一个模板类,用于管理互斥锁的锁定和解锁操作。它是C++11标准库 < mutex > 头文件的一部分,并遵循RAII原则,即在构造时获取资源(在这里是锁定互斥锁),在析构时释放资源(在这里是解锁互斥锁)。
当我们给临界区中加锁后,当线程在访问临界区中发生异常,这时线程的锁就得不到释放,会出现死锁的情况,lock_guard 的存在就可以简化互斥锁的使用,并确保锁在不再需要时能够被正确释放,从而避免死锁和资源泄露等问题。
在下面的代码中,我们就让线程在访问共享区域的时候,可能发生异常,多运行几遍发现,线程确实因为异常的缘故出现了死锁的情况。
下面的代码我们使用lock_guard来保护公共区域,并且我们增大调用函数的次数,我们发现虽然线程会因为异常被中断,但是并不是出现死锁的情况。
12.2.4 unique_lock 类
unique_lock 是一个功能更为强大的互斥锁管理器,相较于lock_guard,它提供了更多的灵活性和控制力。unique_lock 同样位于C++11标准库的 < mutex > 头文件中,并且也遵循RAII原则,即在构造时获取资源(在这里是锁定互斥锁),在析构时释放资源(在这里是解锁互斥锁)。
unique_lock 允许在对象构造时不立即锁定互斥锁,而是可以在稍后的某个时间点进行锁定。unique_lock 对象可以被移动(但不可被复制),这意味着锁的所有权可以在不同的 unique_lock 对象之间转移。unique_lock 常与条件变量一起使用,以实现线程间的同步。除了自动管理锁的生命周期外,unique_lock 还允许程序员显式地锁定和解锁互斥锁。
12.3 条件变量
12.3.1 条件变量的简单介绍
condition_variable是一个用于多线程同步的重要类,它通常与锁进行配合使用,来实现线程间基于条件的等待与通知机制。condition_variable介绍文档
12.3.2 condition_variable的中常用的接口
12.3.2.1 wait函数
void wait (unique_lock<mutex>& lck);
condition_variable中的wait函数主要用于让线程等待特定条件的满足,当线程调用wait函数时,它会自动释放当前线程已经获取到的与之关联的互斥锁(通常是mutex通过unique_lock管理的锁)。
大家可以思考一下,为什么这里要用unique_lock来管理锁,而不用lock_guard来管理锁呢?
因为lock_guard只支持析构的时候释放锁,而unique_lock支持手动释放锁,当线程调用wait时,它需要释放锁让其他线程申请锁资源。
12.3.2.2 notify_one函数
void notify_one() noexcept;
condition_variable的notify_one函数起着唤醒等待线程的重要作用,当有多个线程通过condition_variable的wait在等待某个条件满足时,notify_one函数会选择其中一个线程进行唤醒。
12.4 综合应用(支持两个线程交替打印,一个打印奇数,一个打印偶数)
#include <iostream>
#include <vector>
#include <string>
#include <time.h>
using namespace std;
#include <thread>
#include <mutex>
int main()
{
mutex mtx;
int x = 0;
condition_variable cv;
bool flag = false;
thread th1([&]() {
for (int i = 0; i < 10; i++)
{
unique_lock<mutex> lock(mtx);
if (flag)
cv.wait(lock);
flag = true;
cout << this_thread::get_id() << " : " << x++ << endl;
cv.notify_one();
}
});
thread th2([&]() {
for (int i = 0; i < 10; i++)
{
unique_lock<mutex> lock(mtx);
if (!flag)
cv.wait(lock);
flag = false;
cout << this_thread::get_id() << " : " << x++ << endl;
cv.notify_one();
}
});
th1.join();
th2.join();
return 0;
}
上面的代码保证了th1先运行,并且th1和th2交替运行,下面我将分情况详细的讲解一下。
情况1:线程th1先运行,线程th2待定
线程th1先运行,则线程th1申请锁成功,又flag为false,则不进行wait,将flag变为为true,进行打印,唤醒在当前条件变量下的等待的线程,若没有则什么都不做,最后出作用域再解锁。
在th1访问临界区时,th2有两种情况:
- 线程th2没有启动或没有分到时间片
- 线程th2启动运行了,但是没有申请到锁资源,阻塞在锁上等待
再根据th2的两种情况,分析th1情况
- 在线程th2的第一种情况下,线程th1又分到了时间片,再次申请到锁,但是由于此时flag为true,所以线程th1会在条件变量下进行等待
- 在线程th2的第二种情况下,线程th1访问完临界区后,会先唤醒th2,再释放锁,这时th2就可以竞争锁了。
再根据th1的两种情况,分析th2情况
- 在线程th1的第一种情况下,线程th2总会分到时间片,此时th2申请锁成功,flag为true,th2不会进行wait,进行打印,唤醒在当前条件变量下的等待的线程,若没有则什么都不做,最后出作用域再解锁。
- 在线程th1的第二种情况下,假设th2申请到锁了,此时flag为true,线程th2不会进行wait,再将flag改为false,进行打印,唤醒在当前条件变量下的等待的线程,若没有则什么都不做,最后出作用域再解锁。
后序就进行交替打印了。
情况2:线程th2先运行,线程th1待定
线程th2先运行,则先获取到锁,此时flag为false,所以线程th2需要在条件变量下进行等待。
在线程th2申请锁成功到th2还未进行wait的区间中,线程th1可以分为两种情况。
- 线程th1没有启动或是没有分到时间片
- 线程th1启动运行了,但是没有申请到锁资源,阻塞在锁上等待
在线程th2进行等待后,继续分析线程th1
- 在上面的第一种情况下,线程th1总会分到时间片,此时th1会申请到锁资源,flag为false,th1不会进行wait,将flag变为为true,进行打印,唤醒在当前条件变量下的等待的线程,若没有则什么都不做,最后出作用域再解锁。
- 在上面的第二种情况下,线程th2进行在条件变量下进行等待时,会释放锁资源,th1就可以竞争锁资源了,假设th1申请到了锁资源,flag为false,th1不会进行wait,将flag变为为true,进行打印,唤醒在当前条件变量下的等待的线程,若没有则什么都不做,最后出作用域再解锁。
由于线程th2一直在条件变量下进行等待,需要th1对th2进行唤醒,唤醒以后,后序就进行交替打印了。
12.5 原子操作
12.5.1 atomic类 的简单介绍
atomic 类型提供了一种线程安全的操作方式,以避免在多线程环境下使用共享数据时发生数据竞争。atomic 类型和函数定义在头文件 < atomic > 中。atomic 模板类允许你创建原子类型的变量,这些变量可以确保在多线程环境中的读写操作是原子的,即不可被中断的。atomic介绍文档
atomic 的底层实现依赖于CAS操作,这是大佬们关于CAS的一些文章,有兴趣的可以去学习一下。
无锁队列的实现
无锁HashMap的原理与实现
12.5.2 atomic类 的简单使用
我们之前说过,多个线程同时对共享资源进行操作,可能会导致数据不一致,我们可以在共享区加锁以保护共享区,使同一个共享资源在同一时间内只能有一个线程进行操作。
互斥锁确实能解决多个线程同时对共享资源进行操作导致数据不一致的问题,但是对于这种共享区中只有++、- -、true改为false的这种操作,会占用很多CPU资源,所以这种内置类型的简单操作使用atomic会更加的高效。
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹