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;
解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。
范例:使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取。