C++进阶

move移动语义、forward完美类型转发以及引用折叠

移动语义(Move Semantics)

移动语义允许资源(如动态分配的内存、文件句柄等)的“移动”,而不是复制。这通常用于优化性能,特别是在涉及到临时对象时。移动语义通过引入右值引用(使用 && 声明的引用)来实现。

  • 右值引用:绑定到右值的引用(一般指销毁或不再需要的对象)。
  • 移动构造函数和移动赋值运算符:这些特殊成员函数接受一个右值引用参数,允许资源从源对象“移动”到目标对象。移动构造是相对于拷贝构造的,同样的一个对象,将其作为左值来构造一个新对象时,调用的是拷贝构造函数,将其作为右值调用构造时,会调用移动构造函数。
  • move():使用move(val)可以将val转换成右值,从而调用移动构造函数/移动赋值运算符

一个例子:

int main()
{string str1("aaaaa");string str2(str1);cout << "拷贝构造后:" << str1 << endl;
string str3(move(str1));
cout &lt;&lt; &quot;移动构造后:&quot; &lt;&lt; str1 &lt;&lt; endl;return 0;

}

输出结果

拷贝构造后:aaaaa
移动构造后:

可以看到,移动语义会将资源移动而不是拷贝,调用移动构造函数还是拷贝构造函数取决于参数是否是右值。除了使用move()函数手动将参数转变为右值外,使用临时对象来构造另一个对象也可能会调用移动构造函数(之所以说是可能,是因为现代编译器的优化,很多时候如果不使用move强转为右值,可能不会调用移动构造函数,至于会优化成什么情况,要视具体情况而定)

一个完整的实现了移动构造和移动赋值的string类实现: String.h

个人对移动语义的理解:

所谓的右值引用与移动语义,实质上是一种规定或者说指导思想,即C++规定了使用右值作为参数时,调用的函数就会转移这个右值的所有权。C++本身只是提供了一种调用关系,用左值时调用拷贝构造函数,用右值时调用移动构造函数,至于函数具体的实现,由自己决定,并不是强制要实现成移动语义,但是正常来说,都会遵循移动语义的思想去实现传入右值参数的函数。因此,读代码时,看到使用了右值,就知道是移动语义

可以认为,右值引用就是提供了一种函数重载的参数类别,让同一个对象可以有两种参数表现(左值和右值) ,从而调用不同的重载函数。

引用折叠

如下是一个**std::forward和引用折叠的**例子,它将本来需要分别处理左值引用和右值引用的两个函数,合并为一个函数

/*
void push_back(const T& val)
{if (full())expand();_allocator.construct(_last, val);++_last;
}

void push_back(T&& val) // 右值push
{
if (full())
expand();
_allocator.construct(_last, std::move(val));
++_last;
}
*/

template<typename Ty>
void push_back(Ty&& val)
{
if (full())
expand();
_allocator.construct(_last, std::forward<Ty>(val));
_last++;
}

当调用push_back函数时,根据传入的val类型,模版函数实例化,Ty会变成相应的类型,std::forward则会将该类型完整的传入到调用的函数中。

当传入的val类型是String&时,Ty的类型变为String&,形参的类型变成String& &&,引用折叠,变成String&,若是String&& &&,就会折叠成String&&,

完整的vector类实现:vector.h

完美类型转发(Perfect Forwarding)

完美转发是指在模板函数中,能够将参数类型完全不变地转发给另一个函数,包括参数的类型、值类别(左值、右值)和const/volatile修饰符。这通常通过函数模板的实参推演和 std::forward 实现。

  • 模板参数:使用模板参数接收推演出的实际接受的参数类型。
  • std::forward:一个模板函数,它可以将模板参数的类型完整的转发到其他地方。

对象构造/使用过程

下面这个例子展示了不同的对象构造/使用过程,其中因为编译器优化,很多情况下对象的构造/使用过程与我们想象的不一样


class Test {
public:Test(int a=10):a(a){cout << "Test()" << endl;}Test(Test& t) {a = t.a;cout << "Test(Test&)" << endl;}Test(const Test& t)  // const Test&形参可以绑定Test和const Test参数,而Test&形参只能绑定Test参数,临时对象是右值,只能被Test&&形参或是const Test&形参绑定,因此必须要有一个const形参版本拷贝构造{a = t.a;cout << "Test(const Test&)" << endl;}Test(Test&& t)  {a = t.a;cout << "Test(Test&&)" << endl;}Test& operator=(const Test& t) {a = t.a;cout << "operator=(Test&)" << endl;return *this;}
~Test()
{cout &lt;&lt; &quot;~Test()&quot; &lt;&lt; endl;
}

private:
int a;
};

int main()
{
cout << "1." << endl;
Test t1; // 普通构造
cout << "2." << endl;
Test t2(t1); // 拷贝构造
cout << "3." << endl;
Test t3 = t2; // 创建t3的同时使用t2初始化t3,等同于拷贝构造Test t3(t2)
cout << "4." << endl;
Test t4 = Test(20); // 创建t4并使用临时对象初始化,编译器进行优化,相当于Test t4(20)

cout &lt;&lt; &quot;--------------------&quot; &lt;&lt; endl;cout &lt;&lt; &quot;5.&quot; &lt;&lt; endl;
t4 = t2;  // t4 t2都已存在,因此调用赋值运算符
// 显式生成临时对象
cout &lt;&lt; &quot;6.&quot; &lt;&lt; endl;
t4 = Test(30);  // t4已经存在,因此构造临时对象后,使用赋值运算符
cout &lt;&lt; &quot;7.&quot; &lt;&lt; endl;
t4 = (Test)30;  // (Test)30相当于Test(30)
//隐式生成临时对象-&gt;隐式实例化
cout &lt;&lt; &quot;8.&quot; &lt;&lt; endl;
t4 = 30; // t4是Test对象,因此先Test(30)构造临时对象,在赋值给t4cout &lt;&lt; &quot;--------------------&quot; &lt;&lt; endl;
//Test* p = &amp;Test(40);  指针不能指向临时对象(右值),因为临时对象在语句结束后就析构,较新的编译器会直接报错
//const Test* p = &amp;Test();  常量指针也不能指向临时对象//Test&amp; r = Test();  左值引用不能引用临时对象
const Test&amp; ref = Test(50);  // 常左值引用变量可以引用临时对象,此时临时对象就不再是临时的了,该引用变量会绑定到临时对象cout &lt;&lt; &quot;--------------------&quot; &lt;&lt; endl;
return 0;

}

输出结果

1.
Test()
2.
Test(Test&)
3.
Test(Test&)
4.
Test()
--------------------
5.
operator=(Test&)
6.
Test()
operator=(Test&)
~Test()
7.
Test()
operator=(Test&)
~Test()
8.
Test()
operator=(Test&)
~Test()
--------------------
Test()
--------------------
~Test()
~Test()
~Test()
~Test()
~Test()

编译器优化

