什么是线程同步和互斥???
如果你编写的是多线程的程序,那么多个线程的并发执行,可以认为他们是同时执行代码的,但是线程和线程之间并非是毫无关系的,很多时候会有以下的两种关系:
a) 线程A的继续执行 要以线程B完成了某一个操作后为前提,这种称为线程同步
b) 多个线程不可同时修改一个资源(全局变量 ,数据结构,对象),这种称为线程的互斥
为什么需要线程同步和互斥
线程同步:
// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
printf("打开文件\r\n");
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
printf("读写文件\r\n");
return 0;
}
int main (){
HANDLE hThread1 = 0, hThread2;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
//printf("%d", g_n);
return 0;
}
在我们写代码的时候都知道 代码的执行是要有先后顺序的 比如:我们读写文件 要先打开文件才能读写 读写的动作是建立在打开文件的基础上 线程也是如此 这里只是简单的举个例子 只有两个线程 代码也不复杂 当线程多了 代码变得复杂 结果就不是我们预期的结果了
线程互斥:
两个线程对同一个变量进行了操作,结果是随机的
// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
g_n++;
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
g_n++;
return 0;
}
int main (){
HANDLE hThread1 = 0, hThread2;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
printf("%d", g_n);
return 0;
}
我们看最后的结果并非是正确的结果 这就是线程互斥问题 接下来有几个机制完美的解决了互斥和同步的问题
原子操作
特性:解决互斥问题 对于一个变量的基本算数运算保证是原子性的,换句话说就是我操作变量的时候别人是不能操作的
函数 | 作用 |
InterlockedIncrement | 自增 |
InterlockedDecrement | 自减 |
InterlockedExchange | 赋值 |
保证同一时间只有一个线程操作
/*
* 原子操作
两个进程访问同一个全局变量
*/
#include <iostream>
#include <Windows.h>
long g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
InterlockedIncrement(&g_n);
//g_n++;
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
InterlockedIncrement(&g_n);
//g_n++;
return 0;
}
int main() {
HANDLE hThread1 = 0, hThread2;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
结果是正确的
缺点:只能保证在操作一个变量的时候是互斥的 我们通常是要保证一段代码,这个时候原子操作就满足不了我们的需求了
临界区
特性:解决互斥问题 不可跨进程 有线程所有权概念 不是内核对象,能保证一段代码在同一时间只有一个线程操作
线程所有权:这个线程加锁就要这个线程解锁
函数 | 作用 | 备注 |
InitializeCriticalSection | 初始化临界区 | |
EnterCriticalSection | 进入临界区 | |
LeaveCriticalSection | 离开临界区 | |
DeleteCriticalSection | 销毁 | |
/*
临界区
保证一段代码不能被两个进程同时执行
初始化临界区以后 在要保护的代码前后加上锁和开锁
引用计数概念 上几次锁就要开几次
自己上锁自己开
*/
#include <iostream>
#include <Windows.h>
long g_n;
CRITICAL_SECTION g_cs = { 0 };
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
//进入临界区,上锁
EnterCriticalSection(&g_cs);
g_n++;
//离开临界区,解锁
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
//进入临界区,上锁
EnterCriticalSection(&g_cs);
g_n++;
//离开临界区,解锁
LeaveCriticalSection(&g_cs);
}
return 0;
}
int main()
{
//初始化临界区
InitializeCriticalSection(&g_cs);
HANDLE hThread1 = 0, hThread2;
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
printf("%d", g_n);
DeleteCriticalSection(&g_cs);
return 0;
}
缺点:
如果你进入临界区 离开没有开锁 其他的线程是无法进入临界区的 这个时候就会出现一个情况 也就是如果我进入临界区以后 代码有问题 还没有执行解锁就崩了 那我别的程序就进不去了 就会出现死锁的情况
激发态非激发态
再说互斥体之前要了解一个函数和两个概念
激发态:就是有信号 也就是线程可执行
非激发态:没有信号,线程不可执行
WaitForSingleObject函数
WaitForSingleObject(内核对象,时间)
顾名思义等一个对象
作用:
当内核对象处于非激发态的时候,就阻塞住,内核对象处于激发态了 就直接返回
副作用:
等待函数对于被等待的对象有一些影响,这种影响叫等待函数的副作用.
返回值:
WAIT_ABANDONED 0x00000080L | 互斥体的情况下有用 |
WAIT_OBJECT_0 0x00000000L | 等到了内核对象被设置成了激发态 |
WAIT_TIMEOUT 0x00000102L | 超时了 |
WAIT_FAILED (DWORD)0xFFFFFFFF | 失败了 |
互斥体
特性:可跨进程,拥有线程所有权,如果有线程崩溃了 互斥体立即变为激发态 等待互斥体的线程会立即获得这个互斥体 互斥体不会造成死锁问题
函数 | 作用 | 备注 |
CreateMutex | 创建互斥体 | 可以为互斥体起名字 |
OpenMutex | 打开互斥体 得到句柄 | 根据名字才能打开互斥体 |
ReleaseMutex | 释放互斥体 | 使互斥体变为激发态 |
CloseHandle | 关闭句柄 | 使用完关闭句柄 |
WaitForSingleObject | 等待互斥体变为激发态 | 等到激发态后,会使得互斥体再次处于非激发态 |
#include <iostream>
#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hMutex = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hMutex, -1);
g_n++;
ReleaseMutex(g_hMutex);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
//一个激发态互斥体变为非激发态 非激发态别的进程不可执行
WaitForSingleObject(g_hMutex, -1);
g_n++;
//使互斥体变为激发态 激发态别的线程才能继续执行
ReleaseMutex(g_hMutex);
}
return 0;
}
int main() {
HANDLE hThread1 = 0, hThread2;
g_hMutex = CreateMutex(
NULL, //安全描述属性,说明这是一个内核对象
FALSE,//TRUE:初始拥有者是创建互斥体的线程,互斥体为非激发态
//FALSE:没有拥有者,互斥体为激发态
NULL //互斥体的名字 别人可以通过这个名字打开这个互斥体 其他进程
);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
//等待线程从非激发态变为激发态
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
事件解决互斥问题
特性:没有线程所有权概念 任何线程都可以释放事件 可跨进程 解决互斥和互斥体用法有些类似
函数 | 作用 | 备注 |
CreateEvent | 创建事件 | 可以给事件起名字 可设置两种模式:手工和自动 |
OpenEvent | 打开事件,得到句柄 | 根据名字打开事件 |
setEvent | 释放事件 | 使事件处于激发态 |
ResetEvent | 重置事件 | 会使事件处于非激发态,对手工模式事件有效 |
WaitForSingleObject | 等待事件处于激发态 | 等到激发态后,对自动模式得事件会使其在次处于非激发态 |
事件有两种模式 手动和自动 自动的时候waitforsingnalObject会有副作用 和互斥体很像 也可以当作互斥体来用 手工状态很少用
// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hEvent = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hEvent, -1);
g_n++;
//变为激发态,解锁
SetEvent(g_hEvent);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hEvent, -1);
g_n++;
//变为激发态,解锁
SetEvent(g_hEvent);
}
return 0;
}
int main() {
HANDLE hThread1 = 0, hThread2;
g_hEvent = CreateEvent(
NULL, //安全属性
FALSE, //TRUE:手工设置的事件对象
//FALSE:自动设置的事件对象
TRUE, //TRUE:初始状态是激发态
//FALSE:初始状态就是非激发态
NULL
);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
事件解决同步问题
正因为事件没有线程所有权 任何线程都可以释放事件 所以可以解决线程同步问题
我们之前提过 线程同步也就是说 在想执行线程A 就要先执行线程B,那事件是怎么同步线程的哪
// 14_事件对象解决同步问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include<Windows.h>
HANDLE g_CreateEvent = NULL;
HANDLE g_OperationEvent = NULL;
DWORD WINAPI CreateFileProc(LPVOID lpThreadParameter)
{
printf("打开文件\r\n");
printf("申请缓冲区\r\n");
printf("关闭文件\r\n");
return 0;
}
DWORD WINAPI OperationFileProc(LPVOID lpThreadParameter)
{
printf("获取文件大小\r\n");
printf("读写文件\r\n");
return 0;
}
int main()
{
HANDLE hWife = NULL;
HANDLE hHusband = NULL;
g_CreateEvent = CreateEvent(
NULL, //安全属性
FALSE, //TRUE:手工设置的事件对象
//FALSE:自动设置的事件对象
FALSE, //TRUE:初始状态是激发态
//FALSE:初始状态就是非激发态
NULL
);
g_OperationEvent = CreateEvent(
NULL, //安全属性
FALSE, //TRUE:手工设置的事件对象
//FALSE:自动设置的事件对象
FALSE, //TRUE:初始状态是激发态
//FALSE:初始状态就是非激发态
NULL
);
hWife = CreateThread(NULL, NULL, CreateFileProc, NULL, NULL, NULL);
hHusband = CreateThread(NULL, NULL, OperationFileProc, NULL, NULL, NULL);
WaitForSingleObject(hWife, -1);
WaitForSingleObject(hHusband, -1);
}
代码的执行顺序是有问题的
加上了事件同步代码后
// 14_事件对象解决同步问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include<Windows.h>
HANDLE g_CreateEvent = NULL;
HANDLE g_OperationEvent = NULL;
DWORD WINAPI CreateFileProc(LPVOID lpThreadParameter)
{
printf("打开文件\r\n");
//打开文件成功
SetEvent(g_CreateEvent);
//等待获取文件大小
WaitForSingleObject(g_OperationEvent, -1);
printf("申请缓冲区\r\n");
//缓冲区申请成功
SetEvent(g_CreateEvent);
//等待读写文件
WaitForSingleObject(g_OperationEvent, -1);
printf("关闭文件\r\n");
SetEvent(g_CreateEvent);
return 0;
}
DWORD WINAPI OperationFileProc(LPVOID lpThreadParameter)
{
//等待打开文件
WaitForSingleObject(g_CreateEvent, -1);
printf("获取文件大小\r\n");
//获取文件大小完成
SetEvent(g_OperationEvent);
//等待获取缓冲区
WaitForSingleObject(g_CreateEvent, -1);
printf("读写文件\r\n");
//读写文件成功
SetEvent(g_OperationEvent);
return 0;
}
int main()
{
HANDLE hWife = NULL;
HANDLE hHusband = NULL;
g_CreateEvent = CreateEvent(
NULL, //安全属性
FALSE, //TRUE:手工设置的事件对象
//FALSE:自动设置的事件对象
FALSE, //TRUE:初始状态是激发态
//FALSE:初始状态就是非激发态
NULL
);
g_OperationEvent = CreateEvent(
NULL, //安全属性
FALSE, //TRUE:手工设置的事件对象
//FALSE:自动设置的事件对象
FALSE, //TRUE:初始状态是激发态
//FALSE:初始状态就是非激发态
NULL
);
hWife = CreateThread(NULL, NULL, CreateFileProc, NULL, NULL, NULL);
hHusband = CreateThread(NULL, NULL, OperationFileProc, NULL, NULL, NULL);
WaitForSingleObject(hWife, -1);
WaitForSingleObject(hHusband, -1);
}
顺序正确
信号量
特点: 有信号数概念 只要信号数不为0 那么就处于激发态 waitfor函数对他的副作用就是将信号数减1,最大信号数为1的信号量可以解决互斥问题 最大信号量为1的时候和事件相同
函数 | 作用 | 备注 |
CreateSemaphore | 创建信号量 | 可以给信号量起名字 可以指定最大信号量数和当前信号数 |
OpenSemaphore | 打开信号量 | 根据名字打开信号量 |
ReleaseSemaphore | 释放信号量 | 会增加信号量的信号数,但不会超过最大信号数 |
WaitForSingleObject | 等待信号量处于激发态 | 若处于激发态,则会减少一个信号数,信号数为0,将其设置为非激发态 |
// 15_信号量解决互斥问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hSemaphore = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hSemaphore, -1);
g_n++;
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
//激发态设置为非激发态 并且信号数减1
WaitForSingleObject(g_hSemaphore, -1);
g_n++;
//释放一个信号数
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
return 0;
}
int main() {
HANDLE hThread1 = 0, hThread2;
g_hSemaphore = CreateSemaphore(
NULL,//安全属性
1, //初始信号数量
1, //最大信号数量
NULL
);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
多个信号数的信号量 适用于解决多个线程之间有顺序的问题.最为经典的就是生产者消费者问题