Linux C/C++服务器
C++11 新特性
C++11 中的thread、mutex、condition、function、bind等新特性,并使用这些新特性实现一个线程池
1. C++11 多线程
- linux的线程函数在pthread.h中,c++对linux线程函数进行了类封装,使用前需包含thread.h头文件
1.1 thread类
1.1.1 创建线程
std::thread t1(&func1); // 只传递函数
std::thread t2(func2, a, b); // 传入2个函数参数
std::thread t3(func3, std::ref(c)); // 函数参数传入引用
std::thread t4(A::func4, a4_ptr, 10); //传入类成员函数
1.1.2 成员函数
- get_id():获取线程ID
- joinable():判断线程是否可以加入等待
- join():等该线程执行完成后才返回
- detach():子线程的脱离函数
1.1.3 detach、move
detach:调用之后,目标线程驻留后台运行(类似nohup ./mian &)与之关联的std::thread对象失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权,主线程不需要等待该线程结束才结束(与join相反),当线程主函数执行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。
std::thread t5(&func5);
t5.detach(); //脱离
thread t6_1(func6);
thread t6_2(std::move(t6_1)); // 移动构造,t6_1 线程失去所有权
1.1.4 线程封装
zero_thread.h
#ifndef ZERO_THREAD_H
#define ZERO_THREAD_H
#include <thread>
class ZERO_Thread
{
public:
ZERO_Thread(); // 构造函数
virtual ~ZERO_Thread(); // 析构函数
bool start();
void stop();
bool isAlive() const; // 线程是否存活.
std::thread::id id() { return th_->get_id(); }
std::thread* getThread() { return th_; }
void join(); // 等待当前线程结束, 不能在当前线程上调用
void detach(); //能在当前线程上调用
static size_t CURRENT_THREADID();
protected:
void threadEntry();
virtual void run() = 0; // 运行
protected:
bool running_; //是否在运行
std::thread *th_;
};
#endif // ZERO_THREAD_H
1.3 互斥量
- c++11中的互斥量,是将linux的mutex封装为类
1.3.1 成员函数
- 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝
- lock(),调用线程将锁住该互斥量。
- unlock(), 解锁,释放对互斥量的所有权。
- try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。
1.3.2 基本使用
1.4 条件变量
linux中条件变量为pthread_cond_wait、pthread_cond_signal/pthread_cond_broadcast接口实现
C++11中使用wait、notify_one/notify_all成员函数函数实现,其实就是对linux接口的封装
条件变量使用过程:
- 拥有条件变量的线程获取互斥量;
- 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
- 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有等待线程。
1.4.1 成员函数
- wait函数
包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用unique_lock
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
工作原理:当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不能使用lock_guard对象;如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。
- notify_one函数
解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的。
void notify_one() noexcept;
- notify_all函数
解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。
void notify_all() noexcept;
1.4.2 条件变量示例
使用条件变量实现一个简单的多线程同步队列
condition示例
1.5 异步操作
- std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
- std::aysnc: 异步运行某个任务函数
- std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
- std::promise
1.5.1 std::aysnc和std::future
std::future期待一个返回,从一个异步调用的角度来说,future更像是执行函数的返回值;异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。
线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。
1.5.2 std::packaged_task
如果说std::async和std::feature还是分开看的关系的话,那么std::packaged_task就是将任务和feature绑定在一起的模板,是一种封装对任务的封装。
packaged_task示例
2. function、bind
在设计回调函数的时候,无可避免地会接触到可回调对象。在C++11中,提供了std::function和std::bind两个方法来对可回调对象进行统一和封装。
C++语言中有几种可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
2.1 function
2.1.1 基本用法
- 保存普通函数
void func1(int a)
{
cout << a << endl;
}
//1. 保存普通函数
std::function<void(int a)> func;
func = func1;
func(2); //2
- 保存成员函数
class A{
public:
A(string name) : name_(name){}
void func3(int i) const {cout <<name_ << ", " << i << endl;}
private:
string name_;
};
//3 保存成员函数
std::function<void(const A&,int)> func3_ = &A::func3;
A a("wsg");
func3_(a, 1);
2.2 bind
可将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
2.2.1 基本用法
auto f1 = std::bind(fun_1,1,2,3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
f1(); //print: x=1,y=2,z=3
auto f2 = std::bind(fun_1, placeholders::_1,placeholders::_2,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
f2(1,2);//print: x=1,y=2,z=3
auto f3 = std::bind(fun_1,placeholders::_2,placeholders::_1,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
//注意: f2 和 f3 的区别。
f3(1,2);//print: x=2,y=1,z=3
.....详细请查看下方链接
3. 可变参数列表
C++11的新特性--可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数
3.1 可变模板参数函数
可变参数模板语法
template <class... T>
void f(T... args);
上面的可变模版参数的定义当中,省略号的作用有两个:
- 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数;
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
#include <iostream>
using namespace std;
template <class... T>
void f(T... args)
{
cout << sizeof...(args) << endl; //打印变参的个数
}
int main()
{
f(); //0
f(1, 2); //2
f(1, 2.5, ""); //3
return 0;
}
3.2 如何展开参数包
3.2.1 递归函数方式展开参数包
#include <iostream>
using namespace std;
//递归终止函数
void print()
{
cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
cout << "parameter " << head << endl;
print(rest...);
}
int main(void)
{
print(1,2,3,"darren", "youzi");
return 0;
}
上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。
3.2.2 逗号表达式展开参数包
#include <iostream>
using namespace std;
template <class T>
void printarg(T t)
{
cout << t << endl;
}
template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), 0)...};
}
int main()
{
expand(1,2,3,4);
return 0;
}
这个例子将分别打印出1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
4. C++11 线程池
线程池的实现一般为mutex+condition实现方案,即锁加条件变量