概念:
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秒开锁,避免了阻塞导致的程序瘫痪。