C++线程学习

概念:
01并发:计算机中的程序关于某个数据集合上的一次运行活动
,两个或多个独立的任务同时发生
02线程:组成一个进程的基本单元
03进程:应用程序,比如exe的一个运行状态
在这里插入图片描述
每个进程有且只有一个主线程,vs编译器中ctr+F5运行程序,实际上是主线程调用main的代码

04并发的实现:1.多进程实现并发 2.单进程,多线程实现并发,就是一个主线程多个子线程实现并发
临界资源:一个进程中所有线程共享的内存空间,例如全局变量,指针引用

1.thread的简单使用

#include <iostream>
#include <thread>
#include <Windows.h>

using namespace std;

void thread01()
{
	for (int i = 0; i < 5; i++)
	{
		cout << "Thread 01 is working !" << endl;
		Sleep(100);
	}
}
void thread02()
{
	for (int i = 0; i < 5; i++)
	{
		cout << "Thread 02 is working !" << endl;
		Sleep(200);
	}
}

int main()
{
	thread task01(thread01);
	//创建一个thread类对象task01,以thread01做为一个线程创建时构造的参数,创建的线程01执行的是thread01函数中的任务
	thread task02(thread02);
	task01.join();
	//将01子线程加入,阻塞主线程,等到01运行完才进行主线程
	task02.join();
	//将02子线程加入,阻塞主线程,等到02运行完才进行主线程

	for (int i = 0; i < 5; i++)
	{
		cout << "Main thread is working !" << endl;
		Sleep(200);
	}
	system("pause");
}

子线程01,02不会互相阻塞对方,当这两个子线程都结束时才会继续主线程
在这里插入图片描述

2.线程同步

#include <iostream>
#include <thread>
#include <Windows.h>

using namespace std;

int a = 0;
void thread01()
{
	for (int i = 0; i < 10000000; i++)
		a++;
}


int main()
{
	thread task01(thread01);//以thread01做为一个线程创建时的参数,创建的线程01执行的是thread01函数中的任务
	for (int i = 0; i < 10000000; i++)
		a++;
	task01.join();//将01子线程加入,阻塞主线程,等到01运行完才进行主线程
	cout << a << endl;
	system("pause");
}

在这里插入图片描述
将i 设置到10000000时会出现错误结果,一次for循环的时间大于两条语句之间的间隔,可以看到,代码中全局变量i先在主线程中自加了10000000次,再在主线程中自加了10000000次,结果应是20000000,但显示的结果为13010045,推测是主线程进行了3010045次还未结束时子线程就加进来了阻塞了主线程然后执行子线程,于是将子进程设置为空函数,预计最后显示的结果为3010045
在这里插入图片描述
可以看到结果并不是我们的猜想,说明子进程的加入并不会阻塞正在进行的主线程中的操作
我们查看子进程中a++的反汇编:

a++;
00D02643  mov         eax,dword ptr [a (0D0E2D4h)]
//将内存中地址为0D0E2D4h的值mov给寄存器eax
00D02648  add         eax,1  
//将eaxadd1
00D0264B  mov         dword ptr [a (0D0E2D4h)],eax 
//将eax的值放回 0D0E2D4h中
00D02650  jmp         thread01+31h (0D02631h)  

C++为高级语言,一句自增操作在翻译成汇编语言后变成了四句
再查看主进程中a++的反汇编:

00D03C39  mov         eax,dword ptr [a (0D0E2D4h)]  
00D03C3E  add         eax,1  
00D03C41  mov         dword ptr [a (0D0E2D4h)],eax  
00D03C46  jmp         std::thread::join+47h (0D03C27h)  

可以看到a存放的寄存器的地址相同,但指令存放的地址不同。
并发为交替地进行两个以下的操作,假设每个操作执行X毫秒
当子进程加进来后,比如在执行进程1X毫秒后只执行了第一句,假设此时a为5000,然后去执行进程2执行X毫秒a自增了3000,再切换回进程1接着执行之前没有执行完的第二局,a变回5000,在进程2中自增的3000消失掉了。
这就是同步带来的问题

3.线程同步的方法

原子操作:是指线程在访问资源时确保其他线程不会访问相同的资源(让C++一行代码对应的多行汇编代码视作一个整体,要么一起执行完毕,要不一行都不执行)

