C++中的lambda表达式和线程库

98中的一个例子

如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法

#include <algorithm>
#include <functional>
int main()
{
	int array[] = {4,1,8,5,3,7,0,9,2,6};
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array+sizeof(array)/sizeof(array[0]));
	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
	return 0;
}

排序一个单链表
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{
	string _name;
	double _price;
};
struct Compare
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price <= gr._price;
	}
};
int main()
{
	Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, { "菠萝", 1.5 } };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
	return 0;
}

每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便

lambda表达式

函数中声明函数

sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods&left, const Goods& right)
													{
															return left._price < right._price; 
													});

lambda表达式语法

lambda表达式书写格式:

  • [capture-list] (parameters) mutable -> return-type { statement }
  1. lambda表达式各部分说明
    • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
    • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
    • ->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
    • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
[捕捉列表](参数列表)mutable->返回值类型{//函数体};
[]{}; //最简单的lambda表达式
  1. 捕获列表说明
    捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
    • [var]:表示值传递方式捕捉变量var
    • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
    • [&var]:表示引用传递捕捉变量var
    • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
    • [this]:表示值传递方式捕捉当前的this指针

注意:

  1. 父作用域指包含lambda函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  3. 在块作用域以外的lambda函数捕捉列表必须为空。
  4. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。(主函数只能捕获主函数声明得变量)
int a = 3, b = 4;
[=,&g_a]{return a + 3; };	//验证4
  1. lambda表达式之间不能相互赋值,即使看起来类型相同,找不到operator=()。
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();//执行相应得表达式
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[]{};
	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=]{return a + 3; };	//想要在a的基础上+3返回。
	//但是这个lambda表达式没有用
	//因为没有取名字

	// 省略了返回值类型,无返回值类型
	//不知道lambda表达式的类型就用auto
	//[&]以引用的方式捕获当前主函数的变量  a=3;b=13;
	//[=]以值得方式进行捕获			a=3;b=4;
	auto fun1 = [=](int c)mutable{b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;


	// 各部分都很完善的lambda函数
	// [=] 以值的方式捕获所有的变量
	// [&] 以引用的方式捕获所有的变量
	// [=,&b] 对于b变量以引用得方式捕获,对于其它变量用值的方式
	auto fun2 = [=, &b](int c)->int{return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	// 以值得方式捕获x,函数内修改不会影响外部
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;

	return 0;

仿函数与lambda表达式的联系

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象

class Rate
{
public:
	Rate(double rate) 
		: _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate); //定义一个对象将利率传进去
	r1(10000, 2);	//对象调用自身的方法,跟函数调用比较像都是 名字()
	// 仿函数
	//=捕获rate
	auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
	r2(10000, 2);
	return 0;
}

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()。

lambda表达式的应用

int array[] = { 1, 2, 3, 4, 5 };
for_each(array, array + 5, [](int&c){c *= 2; });
for_each(array, array + 5, [](int c){cout << c<<" "; });

线程库

#include<thread>
void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};

int main()
{
	TF tf;
	//线程函数尾函数指针
	thread t1(ThreadFunc, 10);
	//线程函数为lambda表达式
	thread t2([]{cout << "Thread2" << endl; });
	//线程函数为函数对象
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();
	cout << "Main thread!" << endl;

	system("pause");
	return 0;
}

线程之间不能互相赋值,也不能拷贝

线程的启动

C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex() 创建线程函数来实现 (注意_beginthreadex是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针。

#include<thread>
void ThreadFunc1(int& x)
{
	cout << &x << " " << x << endl;
	x += 10;
}

void ThreadFunc2(int*x)
{
	*x += 10;
}

int main()
{
	int a = 10;

	//在线程函数中对a修改,不会影响外部实参,因为:线程函数虽然是引用方式,但其实际引用的是线程栈中的拷贝
	thread t1(ThreadFunc1, a);
	t1.join();
	cout << &a <<" "<< a << endl;

	//地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;

	system("pause");
	return 0;
}

线程的结束

1. join()方式

join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用的join()之后joinable()就将返回false了。主线程会阻塞

2. detach()

detach:会从调用线程中分理出新的线程,之后不能再与新线程交互。就像是你和你女朋友分手,那之后你们就不会再有联系(交互)了,而她的之后消费的各种资源也就不需要你去埋单了(清理资源)。此时调用joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收

原子性操作

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

#include <iostream>
using namespace std;
#include <thread>
unsigned long sum = 0L;
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;
}
int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	//两个线程每个每回都循环10000000次,每次加1
	//如果没问题应该是20000000
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	system("pause");
	return 0;
}

在这里插入图片描述

通过加锁保证线程安全

#include <iostream>
using namespace std;
#include <thread>
#include<mutex>
unsigned long sum = 0L;

mutex m;
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		m.lock();
		sum++;
		m.unlock();
	}
}
int main()
{
	size_t begin = clock();
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	//两个线程每个每回都循环10000000次,每次加1
	//如果没问题应该是20000000
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	size_t end = clock();
	cout << end - begin << endl; //计算时间
	system("pause");
	return 0;
}

加锁后,能够保证线程安全,但是耗费的时间就比较多,而且有可能导致死锁
在这里插入图片描述

原子操作

原子操作:一但开始,不能被打断

对于内置类型

在这里插入图片描述

对于自定义类型

使用atomic模板,定义出需要的任意原子类型

atomic<T> t;

注意事项

原子类型通常属于“资源型”数据,多个线程只能访问单个原子类型的拷贝,因此在c++11中原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造,移动构造以及operator=等,于是,标准库已经将atmoic模板类中的拷贝构造,移动构造,赋值运算符的重载默认删除了

#include <iostream>
using namespace std;
#include <thread>
#include<atomic>

//unsigned long sum = 0L;
atomic_long sum{0}; //定义原子类型变量

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		sum++;
	}
}
int main()
{
	size_t begin = clock();
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	//两个线程每个每回都循环10000000次,每次加1
	//如果没问题应该是20000000
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	size_t end = clock();
	cout << end - begin << endl; //计算时间
	system("pause");
	return 0;
}

在这里插入图片描述
时间更短,也能保证线程安全

发布了253 篇原创文章 · 获赞 41 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/liuyuchen282828/article/details/103970886