目录
线程库
thread类的简单介绍
explicit thread (Fn&& fn, Args&&… args);
fn: 因为是一个万能引用,所以既可以接受左值,又可以接受右值,列举以下能够被接受的: 函数指针、仿函数对象、lamda表达式、function/bind包装过的可以执行对象
args:可以接受可变参数包。
使用thread创建多个线程
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <thread>
#include <vector>
#include <stdio.h>
#include <mutex>
#include <stdio.h>
using namespace std;
void func(int n)
{
for (int i = 0; i < n; i++) {
//this_thread::get_id() 获取当前线程的线程id。
cout << "线程id:" << this_thread::get_id() << ": " << i << endl;
}
}
void test()
{
int n = 0;
cin >> n;
//创建n个线程,执行func函数
vector<thread> vthread;
vthread.resize(n);
for(auto &e : vthread){
e = thread(func, 100);
}
//主线程阻塞新线程
for (auto &e : vthread) {
e.join();
}
}
int main()
{
test();
return 0;
}
使用landmad表达式
void test()
{
mutex mtx;
int n = 0;
int x = 0;
int N = 100;
cin >> n;
//创建n个线程,执行func函数
vector<thread> vthread;
vthread.resize(n);
for(auto &e : vthread){
e = thread([&mtx, &N, &x] {
mtx.lock(); //加锁保证x的原子性
for (int i = 0; i < N; i++) {
++x;
}
mtx.unlock();//解锁
});
}
//主线程阻塞新线程
for (auto &e : vthread) {
e.join();
}
//获取x最终的值
cout << x << endl;
}
mutex使用
使用mutex互斥锁让线程互斥
mutex mtx; //定义互斥锁
void func(int n)
{
mtx.lock();
for (int i = 0; i < n; i++) {
cout << "线程id:" << this_thread::get_id() << ": " << i << endl;
}
mtx.unlock();
}
当1号线程执行完成后继而继续执行2号线程
线程交替打印
void test()
{
mutex mtx;
int n = 0;
int x = 0;
int N = 1000;
cin >> n;
//创建n个线程,执行func函数
vector<thread> vthread;
vthread.resize(n);
for(auto &e : vthread){
e = thread([&mtx, &N, &x]
{
for (int i = 0; i < N; i++) {
mtx.lock();
cout << "线程id: " << this_thread::get_id() << ":" << x << endl;
++x;
//休眠500毫秒
this_thread::sleep_for(chrono::milliseconds(500));
mtx.unlock();
//休眠500毫秒
this_thread::sleep_for(chrono::milliseconds(500));
}
});
}
//主线程阻塞新线程
for (auto &e : vthread) {
e.join();
}
//获取x最终的值
cout << x << endl;
}
原子性操作库atomic
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦:
比如我们这段代码,如果不使用互斥锁将临界资源x保护起来的话,那么x的值其实是不正确的
void test()
{
mutex mtx;
int n = 0;
int x = 0;
int N = 1000;
cin >> n;
//创建n个线程,执行func函数
vector<thread> vthread;
vthread.resize(n);
for(auto &e : vthread){
e = thread([&mtx, &N, &x]
{
for (int i = 0; i < N; i++) {
++x;
}
});
}
//主线程阻塞新线程
for (auto &e : vthread) {
e.join();
}
//获取x最终的值
cout << x << endl;
}
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
需要使用以上原子操作变量时,必须添加< atomic >这个头文件
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型
atmoic<T> t; // 声明一个类型为T的原子类型变量t
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
lock_guard
在我们写代码的实际过程中,直接将一段代码加锁保护其实是不安全的,距举例以下的例子
mutex mtx;
mtx.lock();
//代码段......
mtx.unlock();
为什么说他是不安全的呢?
1、从两个角度出发,如果代码在执行的过程中被突然中断了return后那么原来lock了之后锁是没有被释放的,如果是在多线程环境下这就会导致死锁,
先看malloc的表现,malloc失败后会返回null, 那么if判断之后,会直接返回-1, 整个程序就结束了,可是如果是多线程环境下,那么还是会死锁,一个线程拿到锁之后并没有释放,另外一个线程就会被卡在执行处
mutex mtx;
int func2()
{
mtx.lock();
int* p = (int*)malloc(sizeof(int) * 10);
if (!p) return -1; //内存申请失败后返回
for (int i = 0; i < 10; i++) {
*(p + i) = i;
}
for (int i = 0; i < 10; i++) cout << p[i] << " ";
mtx.unlock();
}
int main()
{
thread t1(func2);
thread t2(func2);
//test();
t1.join();
t2.join();
return 0;
}
2、如果抛异常了之后,跳转到catch处,可是catch并不能为你释放锁资源,那么也会导致死锁的问题
如果代码是正常执行的话,那么并没有什么问题,可以如果是在多线程的环境下new一旦失败了,就会抛异常而try 捕捉到了之后就会交给catch处理,可是catch也并没有去释放锁
mutex mtx;
int func2()
{
mtx.lock();
int* p = new int[10];
if (!p) return -1; //内存申请失败后返回
for (int i = 0; i < 10; i++) {
*(p + i) = i;
}
for (int i = 0; i < 10; i++) cout << p[i] << " ";
mtx.unlock();
}
int main()
{
thread t1(func2);
thread t2(func2);
//test();
t1.join();
t2.join();
return 0;
}
总结:
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII( RAII (Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄r网络连接、斥量等等)的简单技术。 )
的方式对锁进行了封装,即lock_guard和unique_lock,引入lock_guard之后,并不会出现这样的问题
为了帮助我们理解lock_guard锁保护,博主模拟了一下lock_guard的实现过程
我们可以发现,在使用lock_guard之后,无论是抛异常还是程序中间返回,都会被处理。
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前, lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
// 模拟lock_guard的实现过程
namespace mzt
{
template<class T>
class lock_guard
{
public:
lock_guard(T& lock) : _lock(lock)
{
_lock.lock();
}
~lock_guard() { _lock.unlock(); }
lock_guard(lock_guard<T>& lock) = delete;
lock_guard<T>& operator=(lock_guard<T>& lock) = delete;
private:
T& _lock;
};
}
mutex mtx;
int func2()
{
mzt::lock_guard<mutex> lg (mtx);
//mtx.lock();
int* p = (int*)malloc(sizeof(int) * 10);
if (!p) return -1; //内存申请失败后返回
for (int i = 0; i < 10; i++) {
*(p + i) = i;
}
for (int i = 0; i < 10; i++) cout << p[i] << " ";
//mtx.unlock();
return 0;
}
int main()
{
thread t1(func2);
thread t2(func2);
//test();
t1.join();
t2.join();
return 0;
}
另外如果只想保护单段代码也可以通过{ }的方式只括起一小段代码,只要该对象出了作用域之后,都会立刻马上被释放锁。
{
mzt::lock_guard<mutex> lg (mtx);
int* p = (int*)malloc(sizeof(int) * 10);
if (!p) return -1; //内存申请失败后返回
for (int i = 0; i < 10; i++) {
*(p + i) = i;
}
for (int i = 0; i < 10; i++) cout << p[i] << " ";
}
unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式,管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。 与lock_guard不同的是,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所管理的互斥量的指针)。
wait
参数:
lck: 传递unique_lock类型的对象
Predicate: 模板参数,传递对应的可调用对象(lanmda表达式、函数指针、仿函数), 可调用对象必须是具有返回值的,并且返回值必须是一个bool类型, 。
等到通知当前线程(应该已经锁定了 lck 的互斥锁)的执行被阻塞,直到得到通知。
在阻塞线程的那一刻,函数自动调用 lck.unlock(),允许其他被锁定的线程继续。
一旦通知(明确地,由某个其他线程),该函数将解除阻塞并调用 lck.lock(),使 lck 处于与调用函数时相同的状态。然后函数返回(注意最后一个互斥锁可能会在返回之前再次阻塞线程)。
通常,函数通过调用另一个线程中的成员 notify_one 或成员 notify_all 来通知唤醒。但是某些实现可能会在没有调用任何这些函数的情况下产生虚假的唤醒呼叫。因此,该功能的用户应确保满足其恢复条件。
如果指定了 pred (2),则该函数仅在 pred 返回 false 时阻塞,并且通知只能在它变为 true 时解除阻塞线程(这对于检查虚假唤醒调用特别有用)。
此版本 (2) 的行为就像实现为:
while (!pred()) wait(lck);
notify_one
通知一个
解除阻塞当前等待(这属于一个条件)
这个条件的线程之一。
如果没有线程在等待,该函数什么也不做。
如果多于一个,则未指定选择哪个线程。、
使用wait 和 notify_one函数
考虑三种情况
1、情况一:
如果线程2还没有被创建只是线程1创建了,那么线程1当前执行完了之后,就会被挂起等待了,并不会继续向下执行, 当然他也是依赖于pred可调用对象
的返回值, 此后线程1每执行一次,就会就会将flag的值修改为false, 并唤醒线程2,再将自己挂起,最后线程2和线程1每次自身执行完后,都会去修改flag的值并相互通知。
//交替打印奇数和偶数
void test4()
{
mutex mtx;
std::condition_variable cv; //定义条件变量
const int n = 100;
bool flag = true;
thread t1([&]()
{
int i = 1;
for (; i < n; ) {
//lock会绑定mtx, 当lock对象创建的时候,自动上锁
unique_lock<mutex> lock(mtx);
//调用wait函数对当前满足pred返回false时阻塞
cv.wait(lock, [&flag]()->bool
{
return flag;
});
i += 2;
flag = false;
cout <<"线程id: "<< this_thread::get_id() << ":" << i << endl;
//出了当前作用域后,lock会调用析构函数自动释放。
cv.notify_one();// 唤醒在当前等待条件下的线程
}
});
}
2、情况2:线程1还没被创建出来,线程2先获取到锁了, 那么线程2此时flag的值是true, 而线程2的pred函数返回时对falg进行取反后,return的就是false了, while判断的时候再取一层反就是true, 会直接被挂起,所以线程2即使先被创建也并不会立即执行。
//交替打印奇数和偶数
void test4()
{
thread t2([&]() {
int i = 2;
for (; i < n; ) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag]()->bool{
return !flag;
});
cout << "线程id: " << this_thread::get_id() <<":" << i << endl;
i += 2;
flag = true;
cv.notify_one();// 唤醒在当前等待条件下的线程
}
});
t1.join();
t2.join();
}