for (int i = 0; i < 10000000; i++)
	{
		InterlockedAdd((long*)&a, 1);
	}

但这仅限于加法,泛化性不高
因此引入锁这个概念
使用了windows的API #include <Windows.h>
在使用前先创建临界区对象,相当于去买锁
CRITICAL_SECTION g_cs;
要先初始化锁InitializeCriticalSection,使用结束了要删掉锁DeleteCriticalSection,使用锁前EnterCriticalSection加锁,使用后解锁LeaveCriticalSection;

#include <iostream>
#include <thread>
#include <Windows.h>

using namespace std;

int a = 0;

//创建临界区对象--等价于锁
CRITICAL_SECTION g_cs;

void thread01()
{
	for (int i = 0; i < 10000000; i++)
	{
		//进来时上锁
		EnterCriticalSection(&g_cs);
		a++;
		//出去解锁
		LeaveCriticalSection(&g_cs);
	}
}


int main()
{
	InitializeCriticalSection(&g_cs);
	thread task01(thread01);
	for (int i = 0; i < 10000000; i++)
	{
		//进来时上锁
		EnterCriticalSection(&g_cs);
		
		a++;
		//出去解锁
		LeaveCriticalSection(&g_cs);
	}
	task01.join();
	cout << a << endl;
	system("pause");
	//不使用时删掉该锁
	DeleteCriticalSection(&g_cs);
}

在这里插入图片描述

4.颗粒度

上锁方法1


	for (int i = 0; i < 10000000; i++)
	{
		EnterCriticalSection(&g_cs);
		a++;
		LeaveCriticalSection(&g_cs);
	}

上锁方法2

EnterCriticalSection(&g_cs);
for (int i = 0; i < 10000000; i++)
	{	a++;}
LeaveCriticalSection(&g_cs);

1的颗粒度小于2,尽量让颗粒度小,不然其他不会影响该进程的进程就无法加入,浪费资源

5.自己封装线程同步锁

手动上锁的话可能会忘了解锁,如果没有解锁LeaveCriticalSection的话会一直卡在这个进程里,其他进程想进入这个锁的房间进不去(比喻),因此自己封装线程同步锁,新建一个mutex类,将windows的api封装进去

mutex.h

#include<Windows.h>
class mutex
{
public:
	mutex();
	~mutex();
	void lock();
	void unlock();
private:
	CRITICAL_SECTION g_cs;
};

mutex.cpp

#include "mutex.h"
mutex::mutex()
{
	InitializeCriticalSection(&g_cs);
}
mutex::~mutex()
{
	DeleteCriticalSection(&g_cs);
}
void mutex::lock()
{
	EnterCriticalSection(&g_cs);
}
void mutex::unlock()
{
	LeaveCriticalSection(&g_cs);
}

main.cpp

#include <iostream>
#include <thread>
#include <Windows.h>
#include"mutex.h"
using namespace std;
int a = 0;
CRITICAL_SECTION g_cs;
mutex mt;
void thread01()
{
	for (int i = 0; i < 10000000; i++)
	{
		mt.lock();
		a++;
		mt.unlock();
	}
}
int main()
{
	thread task01(thread01);
	for (int i = 0; i < 10000000; i++)
	{
		mt.lock();
		a++;
		mt.unlock();
	}
	task01.join();
	cout << a << endl;
	system("pause");
	DeleteCriticalSection(&g_cs);
}

此时还没有解决可能遗忘解锁的问题

#include <iostream>
#include <thread>
#include <Windows.h>
#include"mutex.h"
using namespace std;

int a = 0;
mutex mt;

将windows的api封装起来在mt中,使用前对该对象进行初始化,在mt的构造函数中初始化锁,析构函数中删除锁

CRITICAL_SECTION g_cs;

创建临界区对象g_cs–等价于锁

class mutexguard
{
public:
	mutexguard()
	{
		mt.lock();
	}
	~mutexguard()
	{
		mt.unlock();
	}
};

