19.6、C++11新特性有哪些⑥【并发】
C++11新特性有哪些
- 并发
- 线程(thread)
- 原子类型
- 互斥量(Mutex)
- 锁(lock)
并发
线程(thread)
- 创建线程对象后,必须要提供线程关联函数。两者都聚齐了,才能启动线程。线程函数一般情况下可按照以下四种方式提供:
- 函数指针
- lambda表达式
- 函数对象
- 包装器(function)
void ThreadFunc(int a) {
cout << "Thread1" << a << endl;
}
void T()
{
cout << "hello" << endl;
}
class TF
{
public:
void TT()
{
cout << "NI" << endl;
}
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([] {cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
thread t5(&TF::TT, TF());
//线程函数为包装器
function<void()> t = T;
thread t4(t);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
cout << "Main thread!" << endl;
return 0;
}
-
禁止拷贝线程对象:thread类 不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即一个线程对象关联的 线程的状态 转移给 其他线程对象,转移期间不影响线程的执行
-
线程函数参数
- 线程函数的参数是以 值拷贝 的方式 拷贝到线程栈空间中的,实际引用的是 线程栈中的拷贝,而不是外部实参。所以,即使参数是引用类型,最后也不能把改变后的结果带到外面。
- 注意:如果线程参数是类成员函数时,必须将this作为线程函数参数
-
可以通过 joinable()函数 判断线程是否是有效的,如果是以下任意情况,则线程无效:
- 线程对象没有关联函数
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用join 或者detach结束
原子类型
- 声明一个类型为T 的原子类型变量t:
atmoic<T> t; // 声明一个类型为T的原子类型变量t
-
特点:
-
如果是对原子类型的变量进行修改,就不需要加锁了。线程会自动地互斥访问原子类型
-
原子类型是不允许拷贝和赋值的,因为atmoic模板类中的拷贝构造、移动构造、赋值运算符重载都被删除掉了
-
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
-
实例:
该例中,使用原子变量后 就不再需要加锁了
互斥量(Mutex)
原子类型可以保证一个变量的安全性,但是对于一段代码的安全性,我们只能通过 互斥量和锁 来实现
-
普通互斥量(std:: mutex)
-
注意,线程函数调用 lock() 时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
-
线程函数调用 try_lock() 时,可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
-
-
递归互斥量(std:: recursive_mutex)
-
递归互斥锁 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权。释放互斥量时需要调用相同次数的 unlock()。除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
-
这对于递归函数 可能需要在同一线程中多次获取锁的情况很有用:
-
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex myRecursiveMutex;
void recursiveAccess(int depth) {
std::unique_lock<std::recursive_mutex> lock(myRecursiveMutex);
if (depth > 0) {
recursiveAccess(depth - 1);
}
// 访问共享资源的代码
std::cout << "Accessing shared resource at depth " << depth << "...\n";
}
int main() {
std::thread t1(recursiveAccess, 3);
t1.join();
return 0;
}
-
定时互斥量(std::timed_mutex)
-
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() :
- try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁 则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回false。
timed_mutex myMutex;
chrono::milliseconds timeout(100); //100毫秒
if (myMutex.try_lock_for(timeout))
{
//在100毫秒内获取了锁
//业务代码
myMutex.unlock(); //释放锁
}
else
{
//在100毫秒内没有获取锁
//业务代码
}
-
try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回false。
-
定时递归互斥量(std::recursive_timed_mutex)
允许同一线程多次获取锁,并提供了超时功能。与std::timed_mutex一样,std::recursive_timed_mutex也提供了try_lock_for()和try_lock_until()方法
锁(lock)
这里主要介绍两种RAII方式的锁封装(std::lock_guard和std::unique_lock),可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
-
std::lock_gurad 是 C++11 中定义的模板类,取代了mutex的lock和unlock函数。定义如下:
- lock_guard类模板通过RAII来封装。在需要加锁的地方,只需要用互斥量实例化一个lock_guard对象,此时类内调用构造函数成功上锁;出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
- lock_guard对象之间是不能拷贝和赋值的
- lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
-
独占锁(unique_lock)
与lock_guard不同的是,unique_lock更加的灵活(但效率上差一点,内存占用多一点),提供了更多的成员函数:
- 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
//1.立即上锁
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
//2.延迟上锁
std::unique_lock<std::mutex> lck(mtx, std::defer_lock); //只是创建对象,不上锁
lck.lock(); // 此时才上锁
if(lck.try_lock()){
// 已获得锁
}
lck.unlock();
- 修改操作:移动赋值、交换即swap(与另一个unique_lock对象互换所管理的互斥量所有权)、释放即release(返回它所管理的互斥量对象的指针,并释放所有权)
std::unique_lock<std::mutex> ul1(mtx);
std::unique_lock<std::mutex> ul2 = std::move(ul1); // 把ul1的锁转移到ul2上,即现在是ul1对mtx上锁
if (!ul1.owns_lock()) // 现在 ul1 应该不再持有锁了
{
assert(true);
}
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)
if(lck.owns_lock()){
// 此时拥有锁
}
auto* m = lck.mutex();