C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是
,
,
,
和
。
:提供原子操作功能,该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。(atomic
:线程模型封装,该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
:互斥量封装,该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
:条件变量,该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
:实现了对指定数据提供者提供的数据进行异步访问的机制。该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。
(1)线程私有的资源
线程id,寄存器(cpu进行计算必须),栈,优先级调度策略...
为什么使用线程:
线程共享文件和内存简单,进程共享比较复杂;
线程的上下文切换消耗资源比进程少;
线程的cpu利用率更高
进程是程序之间的并发执行,线程是同一程序的片段之间并发执行;
(2)多线程中锁的种类
互斥锁(mutex):pthread_mutex_t类型,支持以下几种方法:
pthread_mutex_lock(pthread_mutex_t* mutex);
pthread_mutex_trylock(pthread_mutex_t* mutex);
pthread_mutex_unlock(pthread_mutex_t* mutex);
pthread_mutex_timelock(pthread_mutex_t* mutex, time)
递归锁:支持递归调用
读写锁(共享互斥锁):pthread_rwlock_t类型,支持以下几种方法:
pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
自旋锁:自旋等待,当锁被其他线程占用时,一直忙等(不停尝试获取)阻塞;
而互斥锁会投入睡眠。(下次唤醒的时候伴随着调度成本)
适用情况:锁持有时间短,不想在重新调度上花费时间
(3)线程创建:
C语言:
pthread_create函数,传入函数指针;
C++:
①函数指针:thread t1(func,args[1],args[2]....);
②函数对象:
先将func声明成一个对象,参数为其成员变量,operator()重载,就可以调用:
thread t1(func(args[1],args[2]...));
(4)线程终止:
如果进程调用了exit,那么该进程的所有线程都会终止;
linux中线程终止方式:
①启动函数中返回
②被其他线程取消
③调用pthread_exit
但是在C++标准中,没有线程之间互相终止的机制,一个线程要想终止另外一个线程,可以通过设置一个共享变量,然后让另外一个线程定期检查该变量值,实现两者之间的通信。
调用join函数可以获取一个线程的终止状态,该函数会阻塞直到该线程终止
(5)如何从线程中获取结果
①通过传入结果变量引用或者指针
②使用future
(6)C++中互斥体类的使用
非定时互斥体:std::mutex类和std::recursive_mutex(递归锁)
这两个类支持:lock()、try_lock()、unlock()方法
定时互斥体:std::timed_mutex类、std::recursive_timed_mutex和std::shared_timed_mutex
定时互斥体都支持方法:try_lock_for(time),在给定时间内获得锁,超时返回false
特别的,读写锁shared_timed_mutex除了支持上面的方法取得独占权限之外,还能获得共享拥有权:
lock_shared()、try_lock_shared()、unlock_shared()...
(7)C++锁类的使用
锁类是可选的(使用锁类更加安全),为了更加方便的获得、释放互斥体上的锁(RAII自动析构时释放资源,异常的时候能够释放)
std::lock_guard、std::unique_lock和std::shared_lock
锁类接受一个互斥体类对象进行构造,在该互斥体上进行锁操作
lock_guard
lock的使用:
unique_lock lock1( from.mMutex, defer_lock );//defer_lock表示延迟加锁,此处只管理mutex
unique_lock lock2( to.mMutex, defer_lock );
lock( lock1, lock2 );//lock一次性锁住多个mutex防止deadlock
(8)避免死锁的方法
①控制加锁的顺序,所有线程按照相同顺序加锁(要求事先知道将要用到的锁)
②加锁时限,获取锁的时候加上时限,如果超时(有可能死锁了),放弃获取该锁,并释放已有的锁,一段时间之后再次尝试
(9)条件变量——一种显式的线程间通信
条件本身由互斥量保护,C++11中有两类条件变量:
①condition_variable:等待unique_lock
notify_one();唤醒等待该条件变量线程中的一个;
notify_all();唤醒所有
wait(unique_lock
还可以给wait函数一个时间期限,在该期限之后解除阻塞
②condition_variable_any:等待任意锁对象
(10)线程数量
对于计算密集型任务,在拥有Ncpu个处理器的系统上,当线程池大小为N+1时,通常能实现最优的利用率,(即当计算密集型任务偶尔由于页缺失故障或者其他原因而暂停时,这个额外的现线程也能够确保CPU的时钟周期不会被浪费。)
(11)生产者消费者模型
基于条件变量同步如下:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
mutex mtx; //互斥锁
const int bufferSize = 10; //缓冲区的大小
condition_variable not_full; //非满条件变量
condition_variable not_empty; //非空条件变量
queue buf;
int data = 0 ;
void produce()
{
while(true)
{
unique_lock pLock(mtx);
while(buf.size() == bufferSize) //如果为满,则等待消费者消费
not_full.wait(pLock);
data = ++data % 1000;
buf.push(data);
cout << "生产了一个产品:" << data < cLock(mtx);
while(buf.size() == 0)
not_empty.wait(cLock); //如果为空,等待生产者生产
int data = buf.front();
buf.pop();
cout << "消费了一个产品:" << data << endl;
not_full.notify_all(); //唤醒生产者
cLock.unlock();
chrono::milliseconds dura(1000);
this_thread::sleep_for(dura);
}
}
int main()
{
vector threads;
for (int i = 0; i < 5; i++){/* 生产者线程 */
threads.push_back(thread(produce));
}
for (int i = 5; i < 10; i++){/* 消费者线程 */
threads.push_back(thread(consumer));
}
for (auto& t : threads){/* 等待所有线程的退出 */
t.join();
}
return 0;
}
安全序列是指一个进程序列{P1,…,Pn}是安全的,即对于每一个进程Pi(1≤i≤n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和。(即在分配过程中,不会出现某一进程后续需要的资源量比其他所有进程及当前剩余资源量总和还大的情况)
注:存在安全序列则系统是安全的,如果不存在则系统不安全,但不安全状态不一定引起死锁。
(13)多线程面试题
为什么要用多线程?充分发挥多核的优势。
单核CPU下的多线程。防止IO阻塞,所以在IO密集型作业中,多线程还是要快。但是,如果是CPU计算密集,由于线程环境切换,可能会带来额外的消耗。