0%

C++并发与多线程笔记3

第四节 创建多个线程、数据共享问题分析、案例代码

在这里插入图片描述

一、创建和等待多个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void TextThread()
{
cout << "我是线程" << this_thread::get_id() << endl;
/* … */
cout << "线程" << this_thread::get_id() << "执行结束" << endl;
}
//main函数里 vector threadagg;
for (int i = 0; i < 10; ++i)
{
threadagg.push_back(thread(TextThread));
}
for (int i = 0; i < 10; ++i)
{
threadagg[i].join();
}
123456789101112131415
  • 把thread对象放入到容器中管理,看起来像个thread对象数组,对一次创建大量的线程并对大量线程进行管理有好处
  • 多个线程执行顺序是乱的,跟操作系统内部对线程的运行调度机制有关

二:数据共享问题分析
2.1 只读的数据

  • 是安全稳定的

2.2 有读有写

  • 若不加处理,就会出错
  • 最简单的防止崩溃方法:读的时候不能写,写的时候不能读。
  • 如果写的动作分10小步,由于任务切换,导致各种诡异的事情发生(最可能的还是崩溃)

第五节 互斥量概念、用法、死锁演示及解决详解

在这里插入图片描述

一、互斥量(mutex)的基本概念

  • 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
  • 互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

二、互斥量的用法
包含#include<mutex> 头文件
2.1 lock(),unlock()

  • 步骤:1.lock(),2.操作共享数据,3.unlock()。
  • lock()和unlock()要成对使用

2.2 lock_guard类模板

  • std::lock_guard<std::mutex> sbguard(myMutex);取代lock()和unlock()
  • lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

三、死锁
3.1 死锁演示
死锁至少有两个互斥量mutex1,mutex2。

  • a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
  • b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
  • c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。

3.2 死锁的一般解决方案:
只要保证多个互斥量上锁的顺序一样就不会造成死锁。

3.3 std::lock()函数模板

  • std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
  • 如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)

3.4 std::lock_guard的std::adopt_lock参数

  • std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
    加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
  • adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A{
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//lock_guard<mutex> sbguard(myMutex1, adopt_lock);
lock(myMutex1, myMutex2);
//myMutex2.lock();
//myMutex1.lock();
msgRecvQueue.push_back(i);
myMutex1.unlock();
myMutex2.unlock();
}
}
}
bool outMsgLULProc()
{
myMutex1.lock();
myMutex2.lock();
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
myMutex2.unlock();
myMutex1.unlock();
return true;
}
myMutex2.unlock();
myMutex1.unlock();
return false;
}

void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
if (outMsgLULProc())
{

}
else
{
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};

int main()
{
A myobja;
mutex myMutex;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}

第六节 unique_lock(类模板)详解

在这里插入图片描述

1.unique_lock取代lock_guard
unique_lock比lock_guard灵活很多(多出来很多用法),效率差一点。
unique_lock<mutex> myUniLock(myMutex);

std::chrono::milliseconds sec(20000);

std::this_thread::sleep_for(sec);

2.unique_lock的第二个参数
2.1 std::adopt_lock:

  • 表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
  • 前提:必须提前lock
  • lock_guard中也可以用这个参数

2.2 std::try_to_lock:

  • 尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
  • 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
  • 前提:不能提前lock();
  • owns_locks()方法判断是否拿到锁,如拿到返回true

2.3 std::defer_lock:

  • 如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
  • 不给它加锁的目的是以后可以调用unique_lock的一些方法
  • 前提:不能提前lock

3.unique_lock的成员函数(前三个与std::defer_lock联合使用)
3.1 lock():加锁。

1
2
3
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
12

不用自己unlock();

3.2 unlock():解锁。

1
2
3
4
5
6
7
8
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//处理一些共享代码
myUniLock.unlock();
//处理一些非共享代码
myUniLock.lock();
//处理一些共享代码
1234567

因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。

3.3 try_lock():尝试给互斥量加锁
如果拿不到锁,返回false,否则返回true。

3.4 release():

  • unique_lock
    myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权
  • mutex* ptx = myUniLock.release();所有权由ptx接管,如果原来mutex对象处理加锁状态,就需要ptx在以后进行解锁了。

lock的代码段越少,执行越快,整个程序的运行效率越高。
a.锁住的代码少,叫做粒度细,执行效率高;
b.锁住的代码多,叫做粒度粗,执行效率低;

4.unique_lock所有权的传递
unique_lock myUniLock(myMutex);把myMutex和myUniLock绑定在了一起,也就是myUniLock拥有myMutex的所有权。类似unique_ptr
1. 使用move转移

  • myUniLock拥有myMutex的所有权,myUniLock可以把自己对myMutex的所有权转移,但是不能复制。
  • unique_lock myUniLock2(std::move(myUniLock));
    现在myUniLock2拥有myMutex的所有权。

2. 在函数中return一个临时变量,即可以实现转移

1
2
3
4
5
6
7
unique_lock<mutex> aFunction()
{
unique_lock<mutex> myUniLock(myMutex);
//移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
//返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
return myUniLock;
}

第七节 单例设计模式共享数据分析、解决,call_once

在这里插入图片描述

1.设计模式

  • 程序灵活,维护起来可能方便,用设计模式理念写出来的代码很晦涩,但是别人接管、阅读代码都会很痛苦
  • 老外应付特别大的项目时,把项目的开发经验、模块划分经验,总结整理成设计模式
  • 中国零几年设计模式刚开始火时,总喜欢拿一个设计模式往上套,导致一个小小的项目总要加几个设计模式,本末倒置
  • 设计模式有其独特的优点,要活学活用,不要深陷其中,生搬硬套

