最近自学线程池,看了人家的代码,发现很多代码里用到C++11新特性中某些功能和一些陌生的关键字加上鄙人孤陋寡闻很多都不会这是啥功能,导致都不知道怎么理解,经过一番查阅之后懂了一些特此记录一下。
C++知识点查漏补缺
一、可变参数模板函数基本用法
- 可变参数模板可以创建任意个参数的模板函数和模板类
1)可变参数模板函数声明和定义
template<typename... Args> //Args是一个模板参数包
void Show(Args... args) //args是一个函数参数包
{
//函数功能
return;
}
- 可变参数模板函数一般用于可变参数输出,可变参数args的调用不能用args[2]方式调用,可以采用递归展开和非递归展开。
2)可变参数模板函数调用示例
- 递归展开
void showList(){
cout<<"empty~"<<endl;
}
template<typename T,typename...Args>
void showList(T value,Args...args){
cout<<value<<endl;
showList(args...);
}
调用 showList(5,'L',1.1);
输出 5
'L'
1.1
empty~
- 非递归展开
template<typename T>
void Print(T arg)
{
cout<<arg<<endl;
}
template<typename ... Args>
void showList2(Args ... args)
{
int a[]={(Print(args),0)...};
}
调用 showList2(5,'L',1.1);
输出 5
'L'
1.1
一般来说,递归调用方式安全可控,而非递归调用方式调用次数比较难以控制。
二、emplace_push与push_back区别
直接用代码来说两者的区别吧
class Test{
public:
Test(string n,int by,int a):name(n),bothYear(by),age(a){
cout<<"有参构造"<<endl;
}
Test(const Test&t):name(t.name),bothYear(t.bothYear),age(t.age){
cout<<"拷贝构造"<<endl;
}
private:
string name;
int age,bothYear;
};
int main(int argc,char*argv[]){
vector<Test>t;
t.emplace_back("zjj",1998,7);
cout<<"````"<<endl;
t.push_back( Test("zjj",1998,8));
return 0;
}
//输出:
有参构造
/```
有参构造
拷贝构造
拷贝构造
- 在执行emplace_back的时候,只调用了转移构造函数,在插入的时候直接构造,效率更高,减少额外空间的开辟
- 在执行push_back的时候,调用了构造和拷贝构造函数,因为在使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费
。
三、inline、explicit、implicit关键字
1)inline
- 在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数,栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
// 函数定义为inline即:内联函数
inline char* inline_test(int num)
{
return (num % 2 > 0) ? "奇" : "偶";
}
int main()
{
int i = 0;
for (i = 1; i < 10; i++)
{
printf("inline_test: i:%d 奇偶性:%s\n", i, inline_test(i));
}
return 0;
}
上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实,在内部的工作就是在每个for循环的内部任何调用inline_test(i)的地方都换成了(i%2>0)?”奇”:”偶”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
- inline的使用是有所限制的,inline只适合涵数体内代码简单的涵数使用。
(1) 不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。
(2) 而所有(除了最平凡,几乎什么也没做)的虚拟函数,都追阻止inlining的进行。这应该不会引起太多的惊讶,因为virtual意味着”等待,直到执行时期再确定应该调用哪一个函数“,而inline却意味着”在编译阶段,将调用动作以被调用函数的主体取代之“。如果编译器做决定时,尚不知道该调用哪一个函数,你就很难责成他们做出一个inline函数。 - inline 是一种“用于实现的关键字,而不是一种“用于声明的关键字”
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
上述代码中Foo函数不能成为内联函数而下述代码则可以
void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起
-
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?
-
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
-
以下情况不宜使用内联:
- (1) 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
- (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
- (3) 类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。
2)explicit、implicit
-
我们先来回顾一下啥叫显式和隐式转换法
-
- 显式转换法: person p1; person p2 = person(10); person p3 = person(p2); - 隐式转换法: person p4 = 10;(相当于person p4 = person(10)) person p5 = p4;
- C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显式的, 而非隐式的,跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).
class Test{
public:
Test(int age){
```
}
private:
int age;
};
该类默认是implicit的可以显式和隐式转换
class Test{
public:
explicit Test(int age){
```
}
private:
int age;
};
该类默认是explicit 的只能是显式转换
- explicit关键字的作用就是防止类构造函数的隐式自动转换,只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时,是不会产生隐式转换的, 所以explicit关键字也就无效了。
class Test{
public:
explicit Test(int age,string name){
```
}
private:
int age;
string name;
};
这样的话即使该构造函数定义了explicit ,也是无效的,但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数
class Test{
public:
explicit Test(int age,string name="ccc"){
```
}
/*或这样也可以
explicit Test(int ag):age(ag),name("ccc"){
```
}
*/
private:
int age;
string name;
};
四、std::function与std::bind
1)std::bind
std::bind的头文件是 ,它是一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。
-
std::bind主要有以下两个作用:
- 将可调用对象和其参数绑定成一个防函数;
- 只绑定部分参数,减少可调用对象传入的参数。
-
1、std::bind绑定普通函数
double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);
std::cout << fn_half(10) << '\n'; // 5
-
bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind(my_divide,_1,2)等价于std::bind (&my_divide,_1,2);
-
_1表示占位符,位于中,std::placeholders::_1;
-
2、std::bind绑定类的成员函数
class Foo {
public:
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main()
{
Foo foo;
auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
f(5); // 100
}
- bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
- 必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&;
- 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo;
- 3、参数绑定
bind其调用形式如下:
auto newCallable=bind(callable,arg_list);
- bind的第一个参数为一个可调用对象,可调用对象是指可以对其使用调用运算符()的对象。
- 可调用对象常用的有函数、函数指针、重载了函数调用运算符的类和lambda表达式
arg_list是调用对象的参数列表,可以包含 _ 1, _ 2等这样的占位符,用于占据调用对象的参数位置,数字代表着是第几个未定的占位参数,占位符被定义在,命名空间placeholders中。也可以包含被绑定对象的参数。arg_list应该和被绑定对象的参数一样多。
int sum(int a, int b, int c) {
if (a > b)return a + c;
return b + c;
}
auto add = bind(sum, _1, _2, 10);
这样就将sum绑定由bind新生成的一个调用sum的对象上;
_ 1表示新对象中的第一个参数,是一个占位符。也就是说,实际上,这个bind会add( _ 1, _ 2)会被映射成为sum( _ 1, _ 2, 10),此时add的参数就会代替原来的占位符成为调用sum的参数,当然前提是两者的类型要匹配。
比如:add(20,10)实际调用为sum(20,10,10),结果为30;
参数顺序可换
#include<iostream>
using namespace std;
using namespace placeholders;
int sum(int a, int b, int c)
{
if (a > b)return a + c;
return b + c;
}
int main(void)
{
auto add = bind(sum, _1, _2, 10);
auto add2 = bind(sum, _2, _1, 10);
int t = add(20, 10), t1 = add2(10, 20);
cout << t << " " << t1 << endl;
return 0;
}
bind也可以换原来参数的顺序,因为实际在调用新对象时,我们传递给新对象的参数实际就是那些占位符占据的位置的参数,所以上面调用情况如下:
- add(20,10) 时,参数20对应占位符1,参数10对应占位符2,故实际调用为sum(20,10,10);
- add2(10,20)时,参数10对应占位符1,参数20对应占位符2,故实际调用为sum(20,10,10),从而重排了参数顺序。
可以包含 _ 1, _ 2等这样的占位符,用于占据调用对象的参数位置,数字代表着是第几个未定的占位参数
void printValue(int value1, int value2, int value3)
{
cout << value1 << endl;
cout << value2 << endl;
cout << value3 << endl;
}
void testFunc(std::function<void(int, int)> printFunc, int value1, int value2)
{
printFunc(value1, value2);
}
int main()
{
auto newFunc1 = std::bind(printValue, placeholders::_1, placeholders::_2, 123);
testFunc(newFunc1, 111, 222); //111 222 123
auto newFunc2 = std::bind(printValue, placeholders::_1, 123, placeholders::_2);
testFunc(newFunc2, 111, 222);//111 123 222
system("pause");
}
- 4、绑定一个引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std::placeholders;
using namespace std;
ostream & print(ostream &os, const string& s, char c)
{
os << s << c;
return os;
}
int main()
{
vector<string> words{"helo", "world", "this", "is", "C++11"};
ostringstream os;
char c = ' ';
for_each(words.begin(), words.end(),
[&os, c](const string & s){os << s << c;} );
cout << os.str() << endl;
ostringstream os1;
// ostream不能拷贝,若希望传递给bind一个对象,
// 而不拷贝它,就必须使用标准库提供的ref函数
for_each(words.begin(), words.end(),
bind(print, ref(os1), _1, c));
cout << os1.str() << endl;
}
- 5、指向成员函数的指针
通过下面的例子,熟悉一下指向成员函数的指针的定义方法。
#include <iostream>
struct Foo {
int value;
void f() { std::cout << "f(" << this->value << ")\n"; }
void g() { std::cout << "g(" << this->value << ")\n"; }
};
void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
(foo1->*fun)(); // call fun on the object foo1
(foo2->*fun)(); // call fun on the object foo2
}
int main() {
Foo foo1{1};
Foo foo2{2};
apply(&foo1, &foo2, &Foo::f);
apply(&foo1, &foo2, &Foo::g);
}
成员函数指针的定义:void (Foo::fun)(),调用是传递的实参: &Foo::f;
fun为类成员函数指针,所以调用是要通过解引用的方式获取成员函数fun,即(foo1->*fun)();
2)std::function
- 类模板 std::function 是通用多态函数封装器。
- std::function 的实例能存储、复制及调用任何可调用 (Callable) 目标——函数、 lambda 表达式、 bind
表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。 - 它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的)
- function < T > f 是用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同(即T是retType(args) )
- std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。
#include <functional>
#include <iostream>
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_+i << '\n'; }
int num_;
};
void print_num(int i)
{
std::cout << i << '\n';
}
struct PrintNum {
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
// 存储自由函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// 存储 lambda
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// 存储到 std::bind 调用的结果
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 存储到成员函数的调用
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314159, 1);
// 存储到数据成员访问器的调用
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
// 存储到成员函数及对象的调用
using std::placeholders::_1;
std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
f_add_display2(2);
// 存储到成员函数和对象指针的调用
std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
f_add_display3(3);
// 存储到函数对象的调用
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
}
//输出
-9
42
31337
314160
314160
num_: 314159
314161
314162
18
3)std::function与std::bind之间应用
#include <iostream>
#include <functional>
void test1(){std::cout<<"function"<<std::endl;}
int test2(int i){ return i; }
int test3(int i, int j){ return i+j; }
struct A{
void foo(int i){ std::cout<<i<<std::endl; }
};
int main() {
std::function<void()> fn1 = std::bind(test1);
std::function<int(int)> fn2 = std::bind(test2, std::placeholders::_1);
std::function<int(int, int)> fn3 = std::bind(test3, std::placeholders::_1, std::placeholders::_2);
std::function<int(int)> fn4 = std::bind(test3, 3, std::placeholders::_1);
std::function<int()> fn5 = std::bind(test3, 3, 4);
A a;
std::function<void(int)> fn6 = std::bind(&A::foo, &a, std::placeholders::_1);
fn1();
std::cout<<fn2(1)<<std::endl;
std::cout<<fn3(2, 3)<<std::endl;
std::cout<<fn4(3)<<std::endl;
std::cout<<fn5()<<std::endl;
fn6(8);
}
//输出
function
1
5
6
7
8
五、实现线程池
- 线程池主要都是这几点的需求:
- 1)线程池管理器:初始化和创建线程,启动和停止线程,调配任务,管理线程池
- 2)工作线程:线程池中等待并执行分配的任务
- 3)任务接口:添加任务的接口,一提供工作线程调度任务的执行
- 4)任务队列:用于存放没有处理的任务,提供一种缓冲机制
- 5)任务调度需要加锁和互斥
- 6)每次添加一个新任务就唤醒一个线程
- 7)线程池析构时唤醒所有线程,然后终止回收资源
1)相对容易理解一点的版本
该版本的线程池没有实现线程的优先级调度
#ifndef _THREAD_POOL_2__
#define _THREAD_POOL_2__
#include<thread>
#include<mutex>
#include<condition_variable>
#include<atomic>
#include<vector>
#include<queue>
#include<functional>
class ThreadPool{
public:
using Task=std::function<void()>;
//初始化线程池数量
explicit ThreadPool(int num):_threadNum(num),_threadPoolStatus(false){}
//启动线程池,将所有线程放入到线程容器中
void start(){
_threadPoolStatus=true;
for(int i=0;i<_threadNum;i++){
_threads.emplace_back(std::thread(&ThreadPool::work,this));
}
}
//任务接口:添加任务接口,提供工作线程调度任务的执行
void appendTask(const Task&task){
if(_threadPoolStatus){
std::unique_lock<std::mutex>jk(_mutex);
_tasks.push(task);
_cond.notify_one();
}
}
//关闭线程池
void stop(){
{
std::unique_lock<std::mutex>jk(_mutex);
_threadPoolStatus=false;
_cond.notify_all();
}
for(auto&s:_threads){
if(s.joinable()) s.join();
}
}
//销毁线程池
~ThreadPool(){
if(_threadPoolStatus) stop();
}
private:
//工作线程:线程池中等待并执行分配的任务
void work(){
printf("begin work thread:%d\n",std::this_thread::get_id());
while(_threadPoolStatus){
Task task;
{
std::unique_lock<std::mutex>jk(_mutex);
if(_threadPoolStatus&&!_tasks.empty()){
task=_tasks.front();
_tasks.pop();
}else if(_threadPoolStatus&&_tasks.empty()){
_cond.wait(jk);
}
}
if(task) task();
}
printf("end work thread:%d\n",std::this_thread::get_id());
}
ThreadPool(const ThreadPool&)=delete;//禁止拷贝复制
ThreadPool&operator=(const ThreadPool&)=delete;
std::atomic_bool _threadPoolStatus;//线程状态
std::mutex _mutex;
std::condition_variable _cond;
std::vector<std::thread>_threads;//线程容器:存放线程数量
std::queue<Task> _tasks;//任务队列:用于存放没有处理的任务,提供一种缓冲机制
int _threadNum;//线程数量
};
#endif
#include<iostream>
#include<chrono>
#include"threadPool2.h"
void printfx(int x){
std::cout<<"Task:"<<x<<" work in thread"<<std::this_thread::get_id()<<std::endl;
}
int main(int argc,char*argv[]){
ThreadPool tpool(3);
tpool.start();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
for(int i=0;i<6;i++){
tpool.appendTask(std::bind(printfx,i));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
tpool.stop();
return 0;
}
2)相对难理解一点的版本
#ifndef _THREAD_POOL_HARD_2_H
#define _THREAD_POOL_HARD_2_H
#include<iostream>
#include<vector>
#include<queue>
#include<functional>
#include<future>//C++ 11新特性,可以获取异步任务的结果,可用来实现同步。包括std::sync和std::future。
#include<thread>
#include<mutex>
#include<condition_variable>
#include<memory>
#include<stdexcept>
class ThreadPool{
public:
ThreadPool(size_t);//创建线程、启动、管理
~ThreadPool();//停止线程
template<class F,class... Args>
auto enterQueue(F&&f,Args&&... args)//调配任务
->std::future<typename std::result_of<F(Args...)>::type>;
private:
std::vector<std::thread>workers;//工作区
std::queue<std::function<void()>>tasks;//任务队列
std::mutex queue_mutex;//互斥锁
std::condition_variable condition;//条件变量
bool stop;//线程状态
};
//创建线程、启动、管理
inline ThreadPool::ThreadPool(size_t _threads):stop(false){
for(int i=0;i<_threads;i++){
workers.emplace_back([this](){
while(1){
std::function<void()> task;
{
std::unique_lock<std::mutex>lock(this->queue_mutex);
condition.wait(lock,[this](){
return this->stop||!this->tasks.empty();
});
if(this->stop&&this->tasks.empty()) return;
task=std::move(this->tasks.front());
tasks.pop();
}
task();
}
});
}
}
//停止线程,销毁工作线程
inline ThreadPool::~ThreadPool(){
{
std::unique_lock<std::mutex>lock(this->queue_mutex);
stop=true;
}
//唤醒所有线程,终止线程工作
condition.notify_all();
for(auto&worker:workers)
worker.join();
}
//调配任务
template<class F,class... Args>
auto ThreadPool::enterQueue(F&&f,Args&&... args)
->std::future<typename std::result_of<F(Args...)>::type>{
using return_type = typename std::result_of<F(Args...)>::type;
auto task= std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f),std::forward<Args>(args)...)
);
std::future<return_type>res=task->get_future();
{
std::unique_lock<std::mutex>lock(queue_mutex);
if(stop)
throw std::runtime_error("enterQueue on stopped ThreadPool");
tasks.emplace([task](){
(*task)();
});
}
condition.notify_one();
return res;
}
#endif
其中较难理解的是该函数段:
auto ThreadPool::enterQueue(F&&f,Args&&... args)
->std::future<typename std::result_of<F(Args...)>::type>{
~ ~ ~ ~ ~ ~
}
- 模板中template < class F, class… Args>的class… Args代表接受多个参数。
- 该函数返回类型是 auto,函数名是 enterQueue,形参表(F&& f, Args&&… args)中的&&为右值引用,后面紧跟 -> std::future< typename std::result_of< F(Args…)>::type>其实是c++11中的lambda表达式,代表函数的返回类型。
- std::future 是一个异步任务模板,可用来实现同步,包含在 < future>头文件中。通过 std::future 可以返回这个A类型的异步任务的结果。其中 std::result_of::type 就是这段代码中的A类型 ,result_of获取了F(Args…)的执行结果的类型。所以最后这个模板函数enqueue()的返回值类型就是F(Args…)的异步执行结果类型。
- using return_type = typename std::result_of<F(Args…)>::type; 功能类似typedef。将return_type声明为一个result_of< F(Args…)>::type类型,即函数F(Args…)的返回值类型。
-
auto task= std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f),std::forward<Args>(args)...)
是一个复杂的嵌套, make_shared <>() 开辟()个类型为<>的内存, packaged_task 把任务打包,这里打包的是 return_type , bind 绑定函数 f , 参数为 args… , forward 使()转化为<>相同类型的左值或右值引用,简单来说,这句话相当于把函数f和它的参数args…打包为一个模板内定义的task,便于后续操作,make_shared返回的是task的shared_ptr,用来管理内存。
- res = task->get_future(): 与模板函数的返回类型一致,是函数异步的执行结果。
-
tasks.emplace([task](){ (*task)(); }); 该代码是将该内存的任务放入队列中等待处理
#include<iostream>
#include<vector>
#include<chrono>
#include"threadPool_hard2.h"
int main(){
ThreadPool tpool(4);
std::vector<std::future<int>>results;
for(int i =0;i<8;i++){
results.emplace_back(
tpool.enterQueue([i](){
std::cout<<"hello"<<i<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout<<"word"<<i<<std::endl;
return i*i;
})
);
}
for(auto&&result:results){
std::cout<<result.get()<<" ";
}
std::cout<<std::endl;
return 0;
}