所学知识参考链接:
C++11 thread, C++11 mutex:https://wyqz.top/p/2668140628.html
C++11 左右值与移动构造函数:http://avdancedu.com/a39d51f9/
- 实现多线程安全的任务队列,线程池使用异步操作,提交(submit)使用与thread相同。
- 内部利用完美转发获取可调用对象的函数签名,lambda与function包装任务,使用RAII管理线程池的生命周期。
本GItHub仓库代码的大概思路:
- 当访问到队列的大小时,属于读操作,可以多个线程同时读,故加共享锁
- 当需要修改队列的大小时,属于写操作,只能一个线程进行,故加独占锁
内有线程池构造函数,提交任务函数,析构函数的实现
- 构造函数
将线程工作类传入到n
个线程,使n
个线程能够同时工作。
- 任务提交函数
参数为可变参数,传入一个线程的执行函数以及该函数所需要的所有参数。
首先先将【执行任务(函数)和对应的参数】包装成一个func
函数,此函数无参数,返回值类型为执行任务的返回值类型
- 解决线程的创建和销毁问题(代价问题)
- 解决CPU和IO速度不匹配的问题
- 充分利用多核CPU资源,提高并发效率
线程池: 当进行并行的任务作业操作时,线程的建立与销毁的开销是,阻碍性能进步的关键,因此线程池,由此产生。使用多个线程,无限制循环等待队列,进行计算和操作。帮助快速降低和减少性能损耗。
主要组成:任务队列、执行队列、管理组件
- 线程池管理器:初始化和创建线程,启动和停止线程,调配任务;管理线程池
- 工作线程:线程池中等待并执行分配的任务
- 任务队列:用于存放没有处理的任务,提供一种缓冲机制,同时具有调度功能,高优先级的任务放在队列前面
- 没有任务执行,缓冲队列为空
- 队列中任务数量小于等待线程池中线程任务的数量
- 任务数量大于等待线程池数量,缓冲队列未满
- 任务数量大于线程池数量,缓冲队列已满
其实你线程池容量定义的大小,就是线程池的缓冲队列。
ThreadPool pool(8)
那缓冲队列大小为8,等待队列也就是代码中的安全队列。向线程池中提交任务时,会向队列中添加任务,然后唤醒一个工作线程(因为缓冲队列中为空,也就是执行的线程小于缓冲队列的大小时,有空闲的工作线程在条件变量的队列上阻塞等待,故需要进行唤醒)。
C++ 内存分四大块:
-
全局 主函数运行前使用,初始化
-
静态 变量第一次使用前,初始化
以上两块内存都会在程序结束后自动释放
-
堆区 由程序员管理,C++管理方法有new delete等关键字
-
栈区 由编译器管理,存放程序的局部变量和参数
因此我们需要关注堆区的内存管理。内存管理经常会碰到忘记释放造成的内存泄露。
在C++中引入了智能指针,有shared_ptr
,unique_ptr
和weak_ptr
。
使用智能指针,需要引入头文件,shared_ptr
顾名思义是多个指针指向一块内存。
被管理对象有一个引用计数,这个计数记录在每个指针上,几个shared_ptr
指向它,这个数字就是几,当没有任何shared_ptr
指向它时,引用计数为0,这时,自动释放对象。
其功能为:当所有指针都释放(或是不再指向对象)的时候,自动释放对象
// 鼓励使用make_shared创建对象
auto ptr = make_shared<int>(); // 分配堆空间,创建智能指针
std::packaged_task<>
链接了future
对象与函数(或可调用对象)。
std::packaged_task<>
对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为future
的内部数据,并令future
准备就绪。它可作为线程池的构件单元,亦可用于其他任务管理方案。
例如,为各个任务分别创建专属的独立运行的线程,或者在某个特定的后台线程上依次执行全部任务。若一项庞杂的操作能分解为多个子任务,则可把它们分别包装到多个
std::packaged_task<>
实例之中,再传递给任务调度器或线程池。这就隐藏了细节,使任务抽象化,让调度器得以专注处理std::packaged_task<>
实例,无须纠缠于形形色色的任务函数。
std::packaged_task<>
是类模板,其模板参数是函数签名
std::packaged_task
对象是可调用对象,我们可以直接调用,还可以将其包装在std::function
对象内,当作线程函数传递给std::thread
对象,也可以传递给需要可调用对象的函数。
对于std::packaged_task<void()>
来说,它表示任务,包装某个函数(或可调用对象),这个函数不接收参数,返回void
(倘若真正的任务函数返回任何其他类型的值,则会被丢弃)。
- 在构造函数中加锁,即定义时
- 在析构函数中解锁,判断锁状态,如果已经解锁的话,不操作,未解锁的话解锁
C++17共享锁
将左值或右值
转换为右值
- future表示一个异步任务结果
Future模式可以理解成:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情(期间future可能一直在后台执行这个任务)。一段时间之后,我就便可以从Future那儿取出结果。
Future里面有个方法 get()
是个阻塞等待方法。当线程池一次性submit多个任务的时候。只有所有的任务全部完成,我们才能用GET按照任务的提交顺序依次返回结果。
std::future
提供了一个重要方法就是.get()
,这将阻塞主线程,直到future就绪。注意:.get()
方法只能调用一次。
std::future
不支持拷贝,支持移动构造。c++提供的另一个类std::shared_future
支持拷贝。
可以通过下面三个方式来获得std::future
。
std::promise
的get_future函数std::packaged_task
的get_future函数std::async
函数
C++11标准
哪个线程调用这个方法,就让调用此方法的线程进入阻塞状态,等待此线程执行完毕之后,再往下执行
若当前线程未运行完毕,继续运行该线程,并阻塞主进程;直到该线程执行完毕再往下执行
条件变量是需要和一个互斥锁mutex配合使用,调用wait()
之前应该先获得mutex,当线程调用 wait() 后将被阻塞,当wait陷入休眠是会自动释放mutex。直到另外某个线程调用 notify_one或notify_all唤醒了当前线程。当线程被唤醒时,此时线程是已经自动占有了mutex。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
- 一个线程因等待"条件变量的条件成立"而挂起;
- 另外一个线程使"条件成立",给出信号,从而唤醒被等待的线程。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是std::mutex
,并且管理这个锁只能是 std::unique_lock
RAII模板类。
线程的阻塞是通过成员函数wait()/wait_for()/wait_until()
函数实现的。
void wait(std::unique_lock<std::mutex>& lock);
//Predicate 谓词函数,可以普通函数或者lambda表达式
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
以上的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock()
,以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。
notify_one
若任何线程在 *this
上等待,则调用 notify_one 会解阻塞(唤醒)等待线程之一。因为只唤醒等待队列中的第一个线程;不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用notify_one()
或者notify_all()
。
notify_all
会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。其他未获得锁的线程继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。
参考:https://blog.csdn.net/weixin_42108411/article/details/110138238
在正常情况下,wait类型函数返回时要么是因为被唤醒,要么是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。
指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值,(此时的右值引用又被称为“万能引用”)。
通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。
C++11 标准的开发者已经帮我们想好了解决方案,该新标准还引入了一个模板函数 forword<T>()
RAII,全称资源获取即初始化,C++语言的一种管理资源、避免泄漏的惯用法
RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求
下,只要对象能正确地析构,就不会出现资源泄漏问题。
当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;
当这个变量是类对象时,这个时候,就会自动调用这个类的析 构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。RAII就是这样去完成的。
由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。
在wait
操作之前,需要进行条件判断,而此条件属于临界资源,需要在访问前加锁,此时为保护临界资源的需要。
当条件不满足时,wait操作将执行两个步骤,并保证两个操作的原子性:
- 将当前线程加入条件信号的等待队列
- 解开作为参数传入的锁,解除对临界资源的锁定
主函数中,我们定义了一个线程池,当主函数运行完时,线程池将销毁,执行析构函数,那么线程池就会关闭,不管等待队列里面是否存在线程,整个线程池将会关闭,那么等待队列里面的线程将无法执行。
当我们提交一个空任务时,同时调用了get
方法。get()
是个阻塞等待方法,只有当任务执行完毕有返回结果后,才会取消阻塞,并得到返回结果。
而任务执行的顺序是任务的提交顺序,所以最后提交一个空任务,必须等待前面所有的任务执行完毕有返回结果,此空任务才会执行,相当于一直阻塞,直至所有提交任务执行完毕。