博客园  :: 首页  :: 新随笔  :: 管理

1.3.2 C++11 线程池

Posted on 2023-04-15 14:34  wsg_blog  阅读(142)  评论(0编辑  收藏  举报

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

cpp_thread示例

1.3 互斥量

  • c++11中的互斥量,是将linux的mutex封装为类

1.3.1 成员函数

  • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝
  • lock(),调用线程将锁住该互斥量。
  • unlock(), 解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。

1.3.2 基本使用

mutex示例

1.4 条件变量

linux中条件变量为pthread_cond_wait、pthread_cond_signal/pthread_cond_broadcast接口实现
C++11中使用wait、notify_one/notify_all成员函数函数实现,其实就是对linux接口的封装

条件变量使用过程:

  1. 拥有条件变量的线程获取互斥量;
  2. 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
  3. 某个线程满足条件执行完之后调用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代表的是一次性事件。

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);

function示例

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
.....详细请查看下方链接

bind 示例

3. 可变参数列表

C++11的新特性--可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数

3.1 可变模板参数函数

可变参数模板语法

template <class... T>
void f(T... args);

上面的可变模版参数的定义当中,省略号的作用有两个:

  1. 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数;
  2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
#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实现方案,即锁加条件变量