目录
包装器
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;
}
运行结果如下。
虽然还是和加锁有差异,但是也是一种不错的避免加锁而实现线程安全的方法。
以上便是线程库相关的知识。
本期内容到此结束^_^