文章目录
- 一、lambda表达式
- 二、线程库
- 三、包装器
- 四、绑定(不常用)
一、lambda表达式
如果待排序元素为数据集合中的元素和自定义类型,需要用户定义排序时的比较规则不同如下代码,但都可以使用sort方法,
#include <algorithm>
#include <functional>
int main()
{
int a[] = {4,1,8,5,3,7,0,9,2,6};
sort(a, a+sizeof(a)/sizeof(a[0]));
sort(a, a + sizeof(a) / sizeof(a[0]), greater<int>());
return 0;
}
struct T
{
T(const char* str, double price)
:_name(str)
, _price(price)
{}
string _name;
double _p;
};
struct CPLess
{
bool operator()(const T& p, const T& p)
{
return p._p < p._p;
}
};
struct CPGreater
{
bool operator()(const T& p, const T& p)
{
return p._> p._p;
}
};
int main()
{
vector<T> t= { { "A", 6.6 }, { "B", 6}, { "C", 1.1}, { "D",5.7 } };
sort(t.begin(), t.end(), CPLess());
sort(t.begin(), t.end(), CPGreater());
}
随着C++语法的发展,发现上面的写法太复杂,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式,上面的代码使用C++11中的lambda表达式来解决,可以看出它实际是一个匿名函数。
int main()
{
vector<T> t= { { "A", 6.6 }, { "B", 6}, { "C", 1.1}, { "D",5.7 } };
sort(t.begin(), v.end(), [](const T& g1, const T& g2){
return g1._p < g2._p; });
sort(t.begin(), t.end(), [](const T& g1, const T& g2){
return g1._p > g2._p; });
}
lambda表达式格式:[c] § mutable -> return-type {s}。
1、[c] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
2、§:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
3、mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
4、->return type:返回值类型。用->形式声明函数的返回值类型,没有返回值时此部分可省略,返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
5、{s}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:在lambda函数定义中,参数列表和返回值类型可以不写,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕获列表详解:捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。捕捉外面作用域的变量。
[v]:表示值传递方式捕捉变量v,我们传值的直接用x和y的话,它是一个常量不能修改,所以在后面加上mutable,可变的。但是没有完成交换,传值是一种拷贝,里面的改变不影响外面。正确写法要接下来要说的引用捕捉。
int main()
{
int x = 0, y = 1;
int m = 0, n = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
swap1(x, y);
cout << x << " "<< y << endl;
// 传值捕捉
auto swap2 = [x, y]() mutable
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl;
}
[&v]:表示引用传递捕捉变量v
// 引用捕捉
auto swap2 = [&x, &y]()
{
int tmp = x;
x = y;
y = tmp;
};
当变量太多可以用一种方式来解决,[=]:表示值传递方式捕获所有父作用域中的变量(包括this)和[&]:表示引用传递捕捉所有父作用域中的变量(包括this),也可以混合捕捉。捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复,但是可以[&,a]&全部引用捕捉出了a传值捕捉。
应用场景:比如线程用到lambda表达式,要求m个线程分别打印n。
int main()
{
size_t m;
cin >> m;
vector<thread> vthds(m);
for (size_t i = 0; i < m; i++)
{
size_t n;
cin >> n;
vthds[i] = thread([i, n, m]() {
for (int j = 0; j < n; j++) {
cout << i << ":" << j << endl;
}
cout << endl;
});
}
for (auto& t : vthds)
{
t.join();
}
return 0;
}
lambda大小:
首先 lambda是一个可调用对象,模拟的是函数对象即仿函数对象。函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
lambda其实只有一个字节大,它在底层会被编译器处理成仿函数,参数都会作为仿函数类的参数,lambda对象就是生成仿函数类的类型,它会被编译器转换为一个没有名字的仿函数类对象并由编译器生成类名。而仿函数的类是一个空类,空类对象没有给成员变量,就是一个字节。
总之,底层编译器对于lambda表达式的处理方式,就是按照函数对象的方式处理的,如果定义了一个lambda表达式,编译器会自动生成一个类,名称叫lambda+uuid(唯一字符串保证生成不同的类),在该类中重载了operator()。
所以lambda表达式之间不能相互赋值,即使看起来类型相同,因为底层生成的是不同的类型,每次的生成类名不一样属于不同的类,不同类不能互相赋值。
class Rate
{
public:
R(double r): _r(r)
{}
double operator()(double m, int y)
{ return m * _r * y;}
private:
double _r;
};
int main()
{
// 函数对象
double r = 0.49;
R a(r);
a(10000, 2);
// lambda
auto b = [=](double m, int y)->double{return m*r*y;
};
b(10000, 2);
return 0;
}
二、线程库
C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
thread() :构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1, args2,…):构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数
get_id() :获取线程id
joinable() :线程是否还在执行,joinable代表的是一个正在执行中的线程。
join() :该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach():在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关
1.线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
3、当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下有三种方式提供:函数指针、lambda表达式、函数对象。
4、thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象。
5、 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用join或者detach结束
线程函数参数:
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,所以即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。如果想要通过形参改变外部实参时,必须借助std::ref()函数。
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
atomic:
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的是没有事的,因为只读操作不会影响到数据,也不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦,传统方式可以对共享修改的数据可以加锁保护,是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁,所以C++11就引入了原子操作。原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
它支持原子性,让我们不用加锁即无锁编程,比如线程对一个变量同时进行加减。无锁编程就是尽可能少使用锁,如果这有一个队列,多线程往里面插入数值,要加锁,要加锁效率有点低,这时候就可以实现无锁队列。
首先要保证原子性,保证线程安全。比如两个线程尾插结点,有两个new1和new2,如果同时来的话,没加锁就会导致会导致一个添加上造成内存泄漏,所要要加锁,但是锁太重了。
在C++11中,不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问,使用atomic类模板,定义出需要的任意原子类型。
原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等
如下代码:
int main()
{
int n = 100000;
atomic<int> x = 0;;
thread t1([&, n](){
for (int i = 0; i < n; i++)
{
++x;
}
});
thread t2([&, n]() {
for (int i = 0; i < n; i++)
{
++x;
}
});
t1.join();
t2.join();
cout << x << endl;
return 0;
}
锁:
mutex mtx;
int x = 0;
void Func(int num)
{
//并行
for (int i = 0; i < num; i++)
{
mtx.lock();
++x;
mtx.unlock();
}
// 串行
/* mtx.lock();
for (int i = 0; i < num; i++)
{
++x;
}
mtx.unlock()*/;
}
int main()
{
int k= 10000;
thread t1(Func,1000);
thread t2(Func,2000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
第一个相当于并发,意思就是交错着加加,第二个是串行,意思就是两个中的一个先执行for循环另一个在执行for循环即一个线程跑完另一个再跑。我们经过测试后发现串行比并行快。因为以并行来看,有两个线程来到for循环里面,t1执行,t2阻塞会进入休眠状态切出去,切换上下文会消耗很多时间,可能还没切出去就已经解锁了,就把t2唤醒回来。
如果我们使用list,加完之后再插入或者再增加一些操作,它们的时间就差不多了就往中间靠了,并行可以接近于串行了。并行的时候一个线程执行操作,另一个在阻塞着,但是它可以做下面的事情比如打印,所以串行不一定比并行快。
我们再用前面的lambda表达式改造一下,不期望n改变,因为锁要变,所以捕捉列表可写写[&,n]:
int main()
{
int n = 100000;
int x = 0;
mutex mtx;
size_t begin = clock();
thread t1([&, n](){
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
});
thread t2([&, n]() {
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
});
t1.join();
t2.join();
size_t end = clock();
cout << x << endl;
cout << end - begin << endl;
return 0;
}
有的时候我们需要递归互斥锁:recursive_mutex,如果不用,栈可能会死锁,比如解锁在func(n-1)后面,t1进来还没解锁又调用自己自己把自己又锁住了。它可以加锁之后,如果递归进来发现是自己就会解开。
int x = 0;
recursive_mutex mtx;
void Func(int n)
{
if (n == 0)
return;
mtx.lock();
++x;
Func(n - 1);
mtx.unlock();
}
int main()
{
thread t1(Func, 2000);
thread t2(Func, 2000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
锁这里还面临另一个问题:加锁解锁之后,中间会抛异常,会产生死锁,它调到catch里面,没有解锁,另一个线程再来就会被卡住。
有些地方想支持就算抛异常也不会影响,实现了一个lockguard的类,它借助构造函数把锁给它,然后进行加锁,然后用析构函数解锁,这里这样写不对编译不过,就是在初始化列表那里出了问题,也不能加move因为它没有移动构造,只有在私有域加个引用,是锁对象的别名。但是实际不需要我们写,库里面有lock_guard,还有unique_lock,它可以手动加锁和解锁,比如可能中途需要解锁做一些动作,解开过一会再加过来。
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;
};
void Func(int n)
{
for (int i = 0; i < n; i++)
{
try
{
//LockGuard<mutex> lock(mtx);
//lock_guard<mutex> lock(mtx);
unique_lock<mutex> lock(mtx);
++x;
// 抛异常
if (rand() % 3 == 0)
{
throw exception("抛异常");
}
//mtx.unlock();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
}
int main()
{
thread t1(Func, 10);
thread t2(Func, 10);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
下面就是条件变量:
有一个经典操作,两个线程交替打印偶数和奇数。
这时候就需要条件变量,非常适合我走一下通知你一下的同步场景,交替走。
用一个条件变量就行
1、首先要保证谁先运行,如果让t1打印奇数,那它就需要先运行,放在前面不到一定它运行,一个线程被创建起来,它要放到CPU排队,它有可能排上也有可能排不上。那么如何保证t1先运行。
先熟悉一下条件变量的接口:还要注意条件变量不是线程安全的,要结合锁使用。
wait接口:当前执行的线程会进行阻塞,直到被唤醒,没有通知它之前都会进行阻塞,在马上阻塞的那一瞬间,这个函数会被unlock,允许其他线程继续。
在wait的时候必须得配合互斥锁:一个线程准备在条件变量上等待,但是进入等待之前另一个线程唤醒了条件变量,也就是wait操作被挂起,但并没有完成wait,这时一个线程触发信号操作唤醒,然后第一个线程才完成wait操作,这个线程收不到另一个线程的信号操作即导致唤醒丢失,而且还要防止多个线程同时请求wait,在wait之前要加锁保证wait是原子操作,怎么把锁加上,得传一个unique对象,构造对象的时候就是把锁加上了,也让在wait的时候把锁解开,让其他线程继续走。
如果其他线程notified,唤醒那一瞬间先获取锁,再修改条件变量的值,用锁进行保护。
notified接口:notified_one:通知一个等待的线程;notified_all:通知所有等待线程。如果没有等待什么都不做。
2、解决保证t1先运行,t2打印偶数,加一个判断条件,如果等于奇数就阻塞,进行wait。直到被t1notified唤醒
分析:有两种情况,t1先抢到锁,t2后抢到锁,t1先运行,t2阻塞在锁上面
它
t2先抢到锁,t1后抢到锁,t2先运行,t1阻塞在锁上面,但是t2会被一下步wait阻塞,并且wait会解锁,保证了t1先运行。
所以无论哪种可能,谁先抢到锁,都能保证t1先运行。
3、接下来要防止一个线程不断运行,以上t1会一直运行,要阻止它打印第二次,所以要让t1阻塞,因为t1要打印奇数就往后走,加一个判断条件,如果等于偶数就阻塞,进行wait。直到被t2唤醒。
两种写法,wait还提供一个前置条件的仿函数,如果是false就阻塞。如下:
int main()
{
mutex mtx;
condition_variable cv;
int n = 100;
int x = 1;
thread t1([&, n]() {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x >= 100)
break;
//if (x % 2 == 0) // 偶数就阻塞
//{
// cv.wait(lock);
//}
cv.wait(lock, [&x]() {return x % 2 != 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n]() {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x > 100)
break;
//if (x % 2 != 0) // 奇数就阻塞
//{
// cv.wait(lock);
//}
cv.wait(lock, [&x](){return x % 2 == 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
三、包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。也是可调用对象,它是对以前的对象进行包装比如函数指针、仿函数,包装成新的可调用对象,统一类型。
先看下面这代码,可以看出usf函数模板会被实例化为三份,因为f是变化的,可能是函数之后或仿函数或lambda,x同一个类型。
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()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF(function<double(double)>([](double d)->double{ return d / 4; }), 11.11) << endl;
return 0;
}
我们要想出一种能让只实例化一份,让各种可调用对象进行类型统一。包装器就可以解决,
类模板原型如下:
function在头文件<functional>
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
尖括号代表返回类型,圆括号代表参数列表,Ret: 可调用对象的返回类型,Args…:可调用对象的形参。
还能包装静态成员函数和普通成员函数,都要用取地址用类域限制,但是普通成员函数的调用又和前者不一样,参数列表还多一个参数类型,调用还增加一个匿名对象使用如下:
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
int plusd(int a, int b)
{
return a + b*10;
}
private:
int x = 10;
};
int main()
{
function<int(int, int)> f1(f);
function<int(int, int)> f2(f); // 函数指针
cout << f1(1, 2) << endl;
cout << f2(1, 2) << endl;
//function<int(int, int)> f3(Functor()); // 识别报错
function<int(int, int)> f3 = Functor();// 函数对象
cout << f1(1, 2) << endl;
cout << f3(1, 3) << endl;
function<int(int, int)> f4 = [](const int a, const int b) {return a + b; }; // lambda
cout << f4(1, 3) << endl;
function<int(int, int)> f5 = &Plus::plusi; // 类静态成员函数指针
cout << f5(1, 2) << endl;
Plus plus;
function<int(Plus, int, int)> f6 = &Plus::plusd; // 类成员函数指针
cout << f6(plus, 1, 2) << endl;
function<int(int, int)> f7 = [&plus](int x, int y)->int {return plus.plusd(x, y); };
cout << f7(1, 2) << endl;
// 函数名
cout << useF(function<double(double)>(f), 11.11) << endl;
// 函数对象
Functor ft;
cout << useF(function<double(double)>(ft), 11.11) << endl;
// lamber表达式
cout << useF(function<double(double)>([](double d)->double{ return d / 4; }), 11.11) << endl;
return 0;
return 0;
}
还有一种场景,可以建立命令和动作的映射关系,传任何可调对象都可以:
int(vector<string>&tokens)
{
stack<int>st;
map<string,function<int(int,int)>>opmap={}
{{"+",[](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;}}
for(auto& str:tokens)
{
if(opmap.count(str)==0)
{
st.push(stoi(str));
}
else
{
int r=st.top();
st.pop();
int l=st.top();
st.pop();
st.push(opmap[str](l,r));
}
}
}
四、绑定(不常用)
如果说包装器是类模板,那它就是是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
int Plus(int a, int b)
{
return a + b;
}
int SubFunc(int a, int b)
{
return a - b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b * x;
}
private:
int x = 20;
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);
cout << func1(1, 2) << endl;
function<int(int, int)> func2 = bind(SubFunc, placeholders::_1, placeholders::_2);
cout << func2(1, 2) << endl;
// 调整参数的顺序
function<int(int, int)> func3 = bind(SubFunc, placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
function<bool(int, int)> gt = bind(less<int>(), placeholders::_2, placeholders::_1);
cout << gt(1, 2) << endl;
// 绑定固定参数减少参数
function<int(Sub, int, int)> func4 = &Sub::sub;
cout << func4(Sub(), 10, 20) << endl;
cout << func4(Sub(), 100, 200) << endl;
function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func5(10, 20) << endl;
cout << func5(100, 200) << endl;
return 0;
}