C++11线程库
Posted 阿尔帕兹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11线程库相关的知识,希望对你有一定的参考价值。
C++11线程库
本质是对不同平台的线程库进行封装。因为windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含<thread>
头文件。
thread类
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联线程函数,即没有启动任何线程 |
thread(fn, args1, args2…) | 构造一个线程对象,并关联线程函数fn,argsx为线程函数的参数 |
get_id() | 获取线程id |
joinable() | 线程是否还在执行,joinable表示的是一个正在执行中的线程 |
join() | 主线程调用该函数后会阻塞等待另一个线程返回 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
this_thread命名空间
get_id是需要对象去调用的函数,当成成员函数不太好用,故可写为std::this_thread::get_id()
直接调用。this_thread
是个命名空间用于访问当前进程的属性。
默认构造函数
比如要创建一个线程池,但不清楚要创建多少个线程,就可以用thread()
函数。
int n = 10;//线程个数
int m = 10;//每个线程跑m次
vector<thread> v_t;//创建n个线程对象,但每个线程都是空的
v.resize(n);//会调用线程的默认构造函数
for (auto& t : v_t)
t = thread([m]
for (size_t i = 0; i < m; ++i)
cout << std::this_thread::get_id() << " 跑" << endl;
);
for (auto& t : v_t)
t.join();
初始化线程
线程函数一般情况下可按照以下三种方式提供:函数指针;lambda表达式;函数对象。
线程函数的参数是以值拷贝的方式拷贝到线程独立栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
拷贝构造函数=delete
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,就是将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。
可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:采用无参构造函数构造的线程对象;线程对象的状态已经转移给其他线程对象;线程已经调用join或者detach结束。
注意,2个线程同时对一个变量int n = 0;
进行++操作时,假设每个线程循环100次,只有可能小于期望数(200),不可能大于期望数(200)。且当循环次数越大时,错误越明显。
int val = 0;
mutex mtx;
void fun(int num)
for (int i = 0; i < num; ++i)
++val;
int main()
//两个线程可以调用同一个函数的原因:因为该函数是共享的,fun函数编译好以后是放在在主线程的代码段里,两个从线程都可以调用
thread t1(fun, 100);
thread t2(fun, 100);
t1.join();
t2.join();
cout << val;//结果val<=200
return 0;
注意fun里的加锁位置,放在for循环外面就是串行执行了,不过一个线程一旦申请成功,知道循环结束后才会释放锁,一个线程只需要一次加锁和解锁;放在for循环里面会导致频繁地切换上下文,加锁和解锁的次数跟循环次数相同了,增加了系统消耗。
使fun()函数线程安全的3种方法
//---------放在for循环外----------//运行速度快
void fun(int num)
mtx.lock();
for (int i = 0; i < num; ++i)
++val;
mtx.unlock();
//---------放在for循环里----------//运行速度更慢
void fun(int num)
for (int i = 0; i < num; ++i)
mtx.lock();
++val;
mtx.unlock();
//---------让++变成原子操作----------//
#include <atomic>
atomic_int val 0 ;
void fun(int num)
for (int i = 0; i < num; ++i)
++val;
原子性操作库
底层是靠CAS(compare and swap)来解决,windows和linux底层都有CAS。CAS操作包含3个操作数:1、内存位置V;2、预期原值A;3、新值B。若内存位置的值与预期原值匹配,那么处理器会自动将该位置更新为新值;否则,处理器不做任何处理。现在几乎所有的CPU指令都支持CAS的原子操作。
在C++11中,若提前声明该变量为原子性的,那么程序员不需要对原子类型变量进行加锁解锁操作,线程就能够对原子类型变量互斥访问。更方便的是,程序员可以使用atomic类模板,定义出需要的任意原子类型。
atomic<T> t; // 声明一个类型为T的原子类型变量t
//atomic_int val 0 ;
//atomic<int> val = 0;
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
mutex互斥锁
std::recursive_mutex:递归里不可以用普通互斥锁。有专门的递归互斥锁,recursive_mutex(实现原理是根据线程id判断该线程是否已经加锁,已加锁则不执行,未加锁就加锁)。
std::timed_mutex:比 std::mutex 多了两个成员函数,try_lock_for()【接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,在时间范围内还是没有获得锁,返回 false;否则该线程可以获得对互斥量的锁】,try_lock_until() 【接受一个时间点作为参数,在指定时间内还是没有获得锁,返回 false;否则该线程可以获得对互斥量的锁】。
lock_gurad
RAII。以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。
实例化一个lock_guard对象,调用构造函数则表示成功上锁;出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
C++11 unique_lock
RAII。以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。unique_lock更加的灵活。
- 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock;
- 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权);
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)
condition_variable
wait需要搭配unique_lock使用,pred一般传gelambda表达式
void wait (unique_lock<mutex>& lck);
//------------
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);==>相当于 while (!pred()) wait(lck);
notify_one在linux里就是signal,notify_all对于broadcast。
面试题:两个线程轮流打印奇数和偶数
面试题:并发与并行的区别
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发和并行的区别:并发,指的是多个事情,在同一时间段内同时发生了。并行,指的是多个事情,在同一时间点上同时发生了。并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的、只有在多CPU的情况中,才会发生并行。
C++11 ——— 线程库
文章目录
线程库
在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。
线程库(thread)
线程对象的构造方式
一、调用无参的构造函数
thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程。比如:
thread t1;
由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象。比如:
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
int main()
thread t1;
//...
t1 = thread(func, 10);
t1.join();
return 0;
场景: 实现线程池的时候就是需要先创建一批线程,但一开始这些线程什么也不做,当有任务到来时再让这些线程来处理这些任务。
二、调用带参的构造函数
thread的带参的构造函数的定义如下:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
参数说明:
fn
:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。args...
:调用可调用对象fn时所需要的若干参数。
调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。比如:
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
int main()
thread t2(func, 10);
t2.join();
return 0;
三、调用移动构造函数
thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象。比如:
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
int main()
thread t3 = thread(func, 10);
t3.join();
return 0;
说明一下:
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
- 如果创建线程对象时没有提供线程函数,那么该线程对象实际没有对应任何线程。
- 如果创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行。
- thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
thread提供的成员函数
thread中常用的成员函数如下:
成员函数 | 功能 |
---|---|
join | 对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞 |
joinable | 判断该线程是否已经执行完毕,如果是则返回true,否则返回false |
detach | 将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待 |
get_id | 获取该线程的id |
swap | 将两个线程对象关联线程的状态进行交换 |
此外,joinable
函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:
- 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
- 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
- 线程已经调用join或detach结束。(线程已经结束)
获取线程的id的方式
调用thread的成员函数get_id
可以获取线程的id,但该方法必须通过线程对象来调用get_id
函数,如果要在线程对象关联的线程函数中获取线程id,可以调用this_thread
命名空间下的get_id
函数。比如:
void func()
cout << this_thread::get_id() << endl; //获取线程id
int main()
thread t(func);
t.join();
return 0;
this_thread
命名空间中还提供了以下三个函数:
函数名 | 功能 |
---|---|
yield | 当前线程“放弃”执行,让操作系统调度另一线程继续执行 |
sleep_until | 让当前线程休眠到一个具体时间点 |
sleep_for | 让当前线程休眠一个时间段 |
线程函数的参数问题
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:
void add(int& num)
num++;
int main()
int num = 0;
thread t(add, num);
t.join();
cout << num << endl; //0
return 0;
如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:
方式一:借助std::ref函数
当线程函数的参数类型为引用类型时,如果要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref函数保持对实参的引用。比如:
void add(int& num)
num++;
int main()
int num = 0;
thread t(add, ref(num));
t.join();
cout << num << endl; //1
return 0;
方式二:地址的拷贝
将线程函数的参数类型改为指针类型,将实参的地址传入线程函数,此时在线程函数中可以通过修改该地址处的变量,进而影响到外部实参。比如:
void add(int* num)
(*num)++;
int main()
int num = 0;
thread t(add, &num);
t.join();
cout << num << endl; //1
return 0;
方式三:借助lambda表达式
将lambda表达式作为线程函数,利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉,此时在lambda表达式中对形参的修改也能影响到外部实参。比如:
int main()
int num = 0;
thread t([&num]num++; );
t.join();
cout << num << endl; //1
return 0;
join与detach
启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。thread库给我们提供了如下两种回收线程资源的方式:
join方式
主线程创建新线程后,可以调用join函数等待新线程终止,当新线程终止时join
函数就会自动清理线程相关的资源。
join
函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join
,否则程序会崩溃。比如:
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
int main()
thread t(func, 20);
t.join();
t.join(); //程序崩溃
return 0;
但如果一个线程对象join
后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可以调用一次join
。比如:
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
int main()
thread t(func, 20);
t.join();
t = thread(func, 30);
t.join();
return 0;
但采用join
的方式结束线程,在某些场景下也可能会出现问题。比如在该线程被join
之前,如果中途因为某些原因导致程序不再执行后续代码,这时这个线程将不会被join
。
void func(int n)
for (int i = 0; i <= n; i++)
cout << i << endl;
bool DoSomething()
return false;
int main()
thread t(func, 20);
//...
if (!DoSomething())
return -1;
//...
t.join(); //不会被执行
return 0;
因此采用join
方式结束线程时,join
的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,也就是利用对象的生命周期来控制线程资源的释放。比如:
class myThread
public:
myThread(thread& t)
:_t(t)
~myThread()
if (_t.joinable())
_t.join();
//防拷贝
myThread(myThread const&) = delete;
myThread& operator=(const myThread&) = delete;
private:
thread& _t;
;
使用方式如下:
- 每当创建一个线程对象后,就用myThread类对其进行封装产生一个myThread对象。
- 当myThread对象生命周期结束时就会调用析构函数,在析构中会通过
joinable
判断这个线程是否需要被join
,如果需要那么就会调用join
对其该线程进行等待。
例如刚才的代码中,使用myThread类对线程对象进行封装后,就能保证线程一定会被join
。
int main()
thread t(func, 20);
myThread mt(t); //使用myThread对线程对象进行封装
//...
if (!DoSomething())
return -1;
//...
t.join();
return 0;
detach方式
主线程创建新线程后,也可以调用detach
函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。
- 使用
detach
的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach
函数。 - 否则线程对象可能会因为某些原因,在后续调用
detach
函数分离线程之前被销毁掉,这时就会导致程序崩溃。 - 因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过
joinable
判断这个线程是否需要被join
,如果需要那么就会调用terminate
终止当前程序(程序崩溃)。
互斥量库(mutex)
mutex的种类
四种互斥量
在C++11中,mutex中总共包了四种互斥量:
1、std::mute
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。
mutex中常用的成员函数如下:
成员函数 | 功能 |
---|---|
lock | 对互斥量进行加锁 |
try_lock | 尝试对互斥量进行加锁 |
unlock | 对互斥量进行解锁,释放互斥量的所有权 |
线程函数调用lock
时,可能会发生以下三种情况:
- 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用
unlock
之前,该线程一致拥有该锁。 - 如果该互斥量已经被其他线程锁住,则当前的调用线程会被阻塞。
- 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
线程调用try_lock
时,类似也可能会发生以下三种情况:
- 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用
unlock
之前,该线程一致拥有该锁。 - 如果该互斥量已经被其他线程锁住,则
try_lock
调用返回false,当前的调用线程不会被阻塞。 - 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
2、std::recursive_mutex
recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。
- 如果在递归函数中使用mutex互斥锁进行加锁,那么在线程进行递归调用时,可能会重复申请已经申请到但自己还未释放的锁,进而导致死锁问题。
- 而recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的
unlock
。
除此之外,recursive_mutex也提供了lock
、try_lock
和unlock
成员函数,其的特性与mutex大致相同。
3、std::timed_mutex
timed_mutex中提供了以下两个成员函数:
try_lock_for
:接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间之内还是没有获得锁),则返回false。try_lock_untill
:接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间点到来时还是没有获得锁),则返回false。
除此之外,timed_mutex也提供了lock
、try_lock
和unlock
成员函数,其的特性与mutex相同。
4、std::recursive_timed_mutex
recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时尝试申请锁。
加锁示例
在没有使用互斥锁保证线程安全的情况下,让两个线程各自打印1-100的数字,就会导致控制台输出错乱。比如:
void func(int n)
for (int i = 1; i <= n; i++)
cout << i << endl;
int main()
thread t1(func, 100);
thread t2(func, 100);
t1.join();
t2.join();
return 0;
如果要让两个线程的输出不会相互影响,即不会让某一次输出中途被另一个线程打断,那么就需要用互斥锁对打印过程进行保护。
这里加锁的方式有两种,一种是在for循环体内进行加锁,一种是在for循环体外进行加锁。比如:
void func(int n, mutex& mtx)
mtx.lock(); //for循环体外加锁
for (int i = 1; i <= n; i++)
//mtx.lock(); //for循环体内加锁
cout << i << endl;
//mtx.unlock();
mtx.unlock();
int main()
mutex mtx;
thread t1(func, 100, ref(mtx));
thread t2(func, 100, ref(mtx));
t1.join();
t2.join();
return 0;
说明一下:
- 此处在for循环体外加锁比在for循环体内加锁更高效,因为在for循环体内加锁会导致线程打印数字时频繁进行加锁解锁操作,而如果在for循环体外加锁,那么这两个线程只需要在开始打印1之前进行一次加锁,在打印完100后进行一次解锁就行了。
- 在for循环体外加锁也就意味着两个线程的打印过程变成了串行的,即一个线程打印完1-100后另一个线程再打印,但这时打印效率提高了,因为避免了这两个线程间的频繁切换。
- 为了保证两个线程使用的是同一个互斥锁,线程函数必须以引用的方式接收传入的互斥锁,并且在传参时需要使用ref函数保持对互斥锁的引用。
- 此外,也可以将互斥锁定义为全局变量,或是用lambda表达式定义线程函数,然后以引用的方式将局部的互斥锁进行捕捉,这两种方法也能保证两个线程使用的是同一个互斥锁。
经验分享:
- 在项目中实际不太建议定义全局变量,因为全局变量如果定义在头文件中,当这个头文件被多个源文件包含时,在这多个源文件中都会对这个全局变量进行定义,这时就会导致变量重定义,但如果将全局变量定义为静态,那这个全局变量就只在当前文件可见。
- 如果确实有一些变量需要在多个文件中使用,那么一般建议将这些变量封装到一个类当中,然后将这个类设计成单例模式,当需要使用这些变量时就通过这个单例对象去访问即可。
lock_guard和unique_lock
使用互斥锁时可能出现的问题
使用互斥锁时,如果加锁的范围太大,那么极有可能在中途返回时忘记了解锁,此后申请这个互斥锁的线程就会被阻塞住,也就是造成了死锁问题。比如:
mutex mtx;
void func()
mtx.lock();
//...
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
//...
return; //中途返回(未解锁)
//...
mtx.unlock();
int main()
func();
return 0;
因此使用互斥锁时如果控制不好就会造成死锁,最常见的就是此处在锁中间代码返回,此外还有一个比较常见的情况就是在锁的范围内抛异常,也很容易导致死锁问题。
因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。
lock_guard
lock_guard是C++11中的一个模板类,其定义如下:
template <class Mutex>
class lock_guard;
lock_guard类模板主要是通过RAII的方式,对其管理的互斥锁进行了封装。
- 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用
lock
进行加锁。 - 当lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用
unlock
自动解锁。
通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。比如:
mutex mtx;
void func()
lock_guard<mutex> lg(mtx); //调用构造函数加锁
//...
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
//...
return; //调用析构函数解锁
//...
//调用析构函数解锁
int main()
func();
return 0;
从lock_guard对象定义到该对象析构,这段区域的代码都属于互斥锁的保护范围。
如果只想用lock_guard保护某一段代码,可以通过定义匿名的局部域来控制lock_guard对象的生命周期。比如:
mutex mtx;
void func()
//...
//匿名局部域
lock_guard<mutex> lg(mtx); //调用构造函数加锁
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
//...
return; //调用析构函数解锁
//调用析构函数解锁
//...
int main()
func();
return 0;
模拟实现lock_guard
模拟实现lock_guard类的步骤如下:
- lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
- 调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的
lock
函数进行加锁。 - lock_guard的析构函数中调用互斥锁的
unlock
进行解锁。 - 需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的。
代码如下:
namespace cl
template<class Mutex>
class lock_guard
public:
lock_guard(Mutex& mtx)
:_mtx(mtx)
mtx.lock(); //加锁
~lock_guard()
mtx.unlock(); //解锁
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
Mutex& _mtx;
;
unique_lock
但由于lock_guard太单一,用户没有办法对锁进行控制,因此C++11又提供了unique_lock。
unique_lock与lock_guard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装。在创建unique_lock对象调用构造函数时也会调用lock进行加锁,在unique_lock对象销毁调用析构函数时也会调用unlock进行解锁。
但lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
- 加锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
- 修改操作:移动赋值、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
比如如下场景就适合使用unique_lock:
- 要用互斥锁保护函数1的大部分代码,但是中间有一小块代码调用了函数2,而调用函数2时不需要用函数1中的互斥锁进行保护,函数2内部的代码由其他互斥锁进行保护。
- 因此在调用函数2之前需要对当前互斥锁进行解锁,当函数2调用返回后再进行加锁,这样当调用函数2时其他线程调用函数1就能够获取到这个锁。
如下图:
原子性操作库(atomic)
线程安全问题
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:
void func(int& n, int times)
for (int i = 0; i < times; i++)
n++;
int main()
int n = 0;
int times = 100000; //每个线程对n++的次数
thread t1(func, ref(n), times);
thread t2(func, ref(n)C++11 ——— 线程库