在C++中,编译器在编译时期和链接时期会进行一系列的优化,这些优化可以减少不必要的对象拷贝,提高程序的运行效率。

  1. 编译时期(类型检查和重载决议):
    • 在编译时期,编译器会检查所有的构造函数和重载函数,以确定是否存在一个合适的构造函数来初始化对象或处理函数调用。
    • 如果没有合适的构造函数,编译器会报错,因为这意味着没有合法的方式来构造或赋值对象。
    • 在这个阶段,编译器会根据参数的类型(左值或右值)和const修饰符来选择合适的构造函数或赋值运算符。
  2. 链接时期(代码生成和优化):
    • 在链接时期,编译器会生成机器代码,并进行一系列的优化,包括返回值优化(RVO)、命名返回值优化(NRVO)和移动构造函数的使用。
    • 返回值优化(RVO)和命名返回值优化(NRVO)可以消除临时对象的创建和销毁,直接在目标位置构造对象,从而省略了构造函数的调用。
    • 如果类定义了移动构造函数,编译器可能会使用移动构造函数来处理临时对象,因为移动构造函数可以避免不必要的拷贝,提高效率。
  • **返回值优化(**​Return Value Optimization, RVO):  当一个对象作为函数的返回值时,编译器可能会直接在返回值的内存位置构造对象,而不是先构造一个临时对象然后再拷贝。
  • **命名返回值优化(**​Named Return Value Optimization, NRVO):  类似于RVO,但是适用于命名的返回值变量。
  • 移动构造函数:  如果对象是临时的,并且类有移动构造函数,编译器可能会使用移动构造函数而不是拷贝构造函数。

对象作为参数传入函数以及作为返回值从函数返回的过程

image.png

调用getObject(t1)时,首先根据传入的参数,调用构造函数初始化形参也就是动作3;
执行完getObject()函数体内语句,返回tmp对象时,调用拷贝构造函数,即动作5(这里调用了移动构造函数)。动作5构造的临时对象才是最终的返回值。程序的运行结果如下:

Test()  // 1
Test()  // 2
Test(Test&)  // 3
Test()  //4 
Test(Test&&)  // 5
~Test()  // 6
~Test()  // 7
operator=(Test&)  // 8
~Test()  // 9
--------------
~Test()  // 10
~Test()  // 11

但是在现代C++中,编译器会进行返回值优化,从而优化掉动作5,没有了动作5,也就省去了一次析构的调用。从而节省了计算资源的花费。也就是说现在的流程是

image.png

以下是开启了编译器返回值优化的输出结果

Test()  // 1
Test()  // 2
Test(Test&)  // 3
Test()  // 4
~Test()  //5
operator=(Test&)  // 6
~Test()  // 7
--------------
~Test()  // 8
~Test()  // 9

在VS2022中,编译器返回值优化是默认开启的,可以通过命令行手动关闭,具体参考https://learn.microsoft.com/zh-cn/cpp/build/reference/zc-nrvo?view=msvc-170。如下图:

image.png

总结三条对象优化的原则

  1. 函数参数传递过程中,对象优先按引用传递,不要按值传递
  2. 函数返回对象的时候,应该优先返回一个临时对象,而不是先构造出要返回的对象再将其返回
  3. 接受函数的返回值时,优先按初始化的方式接收,而不是按赋值的方式接收

智能指针

智能指针的介绍

智能指针的作用:资源的自动释放

我们首先来创建一个自己的智能指针类,该类比较简单,但是实现了资源的自动释放

template<typename T>
class CSmartPtr
{
public:CSmartPtr(T* ptr = nullptr) :mptr(ptr) { cout << "CSmartPtr()" << endl; }~CSmartPtr() { delete mptr; cout << "~CSmartPtr()" << endl;}CSmartPtr(CSmartPtr& c_p) {mptr = c_p.mptr;}CSmartPtr& operator=(CSmartPtr& c_p){mptr = c_p.mptr;}
T&amp; operator*() { return *mptr; }
T* operator-&gt;() { return mptr; }

private:
T* mptr;
};

接下来思考一下,如果我们让多个CSmartPtr指针指向同一个对象,会出现什么问题。很显然,第一个CSmartPtr对象析构时,就释放了所指向的对象资源,剩下的CSmartPtr析构时由于要释放的对象不存在,就会出错。

那么要如何解决这样的问题呢?C++中有两种指针,分别解决了这个问题。

  1. 不带引用计数的智能指针,它们不允许多个指针指向同一对象
  2. 带引用计数的智能指针,它们允许多个指针指向同一对象,通过引用计数的方式记录有多少个智能指针指向了对象,直到引用计数为0,才释放对象资源。

不带引用计数的智能指针

  • 头文件
  • auto_ptr
  • scoped_ptr(C++11新标准) (弃用)
  • unique_ptr(C++11新标准) (推荐使用)
  1. auto_ptr

    • 当有多个auto_ptr指向同一个对象时,只会让最后一个指针指向该对象,其余指针都置为空
  2. scoped_ptr(弃用)

    scoped_ptr(const scoped_ptr<T>&) = delete;
    scoped_ptr<T>& operator=(const scoped_ptr<T>&)= delete;
    
    • 上面两行是C++11增加的新语法,意思是删除了这两个函数,如果被调用就直接报错
    • 也就是说scoped_ptr只允许用于一个对象,如果要将其赋值给其他scoped_ptr指针或是用该指针构造其他的指针,就会直接报错。
    • scoped_ptr已经被弃用
scoped_ptr(const scoped_ptr<T>&) = delete;
scoped_ptr<T>& operator=(const scoped_ptr<T>&)= delete;
  1. unique_ptr(推荐使用)

    unique_ptr(const unique ptr<T>&) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>&))=delete;
    
    • unique_ptr同样删除了左值引用拷贝函数和左值引用赋值运算符
    • 但是unique_ptr提供了右值引用的移动语义构造函数和移动语义赋值运算符

所以对于以上三种指针,使用的情况分别如下:

auto_ptr<int> p1(new int);
auto_ptr<int> p2(p1);

*p2 = 20;
cout << *p1 << endl; // 报错,因为p1已经指向nullptr

scoped_ptr<int> p1(new int);
scoped_ptr<int> p2(p1);  // 报错,因为unique_ptr没有拷贝构造和赋值运算符
unique_ptr<int> p1(new int);
unique_ptr<int> p2(std::move(p1));  // 正确,unique_ptr有移动构造函数和移动赋值运算符
unique_ptr<int> p3(p2);  // 错误,unique_ptr没有拷贝构造和赋值运算符
*p2 = 20;
cout << *p1 << endl;  // 错误,p1指向nullptr
cout << *p2 << endl;

也就是说对于这三种指针,都不能实现让多个指针指向同一个对象的目的,如果尝试这么做:

  • auto_ptr可以将同类指针作为左值来构造新的指针,但是只有最后指向该对象的指针有效,其余指针都指向nullptr
  • scoped_ptr完全不允许创建新的指向该对象的指针,如果这么做,将会直接报错
  • unique_ptr允许将同类指针作为右值来构造新的指针,但是只有最后指向该对象的指针有效,其余指针都指向nullptr。如果使用同类指针作为左值来构造,就会报错。

带引用计数的智能指针

