前言
一、常见的锁
按功能分类
互斥锁/独占锁/排他锁
std::mutex
互斥变量,申请内核锁。
std::lock_guard
在std::lock_guard变量创建时内 加锁,生命周期结束就释放锁。俗称c++ RAII 资源管理机制
std::unique_lock
std::unique_lock为锁管理模板类,是对通用mutex的封装。std::unique_lock对象以独占所有权的方式(uniqueowership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;
而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。
unique_lock具有lock_guard的所有功能,而且更为灵活。
虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
递归锁/嵌套锁/可重入锁
在不同的线程中调用已锁定的互斥锁,必然会导致死锁。
但是在同一个线程中,如果想要多次获得一个锁,只能使用递归锁。
举例来说,如果函数A,B都有枷锁逻辑;而在特殊条件下,函数A用调用了函数B;
则需要用递归锁。
应尽量避免使用递归锁,好的设计会避免锁的多次使用
std::recursive_mutex
std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
自旋锁
自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。
共享锁
c++ 17才有
boost:share_mutex
读写锁
原子锁
std::atomic
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
可参考 c++语言:从放弃到入门 <一> c++11新关键字以及引入的新特性 std::atomic
std::atomic_flag
std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:
test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false
clear. 清楚atomic_flag对象
std::atomic_flag可用于多线程之间的同步操作,类似于linux中的信号量。使用atomic_flag可实现mutex.
同步锁
按特征分类
悲观锁
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
二、常见性质
原子性
三、常见的概念
互斥量
信号量
条件变量
std::condition_variable
基本用法:
//创建条件变量和互斥锁 std::condition_variable m_notification; std::mutex m_mutex; ... //添加队列数据时加锁,并唤起一个正在阻塞wait()的线程 std::unique_lock<m_mutex> autoLock(m_mutex); ... m_notification.notify_one();//唤起一个正在阻塞wait()的线程 ... //当当前队列没有数据时 阻塞 std::unique_lock<m_mutex> autoLock(m_mutex); m_notification.wait(autoLock);//把当前线程阻塞
当消费者线程调用wait时 线程进入阻塞状态,当调用notify_one唤醒消费者线程,线程进入可运行状态。
std::condition_variable_any
互斥问题
比如多个线程对公共资源的使用管理问题,即互斥问题(Exclusive);
同步问题
各种依赖关系,比如线程1的工作要基于线程2的工作才能执行,即同步问题(Synchronization)。
三、线程状态转移
初始状态
仅是在语言层面创建了线程对象,还未与操作系统线程关联。
类似c++创建线程状态
std::thread hStartFun;
可运行状态(就绪状态)
(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行,等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
阻塞状态(等待状态/挂起状态)
当发生如下情况时,线程将进入阻塞状态:
1、线程调用了sleep()方法主动放弃其所占用的处理器资源;Sleep()
2、线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞;WaitForSingleObject()
3、线程试图调用一个锁对象,但该锁对象整备其他线程所持有的;try_lock()
4、线程正在等待某个通知 wait()
当前正在执行的线程被阻塞之后,其他线程就可以后的执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态,而不是运行状态。也就是说,被阻塞线程的阻塞解除之后,必须重新等待线程调度再次调度它。
针对上面几种情况,当发生如下特定的情况时可以解除阻塞,让该线程重新进入就绪状态。
1、调用sleep()方法的线程超过了指定的时间;
2、线程调用的阻塞式I/O方法已经返回;
3、线程成功的获得了试图获取的锁对象;
4、线程正在等待某个通知时,其他线程发出了一个通知。
运行状态
指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换。
终止状态
表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
四、使用多线程基本准则
1.尽可能的减小锁定的区域
互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。
五、多线程常用场景
1.生产者消费者队列
push/pop时都需要加锁
总结
1.互斥锁和自旋锁
互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
互斥锁缺点:
(1)等待互斥锁会消耗时间,等待延迟会损害系统的可伸缩性。
(2)优先级倒置。低优先级的线程可以获得互斥锁,因此会阻碍需要同一互斥锁的高优先级线程。
(3)锁护送(lock convoying)。如果持有互斥锁的线程分配的时间片结束,线程被取消调度,则等待同一互斥锁的其它线程需要等待更长时间。
2.sleep和wait
当线程调用了wait()方法时,它会释放掉对象的锁.
sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的.