使用C++11特性实现一个高效线程池

1. 前导知识(线程池涉及到的技术点):

       可变参数, std::future ,decltype ,packaged_task ,bind,支持可变参数列表, 支持获取任务返回值。

2. 线程池模式图:

 为了实现这个线程池,我们先来学习一下里面的技术点:

(1). C++多线程thread

C++std大部分都是封装的类(如:智能指针,线程),使用std::thread的时候需要包含<thread>

默认构造函数:

//创建一个空的 thread 执行对象。
thread() _NOEXCEPT
{ // construct with no thread
_Thr_set_null(_Thr);
}

如创建一个 thread t ,编译器会报错,程序会奔溃,因此不允许这样创建。

初始化构造函数:

//创建std::thread执行对象,该thread对象可被joinable,新产生的线程会调用threadFun函数,该函
数的参数由 args 给出
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);

这里面是一个模板函数,explicit修饰,只能显式调用,禁止编译器自动调用拷贝构造函数,参数都传入右值。如下面的代码:

using namespace std;
void thread_x(int x){

  cout<<"this is thread"<<"\t"<<x<<endl;
 return;

}

thread t(thread_x, ref(x));

这里的std::ref(x)的作用是把x转为右值。

拷贝构造函数:

// 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
thread(const thread&) = delete;
thread t1;
thread t2 =t1; // 错误

Move构造函数:

参考一下代码,即可学会。

#include<thread>
#include <iostream>

using namespace std;

void threadFun(int& a) // 引用传递
{
	cout << "this is thread fun !" << endl;
	cout << " a = " << (a += 10) << endl;
}
int main()
{
	int x = 10;
	thread t1(threadFun, ref(x));
	cout<<t1.get_id()<<endl;// 获取t1线程的id;
	//t1.join();//主线程将被阻塞,等到t1的完成
	thread t2(move(t1)); // t1 线程失去所有权
	cout << t2.get_id() << endl;
    thread t3;
	t3 = std::move(t2); // t2 线程失去所有权
	//t1.join(); 
	t3.join();
	cout << "Main End " << "x = " << x << endl;
	return 0;
}

(2)简单线程的创建

 参考一下的代码,就能全部弄得懂,建议复制到自己的IDE,去跑一下结果。

#include <iostream>
#include <thread> 

using namespace std;

// 1 传入0个参数
void func1()
{
    cout << "func1" << endl;
}


// 2 传入2个参数
void func2(int a, int b)
{
    cout << "func2:  a + b = " << a + b << endl;
}
//重载func2_1
void func2_1(int a, int b)
{
    cout << "func2_1:  a + b = " << a + b << endl;
}
//重载func2_1
int func2_1(string a, string b)
{
    cout << "func2_1: " << a << b << endl;
    return 0;
}

// 3 传入引用
void func3(int& c) // 引用传递,使用ref(a)转换参数
{
    cout << "func3: c = " << &c << endl;
    c += 10;
}

//类A
class A
{
public:
    //    4. 传入类函数
    void func4(int a)
    {
        cout << "thread:" << name_ << ", fun4 a = " << a << endl;
    }
     int func4(string str)
    {
        cout << "thread:" << name_ << ", fun4 str = " << str << endl;
        return 0;
    }
    void setName(string name) {
        name_ = name;
    }
    void displayName() {
        cout << "this:" << this << ", name:" << name_ << endl;
    }
    void play()
    {
        std::cout << "play" << std::endl;
    }
private:
    string name_;
};

//5. detach
void func5()
{
    cout << "func5 into sleep " << endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "func5 leave " << endl;
}

// 6. move
void func6()
{
    cout << "this is func6 !" << endl;
}


int main()
{
    //1. 传入0个值
      cout << "\n\n main1--------------------------\n";
    std::thread t1(func1);  // 只传递函数
    t1.join();  // 阻塞等待线程函数执行结束




    //2. 传入2个值
    cout << "\n\n main2--------------------------\n";
      int a =10;
      int b =20;
     std::thread t2(func2, a, b); // 加上参数传递,可以任意参数
     t2.join();

    int a1 = 10;
    int b1 = 20;
    std::thread t2_1((void(*)(int, int)) func2_1, a1, b1); // 加上参数传递,可以任意参数
    t2_1.join();

    std::thread t2_2((int(*)(string, string)) func2_1, "darren", " and mark"); // 加上参数传递,可以任意参数
    t2_2.join();


    //  3. 传入引用
     cout << "\n\n main3--------------------------\n";
     int c =10;
    std::thread t3(func3, std::ref(c)); // std::ref 加上参数传递,可以任意参数
    t3.join();
    cout << "main3 c = " << &c << ", "<<c << endl;

        //  4. 传入类函数
    cout << "\n\n main4--------------------------\n";
     A * a4_ptr = new A();
    a4_ptr->setName("Name1");
    std::thread t4((void(A::*)(int)) & A::func4, a4_ptr, 10);
    t4.join();
    delete a4_ptr;

        // 重载
    A* a4_ptr2 = new A();
    a4_ptr2->setName("name1");
    std::thread t41((void(A::*)(int)) & A::func4, a4_ptr2, 100);      // 重载void func4(int a)
    t41.join();
    delete a4_ptr2;

    // 重载
    A* a4_ptr3 = new A();
    a4_ptr3->setName("name2");
    std::thread t43((int(A::*)(string)) & A::func4, a4_ptr3, "and mark"); // 重载 int func4(string str)
    t43.join();
    delete a4_ptr3;
   
    //   5.detach
    cout << "\n\n main5--------------------------\n";
    std::thread t5(&func5);  // 只传递函数
    t5.detach();  // 脱离
     cout << "pid: " << t5.get_id() << endl; // t5此时不能管理线程了
    cout << "joinable: " << t5.joinable() << endl; // false
   // t5.join(); t5已成为后台进程,在join将捕捉不到,程序报错
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 如果这里不休眠会怎么样
   cout << "\n main5 end\n";
    // 6.move
    cout << "\n\n main6--------------------------\n";
    int x = 10;
    thread t6_1(func6);
    thread t6_2(std::move(t6_1)); // t6_1 线程失去所有权
   // t6_1.join();  // 抛出异常   after throwing an instance of 'std::system_error'
    t6_2.join();

    return 0;
}