带引用计数的智能指针给每一个对象资源,匹配一个引用计数,每有一个指针使用资源,引用计数+1,每有一个指针不再使用资源,引用计数-1,直到引用计数为0时,释放资源。

C++提供了两个带引用计数的智能指针

  • shared_ptr : 强智能指针
  • weak_ptr:弱智能指针

强智能指针:可以改变资源的引用计数

弱智能指针:不会改变资源的引用计数

弱智能指针的意义是解决强智能指针循环引用(交叉引用)会导致资源无法释放的问题,解决方法是:

  • 定义对象的时候,用强智能指针
  • 引用对象的地方,用弱智能指针。

需要注意的是,weak_ptr不能用来访问对象,通过weak_ptr访问对象时,必须先将weak_ptr转换为shared_ptr。

具体示例如下所示:

class B;  // 先声明B,才能在A中使用
class A {
public:A(){ cout << "A()" << endl; }~A(){ cout << "~A()" << endl; }shared_ptr<B> _ptrb;
};

class B {
public:
B() { cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
shared_ptr<A> _ptra;
};

int main()
{
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;

return 0;

}

输出如下所示,可以看到A和B并没有被释放

A()
B()
2
2

原因是:pa和pb在栈上,main函数运行完毕后,会自动调用pa和pb的析构函数,pa和pb会将引用计数-1。而在堆上开辟的A对象中还有一个指针_ptrb指向B,此时A对象的引用计数是2-1=1,所以A对象未被释放,这就导致B对象的引用计数是2-1=1,也无法释放,这就导致这两个对象的资源都无法释放。

解决方法就是在引用对象时使用弱智能指针,更改后的代码示例如下:

class B;  // 先声明B,才能在A中使用
class A {
public:A(){ cout << "A()" << endl; }~A(){ cout << "~A()" << endl; }weak_ptr<B> _ptrb;
};

class B {
public:
B() { cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
weak_ptr<A> _ptra;
};

int main()
{
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;

return 0;

}

输出如下

A()
B()
1
1
~B()
~A()

可以看到,对象资源被正确释放了。

智能指针删除器

智能指针(如std::unique_ptrstd::shared_ptr)通常包含一个删除器(deleter),这是一个函数对象,它在智能指针被销毁时用来释放其管理的资源。

image.png

智能指针可以使用自定义的删除器,这个删除器会在智能指针被销毁时调用,自定义删除器可以是函数对象或lambda表达式(实质是一个函数对象)。例如:

template<typename T>
class MyDeleter {
public:void operator()(T* ptr)const {cout << "call MyDeleator.operator()" << endl;delete[] ptr;}
};

int main()
{
unique_ptr<int, MyDeleter<int>> p(new int);

unique_ptr&lt;int, function&lt;void(int*)&gt;&gt; ptr1(new int[100],[](int* p)-&gt;void {cout &lt;&lt; &quot;call lambda release int[100]&quot; &lt;&lt; endl;delete[] p;}
);return 0;

}

绑定器、Lambda表达式和function类模板

STL的绑定器

STL的绑定器介绍

绑定器是用于函数对象的函数模版,可以将函数对象中的形参变量绑定为一个确定的值

  • bindlst:operator() 的第一个形参变量绑定一个确定的值
  • bind2nd:operator() 的第二个形参变量绑定一个确定的值

如下示例展示了绑定器的使用

int main()
{vector<int> vec;for (int i = 0; i < 20; i++){vec.push_back(rand() % 100);}
sort(vec.begin(), vec.end(), greater&lt;int&gt;());for (auto e : vec)
{cout &lt;&lt; e &lt;&lt; ' ';
}
cout &lt;&lt; endl;auto it1 = find_if(vec.begin(), vec.end(), bind1st(greater&lt;int&gt;(), 70));  // 返回第一个70&gt;it情况的it
if (it1 != vec.end())vec.insert(it1, 70);for (auto e : vec)
{cout &lt;&lt; e &lt;&lt; ' ';
}
cout &lt;&lt; endl;return 0;

}

  • greater 函数对象,接收两个参数,return left>right
  • less 函数对象,接收两个参数,return left<right

STL的绑定器的实现原理

如下,实现了find_if和bind1st,巧妙之处在于使用了函数模版的实参推演,获取传入到my_bind1st中的参数类型,并用得到的参数类型和参数本身构造一个_my_bind1st函数对象并初始化。

template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator second, Compare compare)
{for (; first != second; ++first){if (*compare(*first)) {return first;}}return second;
}

template<typename Comp, typename T>
class _my_bind1st
{
public:
_my_bind1st(Comp comp, T arg) : _comp(comp), left(arg){}
bool operator()(const T& right) {
return _comp(left, right);
}
private:
Comp _comp;
T left;
};

template<typename Comp, typename T>
_my_bind1st<Comp, T> my_bind1st(Comp comp, T arg)
{
return _my_bind1st<Comp, T>(comp, arg);
}

测试代码

int main()
{vector<int> vec;for (int i = 0; i < 20; i++){vec.push_back(rand() % 100);}
sort(vec.begin(), vec.end(), greater&lt;int&gt;());for (auto e : vec)
{cout &lt;&lt; e &lt;&lt; ' ';
}
cout &lt;&lt; endl;auto it1 = find_if(vec.begin(), vec.end(), my_bind1st(greater&lt;int&gt;(), 70));  // 返回第一个it&lt;70情况的it
if (it1 != vec.end())vec.insert(it1, 70);for (auto e : vec)
{cout &lt;&lt; e &lt;&lt; ' ';
}
cout &lt;&lt; endl;return 0;

}

试想一下,如果没有使用函数模版进行实参推演,会怎么样?

显然,如果没有使用函数模版,我们就需要先获取比较器和要比较的参数类型,使用获得的类型显式的进行函数对象类模版的实例化,然后将该函数对象作为参数传入find_if使用,在C++11推出decltype之前,这几乎无法做到,以下是利用C++11中推出decltype达到的和模版实参推演相同的效果。

_my_bind1st<decltype(greater<int>()), decltype(70)> mybind1st(greater<int>(), 70);
auto it1 = find_if(vec.begin(), vec.end(), mybind1st);

C++11 bind绑定器

C++11 中引入的 std::bind 是一个函数适配器,在 中,它允许你绑定一个可调用对象(比如函数、lambda 表达式、函数对象或成员函数指针)的某些参数,然后返回一个新的可调用对象。例如:

int sum(int a, int b, int c) {return b;
}

int main()
{
auto func1 = bind(sum, 1, placeholders::_1, placeholders::_2);
cout << func1(2, 3) << endl;

return 0;

}

输出

2

由上面的例子,我们可以知道