```cpp
#include "mutex.h"
mutex::mutex()
{
	InitializeCriticalSection(&g_cs);
}
mutex::~mutex()
{
	DeleteCriticalSection(&g_cs);
}
void mutex::lock()
{
	EnterCriticalSection(&g_cs);
}
void mutex::unlock()
{
	LeaveCriticalSection(&g_cs);
}

新建一个类,通过该类的构造函数来调用mt的上锁操作
通过该类的自动析构来自动调用mt的解锁操作


```cpp
void thread01()
{
	{
		mutexguard mg2;
		for (int i = 0; i < 10000000; i++)
			a++;
	}
}
int main()
{
	thread task01(thread01);
	{
		mutexguard mg1;
	    for (int i = 0; i < 10000000; i++)
		a++;
	}
	task01.join();
	cout << "a:"<<a << endl;
	system("pause");
}

其中:
{
mutexguard mg2;
for (int i = 0; i < 10000000; i++)
a++;
}
将mg2的生命周期规定了在括号内,减少颗粒度
在这里插入图片描述
可以看到结果正常

6.STL的锁机制

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
int a = 0;

mutex lock1;

void thread01()
{

	{
		lock1.lock();
		for (int i = 0; i < 10000000; i++)
			a++;
		lock1.unlock();
	}
}
int main()
{
	thread task01(thread01);
	{
		lock1.lock();
		for (int i = 0; i < 10000000; i++)
			a++;
		lock1.unlock();
	}
	task01.join();
	cout << "a:" << a << endl;
	system("pause");
}

也可以用stl里自带的模板来封装该锁来自动删除锁

void thread01()
{

	{
		lock_guard<mutex>lg(lock1);
		for (int i = 0; i < 10000000; i++)
			a++;
	}
}

7.死锁产生原因

死锁:线程在等待一个永远不会成立的条件成立,从而陷入无尽的等待,永远不能被唤醒的状态叫死锁。
在这里插入图片描述

#include<iostream>
#include<Windows.h>
#include<mutex>
#include<thread>
using namespace std;
mutex m1;
mutex m2;
void f1()
{
	lock_guard<mutex>lg1(m1);
	cout << "进程1获得锁1" << endl;
	Sleep(2000);//确保f2能获得锁2
	lock_guard<mutex>lg2(m2);
	cout << "进程1获得锁2" << endl;
}
void f2()
{
	Sleep(1000);//确保f1能获得锁1
	lock_guard<mutex>lg3(m2);
	cout << "进程2获得锁2" << endl;
	lock_guard<mutex>lg4(m1);
	cout << "进程2获得锁1" << endl;
}
void main()
{
	thread task01(f1);
	thread task02(f2);
	task01.join();
	task02.join();
}

将2个线程比作2个人,一个房间有且只有唯一的锁与之对应,当A人进入房间后会将房门的锁从里面反锁上,B想进这个房间只有等A将锁打开,当A把锁打开后,如果B进入了该房间也会从里面反锁上
在等待时,操作系统会调度线程选择一个合适的线程去跑,只要门没有被锁就可以进,比如1000ms时线程2用m2去开门,由于此时虽然m1锁着在,但线程2此时不走m1锁着的门,而是去开m2锁着的门。

在这里插入图片描述

8.互斥量

C++11提供以下4中互斥量
std::mutex 独占的互斥量,不能递归使用
std::recursive_mutex 递归的互斥量,不带超时功能
std::timed_mutex带超时功能的独占互斥量,不能递归使用
std::recursive_timed_mutex带超时功能的递归互斥量

#include<iostream>
#include<Windows.h>
#include<thread>
#include<mutex>
using namespace std;

void main()
{
	recursive_mutex rm;
	rm.lock();
	rm.lock();
	cout << "111" << endl;
	rm.unlock();
	rm.unlock();
}

recursive_mutex 递归的互斥量,相当于这个房间可以多次上锁,但走出房间时要一一把这些锁打开,但要注意多次上锁的动作主体应该是同一个人(进程)

#include<iostream>
#include<Windows.h>
#include<thread>
#include<mutex>
using namespace std;
timed_mutex tm;
void f1()
{
	lock_guard<timed_mutex> g1(tm);
	if(tm.try_lock_for(chrono::seconds(5)))
	cout << "222" << endl;
}
void main()
{
	lock_guard<timed_mutex> g1(tm);
	thread t1(f1);
	cout << "111" << endl;
	t1.join();
}

timed_mutex超时锁,当有人想进入房间时会在这个锁边上等待一段时间,不会因为进不去就立马放弃,使用tm.try_lock_for(chrono::seconds(5))尝试等待5秒开锁,避免了阻塞导致的程序瘫痪。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值