(3).互斥量mutex

C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件<mutex>中,所以如果使用 std::mutex,就必须包含 头文件<mutex>。C++11提供如下4种语义的互斥量(mutex):

std::mutex,独占的互斥量,不能递归使用。(开发中主要使用)

std::time_mutex,带超时的独占互斥量,不能递归使用。

std::recursive_mutex,递归互斥量,不带超时功能。

std::recursive_timed_mutex,带超时的递归互斥量。

std::mutex 构造函数:std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没 有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当 前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁 住,则会产生死锁(deadlock)。

unlock(), 解锁,释放对互斥量的所有权。 try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

(3)lock_guard和unique_lock的使用和区别

相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式。 RAII:也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证 在任何情况下,使用对象时先构造对象,最后析构对象。

使用:

#include <iostream>      
#include <thread>        
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error

std::mutex mtx;//互斥锁

void print_even(int x) {
    if (x % 2 == 0) std::cout << x << " is even\n";
    else throw (std::logic_error("not even"));
}

void print_thread_id(int id) {
    try {
        //这里的lock_guard换成unique_lock是一样的。
        std::unique_lock<std::mutex> lck(mtx);//把互斥锁锁住
        print_even(id);
    }
    catch (std::logic_error&) {
        std::cout << "[exception caught]\n";
    }
}

int main()
{
    std::thread threads[10];
 
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_thread_id, i + 1);

    for (auto& h : threads) h.join();

    return 0;
}

区别:

unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。 unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁, lck.lock()进行上锁,而不必等到析构时自动解锁。

可以运行一下下面的代码,对比学习,挺有趣的。

#include <iostream>
#include <deque>//双向队列,底层为双向链表
#include <thread>
#include <mutex>
#include <condition_variable>//条件变量
#include <Windows.h>
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
int count = 0;
void fun1() {
	while (true) {
		{
		std::unique_lock<std::mutex> locker(mu);
		q.push_front(count++);
		//locker.unlock(); // 这里是不是必须的?
		cond.notify_one();
	     }
		Sleep(1);
	}
}
void fun2() {
	while (true) {
		std::unique_lock<std::mutex> locker(mu);
		cond.wait(locker, []() {return !q.empty(); });
		auto data = q.back();
		q.pop_back();
		std::cout << "thread2 get value form thread1: " << data << std::endl;
	}
}
int main() {
	std::thread t1(fun1);
	std::thread t2(fun2);
	t1.join();
	t2.join();
	return 0;
}

条件变量的目的就是为了,在没有获得某种提醒时长时间休眠; 如果正常情况下, 我们需要一直循环 (+sleep), 这样的问题就是CPU消耗+时延问题,条件变量的意思是在cond.wait这里一直休眠直到 cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程

那么为什么必须使用unique_lock呢?

原因: 条件变量在wait时会进行unlock再进入休眠, lock_guard并无该操作接口 wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则 wait函数返回否则释放锁继续休眠 notify: 进行notify动作并不需要获取锁

使用场景:需要结合notify+wait的场景使用unique_lock; 如果只是单纯的互斥使用lock_guard

lock_guard 1.std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。 2.锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根 据自己的场景编写resource_guard RAII类,避免忘掉释放资源。 std::unique_lock 1. unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与 条件变量一同使用。 2. unique_lock比lock_guard使用更加灵活,功能更加强大。 3. 使用unique_lock需要付出更多的时间、性能成本。

(4)条件变量

互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供有力的支持,这就是条件变量。条件变量位于头文件<condition_variable>下。

条件变量使用过程:

1. 拥有条件变量的线程获取互斥量; 2. 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行; 3. 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有等待线程。 条件变量提供了两类操作:wait和notify。这两类操作构成了多线程同步的基础。

成员函数:

(1).wait函数   函数原型

void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用 unique_lock.

wait函数的工作原理:

当前线程调用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,则进行后面的操作。

(2). notify_one函数   函数原型

void notify_one() noexcept;

解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等的线程多余一个,则唤醒的线程是不确定的。

(3).notify_all函数 函数原型

void notify_all() noexcept;

解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。

范例:使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值