  • bind()用于普通函数时,第一个参数是函数指针,其余的参数按顺序对应于绑定的函数的参数。
  • placeholders::_1是占位符,完整的名称是std::placeholders::_1,总共有20个占位符,从_1到_20
  • 占位符只能从placeholders::_1开始使用,且必须连续(_1, _2, …) ,根据占位符所在的位置,而不是占位符名称,匹配函数相应位置的形参

bind有两个重载版本,一个是普通函数的版本,如上所示,还有一个是成员函数的版本,如下所示

class Test {
public:int add(int a, int b) {return a + b;}
};

int main()
{
using namespace placeholders;
auto func2 = bind(&Test::add, Test(), _1, 30);
cout << func2(10) << endl;
return 0;
}

当bind使用成员函数时,要在第一个参数传入成员函数指针(必须有&符号),第二个参数传入拥有该成员函数的对象,之后则是成员函数的参数。

利用bind实现简易线程池

bind在这里的作用是: C++的多线程thread不能接受成员函数,只能接收普通函数,因此如果线程函数是成员函数,可以使用绑定器,将成员函数绑定到函数对象,就可以当成普通函数使用

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

using namespace std;

// 线程类,实质就是提供了保存线程函数指针,和启动线程的方法。原因是C++提供的线程类,是创建就启动,这个线程类的目的为了将其分为两步
class Thread
{
public:
Thread(function<void()> func) : _func(func){} // 使用线程函数构造对象
thread start()
{
thread t(_func); // 使用线程函数创建一个线程对象并启动
return t; // 返回线程句柄
}

private:
function<void()> _func;
};

// 线程池类
class ThreadPool
{
public:
ThreadPool(){}
~ThreadPool()
{
for (int i = 0; i < _pool.size(); ++i)
{
delete _pool[i];
}
}
// 开启线程池
void startPool(int size)
{
for (int i = 0; i < size; ++i)
{// C++的多线程thread不能接受成员函数,只能接收普通函数,因此使用绑定器,将成员函数绑定到函数对象,就可以当成普通函数使用
_pool.push_back(new Thread(bind(&ThreadPool::runInThread, this, i)));
}
for (int i = 0; i < size; ++i)
{
_handler.push_back(_pool[i]->start()); // 将启动线程,将线程句柄存入_handler容器
cout <<"thread "<<i<<" id: " << _handler[i].get_id() << endl;

	}for (thread&amp; t : _handler){t.join();  // 主线程等待子线程执行结束}
}

private:
vector<Thread*> _pool;
vector<thread> _handler; // 存放线程句柄
// 线程函数,将该成员函数作为线程函数
void runInThread(int sn)
{
cout << "call runInThread sn :" << sn << endl;
}
};

int main()
{
ThreadPool pool;
pool.startPool(10);

return 0;

}

整体流程:

  • 使用绑定器将成员函数转变为普通函数(因为thread不接受成员函数)
  • 将普通函数作为参数构造自定义的Thread对象
  • 将构造好的Thread对象指针存入_pool容器
  • 调用_pool容器中的Thread对象的start方法启动线程,产生线程句柄,并将线程句柄存入_handler容器
  • 子线程调用join(),主线程会等待子线程结束
  • 程序运行完毕,析构对象

Lambda表达式

lambda表达式的介绍

在C++中,lambda表达式是一种创建匿名函数对象的语法。它允许你定义一个函数而不需要给它一个名字。Lambda表达式在编译时会被转换为一个函数对象,这个函数对象是一个编译器生成的类的实例,该类重载了operator(),使得这个对象可以被调用。

Lambda表达式可以被函数指针参数接收,这一点是函数对象的共性特征。

Lambda一般形式

auto lambda = []()-> void {	};
  • []:捕获外部变量
  • () :形参列表
  • void :返回值类型,不一定是void,这里只是为了方便演示
  • {} :函数体

[] 捕获外部变量的规则

  • [] :表示不捕获任何外部变量
  • [=] :以传值的方式捕获外部的所有变量
  • [&] :以引用的方式捕获外部的所有变量
  • [this] :捕获外部的this指针
  • [=, &a] :以传值的方式捕获所有变量,但是a变量以传引用的方式捕获
  • [a, b] :以传值的方式捕获外部变量a和b
  • [a, &b] :以传值的方式捕获a,以传引用的方式捕获b

lambda的实现原理是使用lambda表达式中的内容创建了一个匿名函数对象。查看如下示例,使用TestLambda创建的fun1和使用lambda表达式创建的fun2效果是相同的。

class TestLambda {
public:int operator()(int a, int b)const{return a + b;}
};

int main()
{
int a = 10;
int b = 20;
TestLambda fun1;
auto fun2 = [](int a, int b)->int {
return a + b;
};
int c1 = fun1(a, b);
int c2 = fun2(a, b);

cout &lt;&lt; &quot;c1: &quot; &lt;&lt; c1 &lt;&lt; endl;
cout &lt;&lt; &quot;c2: &quot; &lt;&lt; c2 &lt;&lt; endl;
return 0;

}

lambda的一个典型实用场景,在泛型算法中,直接在需要的时候在参数中写一个lambda表达式,省去了构造一个函数对象的繁琐过程

sort(vec.begin(), vec.end(), [](int a, int b)->bool {return a > b;}
);

for_each(vec.begin(), vec.end(),
[](int e)->void{
cout << e << ' ';
}
);
cout << endl;

该用什么类型来表示或者是接收lambda表达式?

头文件<functional> 中提供的function类型来表示,查看如下示例

map<int, function<int(int a, int b)>> calculateMap;
calculateMap[1] = [](int a, int b)->int {return a + b; };
calculateMap[2] = [](int a, int b)->int {return a - b; };
calculateMap[3] = [](int a, int b)->int {return a * b; };
calculateMap[4] = [](int a, int b)->int {return a / b; };

cout << "10 + 15: " << calculateMap[1](10, 15) << endl;

map对象创建时就需要知道key和value的类型,此时必须要给一个能够接收lambda表达式对象的类型,头文件<functional> 中定义了function类模板,可以用来接收lambda表达式对象。

function<R(Arg1,Arg2)>

