目录
包装器
function包装器
bind绑定
更改实参传递的顺序和实参传递的个数
线程库
本期我们将继续进行C++11新特性的学习。
包装器
function包装器
function包装器,我们也称之为适配器,本质上就是一个类模板,为什么要引入function包装器的概念呢?
观察下述代码。
f(1);
这个代码站在C语言的角度没什么说的就是一个普通的函数调用,但是学了这么长时间的C++,我们能清楚地意识到这绝不可能单单是一个函数的调用,这个f可以为多种可调用对象,最简单的就是f为函数指针(函数名),也就是函数调用,但是当我们学习了仿函数和lambda表达式之后,这个f可以是仿函数对象,也可以是lambda表达式对象。
参考下述代码。
#include<iostream>template<class F, class T>
T useF(F f, T x)
{static int count = 0;std::cout << "count:" << ++count << std::endl;std::cout << "count:" << &count << std::endl;return f(x);
}
//调用的是拷贝构造
double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};int main()
{// 函数名std::cout << useF(f, 11.00) << std::endl;// 函数对象std::cout << useF(Functor(), 11.00) << std::endl;// lambda表达式std::cout << useF([](double d)->double{ return d / 4; }, 11.00) << std::endl;return 0;
}
运行结果如下。
不难发现,这个函数中的全局变量count我们实例化成了3份,因为count变量的地址有三份,也就意味着当我们给useF这个函数传递不同的值时,这个useF函数本质上是被实例化成了三份,因为F这个木类模板被实例化成了三份。
实例化成三份,资源消耗太大,有没有其它的办法使得传递不同的参数时,最终函数只实例化一份呢?
此时,function包装器就派上了用场。
我们先通过一段代码了解一下function包装器如何使用,代码如下。
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 + 1;}double plusd(double a, double b){return a + b;}
};int main()
{//1.传递函数函数指针function<int(int, int)>f1 = f;cout << f1(1, 2) << endl;//2.传递仿函数对象function<int(int, int)>f2 = Functor();cout << f2(1, 2) << endl;//3.传递非静态成员函数函数指针function<double(Plus, double, double)>f3 = &Plus::plusd;cout << f3(Plus(), 1.1, 2.2) << endl;//4.传递静态成员函数函数指针function<int(int, int)>f4 = &Plus::plusi;cout << f4(1, 3) << endl;//5.传递lambda表达式function<int(int, int)>f5 = [](int a, int b) {return (a + b) * 10; };cout << f5(1, 2) << endl;return 0;}
运行结果如下。
其实function就是一个类模板,其创建的对象可以接收可调用对象(函数指针,仿函数,lambda表达式)。
熟悉了function的用法之后,我们再对上述实例化成三份useF函数的情景做出优化。
我们使用function进行代码改造,代码如下。
template<class F, class T>
T useF(F f, T x)
{static int count = 0;std::cout << "count:" << ++count << std::endl;std::cout << "count:" << &count << std::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)> f1 = f;std::cout << useF(f1, 11.00) << std::endl;// 函数对象function<double(double)>f2 = Functor();std::cout << useF(f2, 11.00) << std::endl;// lamber表达式function<double(double)>f3 = [](double d)->double { return d / 4; };std::cout << useF(f3,11.00) << std::endl;return 0;
}
运行结果如下。
不难发现,最终useF函数只实例化了一份,因为count的地址都是一样的。这便是function包装器的用法,提升了代码的效率,节省了资源。
bind绑定
bind是一个函数模板,我们也称之为函数包装器(适配器),用于接收一个可调用对象,生成一个新的可调用对象,适配原对象的参数列表。
更改实参传递的顺序和实参传递的个数
int SubFunc(int a, int b)
{return a - b;
}class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//1.改变实参参数顺序function<int(int, int)>f1 = SubFunc;function<int(int, int)>f2 = bind(SubFunc, placeholders::_1, placeholders::_2);cout << f1(1, 2) << endl;cout << f1(1, 2) << endl;function<int(int, int)>f3 = bind(SubFunc, placeholders::_2, placeholders::_1);cout << f1(1, 2) << endl;//2.改变实参参数个数function<int(Sub, int, int)>f4 = &Sub::sub;cout << f4(Sub(), 1, 2) << endl;function<int(Sub,int, int)>f5 = bind(&Sub::sub, placeholders::_1, placeholders::_2, placeholders::_3);cout << f5(Sub(), 1, 2) << endl;function<int(int, int)>f6 = bind(&Sub::sub,Sub(), placeholders::_1, placeholders::_2);cout << f6(1, 2) << endl;function<int(int)>f7 = bind(&Sub::sub, Sub(),1,placeholders::_1);cout << f7(2) << endl;return 0;
}
运行结果如下。
不难发现,我们通过bind绑定,实现了实参传递时的顺序和个数的不同。
线程库
之前,在学习Linux时,我们已经学习过了线程库的概念,我们在Linux中使用的是Pthread线程库。在C++11中我们也引入了线程库的概念,不过C++11中的线程库是被封装在了一个thread的类里。
通过查看C++文档不难发现,thread线程对象可以调用无参构造创建,但是创建之后不进行任何操作,同时thread线程对象,不允许被拷贝构造生成,但是允许移动构造生成,通过thread线程对象不允许调用赋值运算符重载进行赋值,但是可以调用移动赋值进行赋值。
情景:创建一个全局变量,使得两个线程对其进行++操作。
代码如下。
int x = 0;
void handle(int n)
{for (int i = 0; i < n; i++){++x;}
}int main()
{thread t1(handle,5000);thread t2(handle,5000);t1.join();t2.join();cout << x << endl;return 0;
}
运行结果如下。
两个线程分别对全局变量x,++5000次,最终打印出来的x的值是10000,貌似结果也没有什么问题,如果我们让每个线程都对x,++50000次呢?
按道理说此事的x应该是100000,但是打印出来的结果却是55330,很明显这出了问题,我们称之为线程安全问题,为什么会出现这种问题呢?其实在Linux系统编程中我们已经遇到了类似的问题,这是因为++操作分为三步,第一步,将寄存器中的x的值拿出来;第二步,CPU对x进行++操作;第三步,将++之后的x值放回寄存器,最后由操作系统将最终寄存器的值加载到内存中。 正是因为有了这三步,就增加了风险的概率,第一个线程加x值++之后还没有来的急放回寄存器,第二个线程就又对寄存器中的值进行了++,并且最终将++之后的x值返回到了寄存器中,并且更新到了内存中,此时第一个线程又将++之后的x的值放回寄存器,更新到了内存中,所以此时可能两次++操作,但是内存中x的值,只被++了一次。
怎么解决这样的线程安全问题呢?我们引入了互斥锁的概念,C++中也是有互斥锁的,文档如下。
所以我们就要对++操作进行加锁,但是问题又来了我们是加到for循环内部还是for循环外部呢?
我们建议将锁加在for循环的外面。
为什么要加在for循环的外面呢?
我们先简单分析一下,如果锁加在了for循环的外面,其实两个线程是串行运行的,如果锁加在了for循环的内部,其实两个线程是并行运行的,所以按照道理来说,应该是锁加在for循环内部效率更高,为什么还要加在for循环外呢?
这是因为虽然锁加在了内部是一个并行处理的过程,但是锁只有一把,当一把锁被一个线程占用时,另一个线程就只能被放入阻塞队列中去等待锁资源,与此同时操作系统要保存当前线程的上下文,当前线程获取到了锁资源时,就会从阻塞队列中剥离出来,然后操作系统会恢复其上下文,然后当前线程再去执行。正是因为如此,操作系统对上下文的保存和恢复也是需要耗费时间的,大量的加锁和解锁就意味着多次的上下文的保存和恢复,会去耗费额外大量的时间,所以我们推荐将锁加在for循环的外面。
如果不使用锁,还有什么方法,可以避免上述隐患呢?
其实还有一种方法,就是原子操作。
先不使用原子操作,现在for循环外使用锁,我们查看代码的运行时间。
int main()
{//atomic<int> x = 0;int x = 0;mutex mt;int costime = 0;thread t1([&x, &mt,&costime](int n){int begin1 = clock();mt.lock();for (int i = 0; i < n; i++){++x;}mt.unlock();int end1 = clock();costime += end1 - begin1;},50000000);thread t2([&x, &mt,&costime](int n){int begin2 = clock();mt.lock();for (int i = 0; i < n; i++){++x;}mt.unlock();int end2 = clock();costime += end2 - begin2;}, 50000000);t1.join();t2.join();cout<< x << endl;cout << costime << endl;return 0;
}
运行结果如下。
不难发现,代码的运行时间没293毫秒。
如果我们使用原子操作,代码如下。
int main()
{atomic<int> x = 0;mutex mt;int costime = 0;thread t1([&x, &mt,&costime](int n){int begin1 = clock();for (int i = 0; i < n; i++){++x;}int end1 = clock();costime += end1 - begin1;},50000000);thread t2([&x, &mt,&costime](int n){int begin2 = clock();for (int i = 0; i < n; i++){++x;}int end2 = clock();costime += end2 - begin2;}, 50000000);t1.join();t2.join();cout<< x << endl;cout << costime << endl;return 0;
}
运行结果如下。
虽然还是和加锁有差异,但是也是一种不错的避免加锁而实现线程安全的方法。
以上便是线程库相关的知识。
本期内容到此结束^_^