2.单例设计模式:
整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。
单例类:只能生成一个对象。

3.单例设计模式共享数据分析、解决
面临问题:需要在自己创建的线程中来创建单例类的对象,这种线程可能不止一个。我们可能面临GetInstance()这种成员函数需要互斥。
可以在加锁前判断m_instance是否为空,否则每次调用Singelton::getInstance()都要加锁,十分影响效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>	
#include <mutex>
using namespace std;

mutex myMutex;
//懒汉模式
class Singelton
{
public:
static Singelton * getInstance() {
//双重锁定 提高效率
if (instance == NULL) {
lock_guard<mutex> myLockGua(myMutex);
if (instance == NULL) {
instance = new Singelton;
}
}
return instance;
}
private:
Singelton() {}
static Singelton *instance;
};
Singelton * Singelton::instance = NULL;

//饿汉模式
class Singelton2 {
public:
static Singelton2* getInstance() {
return instance;
}
private:
Singelton2() {}
static Singelton2 * instance;
};
Singelton2 * Singelton2::instance = new Singelton2;

int main(void)
{
Singelton * singer = Singelton::getInstance();
Singelton * singer2 = Singelton::getInstance();
if (singer == singer2)
cout << "二者是同一个实例" << endl;
else
cout << "二者不是同一个实例" << endl;

cout << "---------- 以下 是 饿汉式 ------------" << endl;
Singelton2 * singer3 = Singelton2::getInstance();
Singelton2 * singer4 = Singelton2::getInstance();
if (singer3 == singer4)
cout << "二者是同一个实例" << endl;
else
cout << "二者不是同一个实例" << endl;

return 0;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

如果觉得在单例模式new了一个对象,而没有自己delete掉,这样不合理。可以增加一个类中类CGarhuishou,new一个单例类时创建一个静态的CGarhuishou对象,这样在程序结束时会调用CGarhuishou的析构函数,释放掉new出来的单例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Singelton
{
public:
static Singelton * getInstance() {
if (instance == NULL) {
static CGarhuishou huishou;
instance = new Singelton;
}
return instance;
}
class CGarhuishou {
public:
~CGarhuishou()
{
if (Singelton::instance)
{
delete Singelton::instance;
Singelton::instance = NULL;
}
}
};
private:
Singelton() {}
static Singelton *instance;
};
Singelton * Singelton::instance = NULL;

4.std::call_once():
函数模板,该函数的第一个参数为标记,第二个参数是一个函数名(如a())。
功能:能够保证函数a()只被调用一次。具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。
call_once()需要与一个标记结合使用,这个标记为std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。

多个线程同时执行时,一个线程会等待另一个线程先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
once_flag g_flag;
class Singelton
{
public:
static void CreateInstance()//call_once保证其只被调用一次
{
instance = new Singelton;
}
//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
static Singelton *getInstance() {
call_once(g_flag, CreateInstance);
return instance;
}
private:
Singelton() {}
static Singelton *instance;
};
Singelton * Singelton::instance = NULL;

第八节 condition_variable、wait、notify_one、notify_all

在这里插入图片描述

一、条件变量condition_variable、wait、notify_one、notify_all
std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。

1
2
3
4
5
6
7
8
9
10
std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
return true;
return false;
});

condition.wait(sbguard1);
123456789

wait()用来等一个东西

如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行
如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。

阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止;

如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样

wait()将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用notify_one()成员函数为止。

当其他线程用notify_one()将本线程wait()唤醒后,这个wait恢复后

1、wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁

2.1、如果wait有第二个参数就判断这个lambda表达式。

  • a)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
  • b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。

2.2、如果wait没有第二个参数,则wait返回,流程走下去。

流程只要走到了wait()下面则互斥量一定被锁住了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <thread>
#include <iostream>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue插入一个元素" << i << endl;

std::unique_lock<std::mutex> sbguard1(mymutex1);
msgRecvQueue.push_back(i);
//尝试把wait()线程唤醒,执行完这行,
//那么outMsgRecvQueue()里的wait就会被唤醒
//只有当另外一个线程正在执行wait()时notify_one()才会起效,否则没有作用
condition.notify_one();
}
}

void outMsgRecvQueue() {
int command = 0;
while (true) {
std::unique_lock<std::mutex> sbguard2(mymutex1);
// wait()用来等一个东西
// 如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行
// 阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止;
//当 wait() 被 notify_one() 激活时,会先执行它的 条件判断表达式 是否为 true,
//如果为true才会继续往下执行
condition.wait(sbguard2, [this] {
if (!msgRecvQueue.empty())
return true;
return false;});
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//因为unique_lock的灵活性,我们可以随时unlock,以免锁住太长时间
sbguard2.unlock();
cout << "outMsgRecvQueue()执行,取出第一个元素" << endl;
}
}

private:
std::list<int> msgRecvQueue;
std::mutex mymutex1;
std::condition_variable condition;
};

int main() {
A myobja;
std::thread myoutobj(&A::outMsgRecvQueue, &myobja);
std::thread myinobj(&A::inMsgRecvQueue, &myobja);
myinobj.join();
myoutobj.join();
}

二、深入思考

上面的代码可能导致出现一种情况:
因为outMsgRecvQueue()与inMsgRecvQueue()并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue 中已经有了很多消息,但是,outMsgRecvQueue还是被唤醒一次只处理一条数据。这时可以考虑把outMsgRecvQueue多执行几次,或者对inMsgRecvQueue进行限流。

三、notify_all()

notify_one():通知一个线程的wait()

notify_all():通知所有线程的wait()