  • R表示lambda表达式的返回值类型
  • Arg表示lambda表达式参数的类型,Arg可以有多个,且可以是不同类型,根据lambda表达式实际的参数数量和类型而定。
  • 即使lambda表达式没有参数,括号也要保留,即function<a()>

下面两个使用示例,有助于增加堆lambda表达式的理解

unique_ptr<FILE, function<void(FILE*)>> ptr1(fopen("data.txt", "w"),[](FILE* f)->void {fclose(f);});  // 模版参数1:对象类型,模版参数2:对象的删除器类型
class Data {
public:Data(int a = 10, int b = 10, int c = 10):ma(a), mb(b),mc(c){}int ma;int mb;int mc;
};

int main()
{
using FUNC = function<bool(Data&, Data&)>;
priority_queue<Data, vector<Data>, FUNC> maxHeap(
[](Data& l, Data& r)->bool {
return l.ma > r.ma;
} // priority_queue<数据类型,容器类型,排序函数对象类型>
);
maxHeap.push(Data(10, 20, 30));
maxHeap.push(Data(15, 15, 15));
maxHeap.push(Data(20, 30, 100));

return 0;

}

模版的显式特例化和实参推演

模版的完全显式特例化和非完全(部分)显式特例化

模版的显示特例化作用是让类模版在可以特定情况下按照特定模版生成类,当使用类模版创建对象时,按照传入的类模版参数类型,优先寻找最匹配的显式特例化模版。如下是一个类的模版显示特例化的例子,函数的模板显式特例化也是同样的语法

// 类模版,没有任何匹配的显式特例化模版时,使用此模版
template<typename T>
class Vector
{
public:Vector() { cout << "call Vector template init" << endl; }
};

// 完全特例化,类模版参数为char类型时优先使用此模版,编译器需要 template<> 来识别这是一个模版特化
template<>
class Vector<char
>
{
public:
Vector() { cout << "call Vector<char*> template init" << endl; }
};

// 部分特例化,类模版参数为指针类型时优先使用此模版
template<typename Ty>
class Vector<Ty>
{
public:
Vector() { cout << "call Vector<Ty
> template init" << endl; }
};

// 部分特例化,类模版参数为函数指针类型时优先使用此模版
template<typename R, typename A1, typename A2>
class Vector<R()(A1, A2)>
{
public:
Vector() { cout << "call Vector<R(
)(A1, A2)> template init" << endl; }
};

// 部分特例化,类模版参数为函数类型时优先使用此模版
template<typename R, typename A1, typename A2>
class Vector<R(A1, A2)>
{
public:
Vector() { cout << "call Vector<R(A1, A2)> template init" << endl; }
};

int main()
{
Vector<int> v1;
Vector<char> v2;
Vector<int
> v3;
Vector<int(*)(int, int)> v4;
Vector<int(int, int)> v5
return 0;
}

输出结果

call Vector template init
call Vector<char*> template init
call Vector<Ty*> template init
call Vector<R(*)(A1, A2)> template init
call Vector<R(A1, A2)> template init
  • 编译器需要 template<> 来识别这是一个特化,而不是一个普通的类定义。没有 template<>,编译器无法将这个定义与模板关联起来。

函数模版的实参推演

函数模版可以根据传入的参数,推演出模版参数的类型。如下是函数模版的实参推演的一些示例

template<typename T>
void func1(T a)
{cout << typeid(T).name() << endl;
}

template<typename R, typename A1, typename A2>
void func2(R(*a)(A1, A2))
{
cout << typeid(R).name() << endl;
cout << typeid(A1).name() << endl;
cout << typeid(A2).name() << endl;
}

template<typename R, typename T, typename A1, typename A2>
void func3(R(T::*a)(A1, A2))
{
cout << typeid(R).name() << endl;
cout << typeid(T).name() << endl;
cout << typeid(A1).name() << endl;
cout << typeid(A2).name() << endl;
}

class Test {
public:
int sum(int a, int b) { return a + b; }
};

int sum(int a, int b) { return a + b; }

int main()
{
func1(10);
func1("aaa");
func1(sum);
cout << "--------------------" << endl;
func2(sum);
cout << "--------------------" << endl;
func3(&Test::sum);

return 0;

}

int
char const *
int (__cdecl*)(int,int)
--------------------
int
int
int
--------------------
int
class Test
int
int

function类模板

std::function是一个类模板,它被定义在头文件<functional>中。std::function旨在提供一种通用的方式来存储、管理和调用可调用对象(callable objects) ,这些对象可以是普通函数、lambda表达式、函数对象、成员函数指针或成员数据指针。

function的实现原理

利用类模板的显式特例化,和变长模板参数,可以实现与function几乎相同的类模板,说是几乎,是因为我们自己实现的类模板只能以接收参数的形式接收lambda表达式,而不能用赋值方式来接收lambda表达式,不过对于一般函数,效果是完全相同的。具体情况,可以看下面的示例:

template<typename T>
class MyFunction{};

template<typename R, typename... _Ty>
class MyFunction<R(_Ty...)>
{
public:
using F = R(*)(_Ty...);
MyFunction(F f) : fun(f) {}
R operator()(_Ty... args) {
cout << "MyFunction<R(_Ty...)>" << endl;
return fun(args...);
}

private:
F fun;
};

int sum(int a, int b)
{
return a + b;
}

int main()
{
function<int(int, int)> func1 = [](int a, int b)->int {return a + b; };
function<void(string)> func2 = [](string str)->void {cout << str << endl; };
func1(3, 4);
func2("Hello World!");

MyFunction&lt;int(int, int)&gt; func3([](int a, int b)-&gt;int {return a + b; });
MyFunction&lt;void(string)&gt; func4([](string str)-&gt;void {cout &lt;&lt; str &lt;&lt; endl; });
MyFunction&lt;int(int, int)&gt; func5 = sum;func3(3, 4);
func4(&quot;Hello World!&quot;);
func5(1, 2);return 0;

}

function、lambda表达式以及变长模板参数是C++11标准中引入的功能,它们为模板编程提供了更大的灵活性和表达力。

function、lambda表达式以及变长模板参数是C++11标准中引入的功能,它们为模板编程提供了更大的灵活性和表达力。

C++11特性总结

1. 关键字和语法

auto:可以根据右值,推导出右值的类型

nullptr:给空指针专用,能够和数值0进行区别(以前都是NULL)

foreach:基于范围的循环,可以方便的遍历数组和容器。底层就是通过指针或者迭代器来实现的。

for(Type val : container)
{cout<<val<<' ';
}

右值引用:move移动语义函数,move(val),forward类型完美转发函数,forward(val)

可变参数模版:template<typename… Args>

2. 绑定器和函数对象

C++11开始有函数对象,函数对象就是重写了operator()运算符的类创造的对象。

function:可以接收普通函数类型、函数对象类型和lambda表达式类型的类模版。

bind:可以最多绑定20个参数的绑定器

lambda表达式:匿名函数对象

3. 智能指针

unique_ptr:只允许右值赋值(构造)来移动转移对象所有权

shared_ptr 和 weak_ptr:shared_ptr会根据指向对象的指针数量提供一个引用计数,引用计数为0时才会释放对象,weak_ptr是为了解决shared_ptr在循环引用(交叉引用)时无法释放对象内存的情况。使用原则时,创建对象时使用shared_ptr,引用对象时使用weak_ptr。

4. 容器

unordered_set和unordered_map :使用哈希表实现,无序但是增删查的时间复杂度是O(1)

array:固定大小的数组容器,不可扩容,内存连续,提供了类似于标准容器的方法,如size(),at(),begin()等,性能比vector更高

forward_list:单向链表,只提供单向迭代器,比list(双向循环链表)省内存

5. C++语言提供了语言级别的多线程编程

#include<thread>

有了语言级别的多线程之后,就不必再依赖windows操作系统或是linux操作系统的系统调用,最大的优点就是方便了跨平台。

实际上,语言级别的多线程用的还是系统调用,只不过是通过编译器,在window下编译成createThread,linux下编译成pthread_create。

C++11多线程

多线程使用

  1. 创建和启动
    1. std::thread(func, args)传入线程函数,以及线程函数所需的参数(如果有的话,从第二个参数位置开始传入),线程创建完成会自动启动
    2. 这种创建完成直接启动的编码模式叫“直接构造函数初始化”(Direct Constructor Invocation)
  2. 子线程如何结束
    • 子线程函数运行完成,线程结束。
  3. 主线程如何处理子线程
    1. 子线程对象调用join()函数,主线程等待该线程结束才会继续运行;
    2. 子线程对象调用detach()函数,子线程与主线程分离,成为独立线程,主线程和子线程无关,也看不到子线程的输出,主线程结束后,子线程仍然会运行至自然结束。类似于守护线程。
  4. 线程的ID
    • 每个线程都有一个唯一的ID,可以使用std::this_thread::get_id()获取当前线程的ID。

示例

void threadHandle1(const char* name,int time)
{// 线程延时函数std::this_thread::sleep_for(std::chrono::seconds(time));cout << "Hello thread "<<name<<'!' << endl;
}

int main()
{
// 创建一个线程对象,传入一个线程函数,并且新线程会直接开始运行
std::thread t1(threadHandle1, "1", 2);
std::thread t2(threadHandle1, "2", 1);

t1.join();  // 等待子线程结束
t2.join();
//t1.detach();  // 将子线程设置为分离线程
cout &lt;&lt; &quot;main thread done!&quot; &lt;&lt; endl;// 主线程运行完成,如果查看当前线程有未运行完的子线程,进程就会异常终止
return 0;

}

互斥锁

互斥锁(Mutex,全称 Mutual Exclusion)是一种同步原语,用于多个线程同时访问共享资源时,避免竞态条件和数据不一致的问题。互斥锁确保任何时候只有一个线程可以访问被保护的资源。

竞态条件(Race Condition) 是指在多线程或多进程环境中,多个进程或线程同时访问并修改同一数据,导致最终结果的正确性受到破坏的情况。它的本质在于资源的争用,由于多个进程或线程对同一资源同时进行操作,最终可能导致不一致或不正确的结果。竞态条件通常发生在以下情况下:多个线程访问同一个共享资源,并且至少有一个线程在写入这个资源。当出现静态条件时,我们称这块代码是不可重用的,是临界区代码块。

为什么会出现竞态条件?

原因是,一行代码,实际在硬件层面可能分成了多个操作,如 tickedCount--,实质可能是

00B48657  mov         eax,dword ptr [tickedCount (0B53000h)]  
00B4865C  sub         eax,1  
00B4865F  mov         dword ptr [tickedCount (0B53000h)],eax  

而第一个线程执行的时候,另一个线程可能已经把数据给更改了,因此就产生了竞态条件。

互斥锁的基本使用

std::mutex在头文件<mutex>中。

std::mutex可以创建一个锁对象mtx,当一个线程执行 mtx.lock() 并获取锁时,其他线程在尝试执行它们自己的 mtx.lock() 语句时,如果锁已经被占用,它们将无法继续执行,会被挂起(阻塞),直到锁被释放。这意味着,其他线程在到达自己的 lock() 语句之前不会被阻塞,但是一旦到达并尝试获取锁,如果锁不可用,它们就会进入阻塞状态,等待锁被释放。(请注意,只有使用同一把锁的多个线程,才会在一个线程拿到锁执行的时候,直到锁释放前阻塞其他线程)

如下是一个使用互斥锁的示例

int tickedCount = 100;
std::mutex mtx;   // 创建锁

void sellTicket(int index)
{
while (tickedCount > 0)
{
// 临界代码段 -》原子操作 -》 线程间互斥操作
mtx.lock();
cout << index << " : " << tickedCount << endl;
tickedCount--;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

int main()
{
list<thread> list;
for (int i = 0; i < 3; ++i)
{
list.push_back(std::thread(sellTicket, i)); // 创建并启动,同时保存线程句柄
}

for (std::thread&amp; t : list)
{t.join();  // 设置主线程等待子线程执行完成
}return 0;

}

但是此时仍然会出现问题,如下所示,多线程程序运行中,可能出现这样一种情况,当tickedCount=1时,线程0,1,2都运行到了while(tickedCount>0),满足条件,都进入while循环体内。此时

  1. 线程2先拿到锁,于是线程0和线程1阻塞,线程2打印出的ticketCount为1,执行tickedCount--,然后释放锁,此时ticketCount的值为0。
  2. 接着线程0拿到锁,打印出ticketCount为1,执行tickedCount--,然后释放锁,此时ticketCount的值为-1。
  3. 最后线程1拿到锁,打印出ticketCount为-1,执行tickedCount--,然后释放锁,此时ticketCount的值为-2。

很显然,这是不合理的,tickedCount=0 后就不应该对其做任何操作了。

...
1 : 5
1 : 4
1 : 3
1 : 2
2 : 1
0 : 0
1 : -1

为了解决上述问题,我们要做的是在拿到锁之后,对tickedCount进行操作前,再对tickedCount进行一次判断。改进代码如下

void sellTicket(int index)
{while (tickedCount > 0)   // 锁+双重判断{// 临界代码段 -》原子操作 -》 线程间互斥操作mtx.lock();if (tickedCount > 0) {cout << index << " : " << tickedCount << endl;tickedCount--;}mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}

lock_guard:自动释放锁

为了解决忘记调用 unlock()导致锁没有释放,从而使线程阻塞的问题,可以使用**lock_guard类模板**创建一把自动释放的锁,它用于管理互斥锁(mutex)的生命周期,确保在代码块执行期间持有锁,并在代码块结束时自动释放锁。使用示例如下所示:

int tickedCount = 100;
std::mutex mtx; 

void sellTicket(int index)
{
while (tickedCount > 0) // 锁+双重判断
{
// 临界代码段 -》原子操作 -》 线程间互斥操作
{
lock_guard<std::mutex> lock(mtx);
if (tickedCount > 0)
{
cout << index << " : " << tickedCount << endl;
tickedCount--;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

上述代码,将加锁部分的代码以及锁都放到一个代码块中(被 {}包围的区域),是为了让锁在出代码块之后自动释放,从而将 sleep延时的语句排除在加锁的语句中。通常情况下,栈上对象的析构函数会在函数返回或出代码块时调用。

CAS和Atomic实现无锁同步

C++11中引入了mutex和lock_guard来避免竞态条件,但是这些属于软件层面的方法,性能开销是比较大的。

C++11还提供了新增的原子操作类<atomic>,它使用了硬件方法来保证资源的原子性,相较于加锁的方法,它的性能更高。对使用atomic中的类创建的变量进行操作是原子操作,无论多少个线程去修改这个变量,同一时间只能有一个修改操作,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。

atomic禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。

以下是一个使用原子类的示例,mycount是atomic类型变量,volatile保证了程序会去原始内存中读取变量的值而不是从缓存中,原子操作是在硬件层面上保证不可再分的操作,因此不用加锁,也不会出现多个线程同时修改mycount的情况。

volatile std::atomic<int> mycount = 0;  // 创建原子类型变量,对它的操作都为原子操作

void task()
{
for (int i = 0; i < 100000; ++i)
{
mycount++;
}
}

int main()
{
list<std::thread> tlist;
for (int i = 0; i < 10; ++i)
{
tlist.push_back(std::thread(task));
}

for (auto &amp;t : tlist)
{t.join();
}
cout &lt;&lt; mycount &lt;&lt; endl;return 0;

}

CAS指的是Compare-And-Swap。std::atomic 类提供了 CAS 操作的支持,主要通过 compare_exchange_weakcompare_exchange_strong 方法实现。
CAS 的基本步骤:

  1. 比较:检查内存位置 V 的当前值是否等于预期原值 A。
  2. 交换:如果当前值等于预期原值,则将新值 B 写入内存位置 V。
  3. 返回:返回内存位置 V 的原始值。

CAS 的优点:

  • 非阻塞:CAS 是一种非阻塞算法,即使操作失败,线程也不会被挂起,而是继续尝试。
  • 性能:在许多情况下,CAS 操作比传统的锁机制更高效,因为它们避免了线程调度和上下文切换的开销。

CAS 的缺点:

  • ABA 问题:如果内存位置的值在比较和交换之间被其他线程更改,即使最终值没有变化,CAS 操作也会失败。这就是所谓的 ABA 问题。
  • 循环等待:如果多个线程同时尝试更新同一个值,可能会导致线程长时间循环等待,消耗 CPU 资源。

线程间同步通信:生产者消费者模型

C++11开始,condition_variable 是标准库的一部分。condition_variable 是一种同步原语,通常在多线程编程中使用。它允许一个或多个线程等待某个条件的发生,而其他线程则可以通知这些等待的线程条件已经满足。condition_variable 通常与互斥锁(mutex)一起使用,以确保在等待条件时数据的一致性和线程安全。

以下是 condition_variable 的一些基本用法:

  1. 等待条件:线程可以调用 condition_variablewait 方法来挂起执行,直到另一个线程通知条件变量。调用 wait 方法时,必须提供一个已经锁定的互斥锁作为参数wait 方法会解锁互斥锁,并使当前线程进入等待状态,直到被唤醒
  2. 通知条件:当条件满足时,另一个线程可以调用 condition_variablenotify_one 方法来唤醒等待该条件的一个线程,或者调用 notify_all 来唤醒所有等待的线程,并且会释放互斥锁被唤醒的线程会重新锁定互斥锁,并继续执行。
  3. 超时等待:有些实现允许线程在等待条件变量时设置一个超时时间,如果在指定时间内没有被通知,线程将结束等待。

如下是结合多线程、互斥锁和条件变量(condition_variable​ )实现的一个生产者消费者模型,详细的程序演变过程可以查看详细的生产者消费者模型代码一步步演变过程1

queue<int> que;
std::mutex mtx;  
condition_variable cv;  // 条件变量

// 生产者消费者模型
void put(int val)
{
unique_lock<std::mutex> ulk(mtx);
while (!que.empty())
{
cv.wait(ulk);
}
que.push(val);
cout << "生产了: " << val << endl;
cv.notify_all();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

void getVal(int val)
{
unique_lock<std::mutex> ulk(mtx);
while (que.empty())
{
cv.wait(ulk);
}
que.pop();
cout << "消费了: " << val << endl;
cv.notify_all();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

int main()
{
list<std::thread> tlist;
for (int i = 1; i < 11; ++i)
{
tlist.push_back(std::thread(put, i));
tlist.push_back(std::thread(getVal, i));
}

for (auto&amp; t : tlist)
{t.join();
}return 0;

}

unique_lock与lock_guard区别

unique_locklock_guard 都是C++标准库中的RAII(Resource Acquisition Is Initialization)风格的互斥锁管理工具,用于自动管理互斥锁的生命周期,确保在作用域结束时释放互斥锁。尽管它们的目的相似,但它们在灵活性和使用场景上有所不同:

lock_guard

  • 简单性lock_guard 是最简单的RAII锁管理器,它在构造时自动加锁,并在析构时自动解锁。
  • 不可回收性:一旦构造了 lock_guard 对象,你就不能释放它持有的锁,直到它被销毁。
  • 异常安全性lock_guard 提供了异常安全的保证,即使在锁定后发生异常,它也会确保互斥锁被释放。
  • 使用场景:适用于简单的锁管理,当你需要在整个作用域内保持锁状态时。

unique_lock

  • 灵活性unique_lock 提供了更多的控制,包括在构造时不立即锁定互斥锁,以及在对象生命周期内多次加锁和解锁。
  • 可回收性unique_lock 允许你通过调用 unlock 方法来释放互斥锁,并在需要时再次加锁。
  • 条件变量unique_lock 通常与条件变量(std::condition_variable)一起使用,因为它需要能够解锁和重新加锁。
  • 异常安全性unique_lock 也提供了异常安全的保证,但它允许在发生异常时选择性地释放互斥锁。
  • 使用场景:适用于需要更细粒度控制互斥锁的场景,例如,当你需要在等待条件变量时释放锁,或者在持有锁的同时需要执行可能抛出异常的操作时。

  1. 详细的生产者消费者模型代码一步步演变过程

    详细的生产者消费者模型代码一步步演变过程

    1. 只使用多线程

    queue<int> que;
    

    // 生产者消费者模型
    void put(int val)
    {
    que.push(val);
    cout << "生产了: " << val << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    void getVal(int val)
    {
    que.pop();
    cout << "消费了: " << val << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    int main()
    {
    list<std::thread> tlist;
    for (int i = 1; i < 11; ++i)
    {
    tlist.push_back(std::thread(put, i));
    tlist.push_back(std::thread(getVal, i));
    }

    for (auto&amp; t : tlist)
    {t.join();
    }return 0;
    

    }

    此时问题很明显,因为是连续创建十个生产和消费线程

    很容易出现生产和消费的冲突,若某个线程消费时,que却为空

    那就会出错

    2. 使用互斥锁

    queue<int> que;
    std::mutex mtx;
    

    // 生产者消费者模型
    void put(int val)
    {
    mtx.lock();
    que.push(val);
    cout << "生产了: " << val << endl;
    mtx.unlock();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    void getVal(int val)
    {
    mtx.lock();
    que.pop();
    cout << "消费了: " << val << endl;
    mtx.unlock();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    int main()
    {
    list<std::thread> tlist;
    for (int i = 1; i < 11; ++i)
    {
    tlist.push_back(std::thread(put, i));
    tlist.push_back(std::thread(getVal, i));
    }

    for (auto&amp; t : tlist)
    {t.join();
    }return 0;
    

    }

    现在的话情况好一点了,因为给生产和消费的过程加了互斥锁

    因此生产时不能消费,消费时也不能生产,但是仍然存在问题,

    如果生产者抢到一次锁后,消费者连续两次抢到锁,就会导致第二次消费时que为空

    从而报错退出

    3. 进行条件判断

    queue<int> que;
    std::mutex mtx;
    

    // 生产者消费者模型
    void put(int val)
    {
    mtx.lock();
    if (que.empty())
    {
    que.push(val);
    cout << "生产了: " << val << endl;
    }
    mtx.unlock();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    void getVal(int val)
    {
    mtx.lock();
    if (!que.empty())
    {
    que.pop();
    cout << "消费了: " << val << endl;
    }
    mtx.unlock();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    现在不会再出现报错退出的情况了,

    生产者和消费者线程执行的时候,都会先检查que的情况,

    符合条件的情况下才会生产/消费,否则就会释放锁然后退出

    但是仍然存在问题,那就是最终生产和消费的数目与预期不符,

    我们启动了十个生产者和十个消费者线程,期望的是生产十次消费十次,

    现在这种写法,检查que不符合条件时线程就会结束,相当于跳过了一次生产/消费

    4. 使用条件变量

    queue<int> que;
    std::mutex mtx;
    condition_variable cv;
    

    // 生产者消费者模型
    void put(int val)
    {
    unique_lock<std::mutex> ulk(mtx);
    if (!que.empty())
    {
    cv.wait(ulk);
    }
    que.push(val);
    cout << "生产了: " << val << endl;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    void getVal(int val)
    {
    unique_lock<std::mutex> ulk(mtx);
    if (que.empty())
    {
    cv.wait(ulk);
    }
    que.pop();
    cout << "消费了: " << val << endl;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    现在已经接近完成,但是仍然会有问题,这里利用了 condition_variable ,

    它可以让线程释放锁并阻塞,然后等待唤醒,然而由于用的是if语句判断,所以可能出现这样一种情况

    两个消费者线程连续的抢到锁,都在if判断语句中阻塞,然后一个生产者拿到锁,生产完成,释放锁,

    并且唤醒了了两个阻塞的消费者,因为是if语句,不会再次判断,所以两个消费者都进行了消费,

    但是que中只有一个可供消费的产品,因此报错退出

    5. 改进判断条件

    queue<int> que;
    std::mutex mtx;
    condition_variable cv;
    

    // 生产者消费者模型
    void put(int val)
    {
    unique_lock<std::mutex> ulk(mtx);
    while (!que.empty())
    {
    cv.wait(ulk);
    }
    que.push(val);
    cout << "生产了: " << val << endl;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    void getVal(int val)
    {
    unique_lock<std::mutex> ulk(mtx);
    while (que.empty())
    {
    cv.wait(ulk);
    }
    que.pop();
    cout << "消费了: " << val << endl;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    int main()
    {
    list<std::thread> tlist;
    for (int i = 1; i < 11; ++i)
    {
    tlist.push_back(std::thread(put, i));
    tlist.push_back(std::thread(getVal, i));
    }

    for (auto&amp; t : tlist)
    {t.join();
    }return 0;
    

    }

    现在就是最终版本了,利用condition_variable,使得线程在不符合条件的情况下阻塞,等待条件满足时被唤醒执行 ↩

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NLP-自然语言处理基础

人工智能是建立可智能化处理事物的系统,让机器能够像人类一样完成智能任务。NLP(自然语言处理)是人工智能的一个分支,其主要任务包括命名实体识别、关系抽取、情感分析、机器翻译、问答系统、信息抽取、文本分类、机器阅读理解、智能对话和自动文摘等。NLP的发展历史经历了…

做题小结-未完成的二分写法

好像身不由己 不能自己很失败 细细品味这一首歌 又记起来了hjk。 https://www.luogu.com.cn/problem/CF2024B这个题也是逆天了 总结 我没做出来 然后我讲下我补题的思路吧 不知道和正解一样不一样 首先最基本的思路 就是我们先对所有的取一个min 这个就是一开始要拿的 再剔除最…

java环境安装

java安装链接[https://www.oracle.com/java/technologies/downloads/#java8-windows] 1 点击程序直接一直下一步下一步就好 注意安装的时候记得把第一次出现的地址保存一下 后面会用到 会安装两次 第一次安装的是jdk 第二次安装的是jre 其中jdk是java开发工具包 jre是java运行环…

NLP-题-Bilibili

NLP-题-Bilibili ​#国科大#​ ​#复习#​ ​#nlp#​ ‍ ‍ 有错题 一、题型填空题20道,每道1分; 简答题6道,每道7分; 综合题38分(计算、设计等)二、考试内容填空题(1)卷积层作用是什么?通过卷积操作减少参数(2)池化层作用是什么? 通过采样减少网络规模(3)Prompt…

1.16 java项目

今天主要进行了后端的搭建,html页面还并未完善另外,在进行安卓程序测试时出现了问题,AVD模拟器中HAXM总是安装失败,正在查找问题解决方法

Linux系统中 ping 的平均时间

在 Linux系统中提取 ping 的平均时间使用 ping 命令可以测试网络连接的质量,包括延迟和丢包率。在 Linux 系统中,计算 ping 命令的耗时可以通过以下方法进行: ping -c 5 域名 | grep rtt min/avg/max/mdev | awk -F[ /] {print $8, $NF}ping -c 5 域名: 向 域名 发送 5 次 I…

G1原理—9.如何优化G1中的MGC

大纲 1.大对象导致频繁Mixed GC的案例 2.Mixed GC到底是在优化什么(从避免到提速) 3.Mixed GC相关参数详解之堆内存分配参数 4.Mixed GC其他相关的参数详解及优化1.大对象导致频繁Mixed GC的案例 (1)案例背景 (2)问题现场 (3)Redis缓存有什么问题 (4)缓存同步服务有什么问题 (5…

Java 中的 ZoneOffset

介绍 在我们的这个世界上因为地球是圆的,所以每个国家都会有自己特定的时区。 时区在我们对时间的使用上扮演了非常重要的角色。但又因为时区的存在,又给我们带来了很多的麻烦,比如北美地区使用的夏令时和中国统一使用东 8 区的时间等。当这些时间在我们计算机中进行体现的时…

转:gunicorn相关配置

转自:https://blog.csdn.net/xu710263124/article/details/118975404一、gunicorn的简介Gunicorn是基于unix系统,被广泛应用的高性能的Python WSGI HTTP Server。用来解析HTTP请求的网关服务。它通常是在进行反向代理(如nginx),或者进行负载均衡(如 AWS ELB)和一个web 应…

Microsoft Office 2024 下载安装教程 ,超详细教程(建议新手收藏)

大家好,我是你们的效率小专家!今天给大家带来一篇非常实用的教程——**如何安装 Microsoft Office 2024 Professional Plus**!这款软件升级不仅让我们平时常用的 Word、Excel、PowerPoint 等工具变得更加强大,还新增了很多酷炫的功能,比如数据分析、动画创作、音视频编辑等…

2025年员工绩效考核指南

为什么要进行年度绩效评估? 评估用于形式化和记录员工与其工作期望相比的工作方式。这样,可以增强或指出性能需要更改或改进。此正式评估支持薪酬决定或人员行动,例如重新分类,永久性额外关税和纠正措施。 谁需要接受评估? 作为最佳实践,任何定期工作且处于预期继续职位的…

.NET 项目如何管理资源及配置文件

.NET/WPF 项目如何管理资源及配置文件.NET项目如何管理资源及配置文件_哔哩哔哩 .NET 项目的资源及配置文件(视频中的思维导图)本文为以上视频的笔记🍉 生成操作 Build actions for files - Visual Studio (Windows) | Microsoft Learn复制到输出目录 这个是跟生成操作独立…