Win32线程安全问题.同步函数

时间:2023-12-25 15:19:01

              线程安全问题.同步函数

一丶简介什么是线程安全

  通过上面几讲.我们知道了线程怎么创建.线程切换的原理(CONTEXT结构) 每个线程在切换的时候都有自己的堆栈.

但是这样会有安全问题. 为什么?  我们每个线程都使用自己的局部变量这个是没有安全问题的. 但是线程可能会使用全局变量.这样很有可能会产生安全问题.为什么是很有可能.

1.有全局变量的情况下.有可能会有安全问题.

2.对全局变量进行写操作.则一定有安全问题.

上面两个条件都具备,线程才是不安全的.

为什么是不安全的.

试想一下. 如果这个全局变量在更改.另一个线程也更改了.最后则会出现两个线程同时更改这个全局变量. 问题就会出现在这.

例如以下代码:

// 临界区同步函数.cpp : 定义控制台应用程序的入口点。
// #include "stdafx.h"
#include <Windows.h>
DWORD g_Number = ;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
while (g_Number > )
{
printf("+++剩下Number个数 = %d\r\n", g_Number);
g_Number--;
printf("+++当前的Number个数 = %d\r\n", g_Number);
}
return ;
} DWORD WINAPI MyThreadFun2(LPVOID lParame)
{
while (g_Number > )
{
printf("***剩下Number个数 = %d\r\n", g_Number);
g_Number--; //产生线程安全问题
printf("***当前的Number个数 = %d\r\n", g_Number);
}
return ;
} int main(int argc,char *argv[])
{
HANDLE hThreadHand[] = { NULL };
hThreadHand[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, , NULL);
hThreadHand[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, , NULL); //创建两个线程
WaitForMultipleObjects(, hThreadHand, TRUE, INFINITE); printf("Number个数 = %d \r\n", g_Number);
system("pause");
return ;
}

上面的代码很简单. 看下运行结果

Win32线程安全问题.同步函数

为什么会产生这个问题.原因是.在线程中我们有个地方

while(全局变量 > 0) 则会执行下边代码. 但是很有可能执行完这一句. 线程发生了切换. 去执行另一个线程去了. 最终会产生这样的结果.

如果看反汇编.则会发现 全局变量--的地方.汇编代码 并不是一局. 如果发生线程切换则会出现错误.

Win32线程安全问题.同步函数

首先获取全局变量的值.

然后sub -1

最后重新赋值.

很有可能在sun eax 1的时候就发生了切换. 这样就有安全问题了.为了解决这些问题.我们必须想办法. 所以Windows提供了一组线程同步的函数.

二丶线程同步函数之临界区

什么时候临界区. 临界区的意思就是 这一个区域我给你锁定.当前有且只能有一个线程来执行我们临界区的代码.

而临界资源是一个全局变量

临界区的使用步骤.

1.创建全局原子变量.

2.初始化全原子变量

3.进入临界区

4.释放临界区.

5.删除临界区.

具体API:

  1.全局原子变量

CRITICAL_SECTION g_cs;  //直接创建即可.不用关心内部实现.

  2.初始化全局原子变量.InitializeCriticalSection

    _Maybe_raises_SEH_exception_ VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //传入全局原子变量的地址
);

3.使用的API 进入临界区.

void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //全局原子变量
);

下面还有一个. 是尝试无阻塞模式进入临界区. 意思就是内部加了一个判断.是否死锁了.

BOOL TryEnterCriticalSection(                  返回吃持有的临界区对象.如果成功的情况下.
LPCRITICAL_SECTION lpCriticalSection
);

  4.使用API 释放临界区.

  

void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //全局原子对象
);

  5.删除临界区对象.

void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);

代码例子:

// 临界区同步函数.cpp : 定义控制台应用程序的入口点。
// #include "stdafx.h"
#include <Windows.h>
//创建临界区结构
CRITICAL_SECTION g_cs; DWORD g_Number = ;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
EnterCriticalSection(&g_cs); //进入临界区
while (g_Number > )
{
printf("+++剩下Number个数 = %d\r\n", g_Number);
g_Number--;
printf("+++当前的Number个数 = %d\r\n", g_Number);
}
LeaveCriticalSection(&g_cs);
return ;
} DWORD WINAPI MyThreadFun2(LPVOID lParame)
{
EnterCriticalSection(&g_cs); //进入临界区
while (g_Number > )
{
printf("***剩下Number个数 = %d\r\n", g_Number);
g_Number--; //while语句内就是临界区了.有且只能一个线程访问.
printf("***当前的Number个数 = %d\r\n", g_Number);
}
LeaveCriticalSection(&g_cs);
return ;
} int main(int argc,char *argv[])
{
//初始化临界区全局原子变量
InitializeCriticalSectionAndSpinCount(&g_cs, 0x00000400);
//InitializeCriticalSection(&g_cs); //初始化临界区.两个API都可以. HANDLE hThreadHand[] = { NULL };
hThreadHand[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, , NULL);
hThreadHand[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, , NULL); //创建两个线程
WaitForMultipleObjects(, hThreadHand, TRUE, INFINITE); DeleteCriticalSection(&g_cs); //删除临界区. printf("+Number个数 = %d \r\n", g_Number);
system("pause");
return ;
}

官方MSDN例子:

链接:  https://docs.microsoft.com/zh-cn/windows/desktop/Sync/using-critical-section-objects

三丶线程同步之互斥体

1.临界区缺点.以及衍生出来的跨进程保护

上面讲了临界区. 但是我们的临界资源是一个全局变量.例如下图:

Win32线程安全问题.同步函数

如果我们的临界资源是一个文件. 需要两个进程都要访问怎么办? 此时临界区已经不可以跨进程使用了.

2.跨进程控制.

  跨进程控制就是指 不同进程中的多线程控制安全..比如A进程访问临界资源的时候. B进程不能访问. 因为临界区的 令牌.也就是我们说的全局原子变量.只能在应用层.

但是如果放到内核中就好办了. 如下图所示

  Win32线程安全问题.同步函数

A进程的线程从内核中获取互斥体. 为0 还是为1. B进程一样. 如果为 0 则可以进行访问临界资源.  访问的时候.互斥体则设置为1(也就是令牌设置为1)这样B进程就获取不到了.自然不能访问

临界区资源了.

3.互斥体操作API

  既然明白了互斥体是一个内核层的原子操作.那么我们就可以使用API 进行操作了.

操作步骤.

    1.创建互斥体. 信号量设置为有信号的状态    例如全局的原子变量现在是有信号.是可以进行访问的.

    2.获取信号状态. 如果有信号则进入互斥体临界区执行代码.此时互斥体信号为无信号. 也就是说别的进程访问的时候.因为没有信号.执行不了代码.

    3.释放互斥体. 信号状态为有信号. 此时别的进程信号已经有了.所以可以进行访问了.

具体API:

1.创建互斥体

HANDLE CreateMutexA(
LPSECURITY_ATTRIBUTES lpMutexAttributes, SD安全属性.句柄是否可以继承.每个内核对象API都拥有.
BOOL bInitialOwner, 初始的信号量状态. false为有信号. 获取令牌的时候可以获取到. True为无信号. 且如果为True互斥体对象为线程拥有者.
LPCSTR lpName 全局名字. 根据名字寻找互斥体.
);

2.获取令牌.

  

DWORD WaitForSingleObject(
HANDLE hHandle, 等待的内核对象
DWORD dwMilliseconds 等待的时间
);

调用此函数之后.信号为无信号.别的进程是进入不了互斥体临界区的.

3.释放互斥体

  

BOOL ReleaseMutex(
HANDLE hMutex
);

调用完比之后.互斥体为有信号.可以使用了.

代码例子:

  两个工程代码是一样的.贴一份出来.

#include "stdafx.h"
#include <Windows.h>
//创建临界区结构 int main(int argc,char *argv[])
{
//初始化临界区全局原子变量
HANDLE MutexHandle = CreateMutex(NULL, FALSE, TEXT("AAA")); //创建互斥体. 信号量为0. 有信号的状态.wait可以等待 WaitForSingleObject(MutexHandle,INFINITE); for (size_t i = ; i < ; i++)
{
Sleep();
printf("A进程访问临街资源中临街资源ID = %d \r\n", i);
} ReleaseMutex(MutexHandle);
return ;
}

先运行A进程在运行B进程. 则B进程处于卡死状态.

Win32线程安全问题.同步函数

实现了同步. 除非A进程释放互斥体句柄使信号变为有信号.此时才可以访问B

官方代码例子:

  

#include <windows.h>
#include <stdio.h> #define THREADCOUNT 2 HANDLE ghMutex; DWORD WINAPI WriteToDatabase( LPVOID ); int main( void )
{
HANDLE aThread[THREADCOUNT];
DWORD ThreadID;
int i; // Create a mutex with no initial owner ghMutex = CreateMutex(
NULL, // default security attributes
FALSE, // initially not owned 有信号
NULL); // unnamed mutex 不需要跨进程使用.所以不用名字 if (ghMutex == NULL)
{
printf("CreateMutex error: %d\n", GetLastError());
return ;
} // Create worker threads for( i=; i < THREADCOUNT; i++ )
{
aThread[i] = CreateThread( //创建 THREADCOUNT个线程
NULL, // default security attributes
, // default stack size
(LPTHREAD_START_ROUTINE) WriteToDatabase,
NULL, // no thread function arguments
, // default creation flags
&ThreadID); // receive thread identifier if( aThread[i] == NULL )
{
printf("CreateThread error: %d\n", GetLastError());
return ;
}
} // Wait for all threads to terminate WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); //等待线程执行完毕 // Close thread and mutex handles for( i=; i < THREADCOUNT; i++ )
CloseHandle(aThread[i]); CloseHandle(ghMutex); return ;
} DWORD WINAPI WriteToDatabase( LPVOID lpParam )
{
// lpParam not used in this example
UNREFERENCED_PARAMETER(lpParam); DWORD dwCount=, dwWaitResult; // Request ownership of mutex. while( dwCount < )
{
dwWaitResult = WaitForSingleObject( //线程内部等待互斥体.因为一开始为FALSE所以有信号.第一次执行线程的时候则会执行.
ghMutex, // handle to mutex
INFINITE); // no time-out interval switch (dwWaitResult)
{
// The thread got ownership of the mutex
case WAIT_OBJECT_0:
__try {
// TODO: Write to the database
printf("Thread %d writing to database...\n",
GetCurrentThreadId());
dwCount++;
} __finally {
// Release ownership of the mutex object
if (! ReleaseMutex(ghMutex)) //执行完毕.释放互斥体.信号量变为有信号. 其余线程等待的时候可以等到则可以继续执行线程代码
{
// Handle error.
}
}
break; // The thread got ownership of an abandoned mutex
// The database is in an indeterminate state
case WAIT_ABANDONED:
return FALSE;
}
}
return TRUE;
}

四丶事件操作API

  相应的管理线程同步操作的.还有事件.

具体API:

  1.创建事件对象

HANDLE CreateEventA(
LPSECURITY_ATTRIBUTES lpEventAttributes, SD安全属性
BOOL bManualReset, 通知类型
BOOL bInitialState, 初始值有信号还是无信号.false无信号
LPCSTR lpName 全局名字
);
返回事件句柄

首先这个函数有点复杂. 主要是第二个跟第三个参数.

第三个参数我们很好理解. 有信号还是无信号.  false为无信号. true为有信号.  这样Wait函数根据有无信号就可以进行线程是否执行了.

主要是第二个参数. 通知类型.这个比较复杂.

通知类型的意思就是指.  如果我们按照以前.我们使用了wait函数. 那么有信号会变为无信号.除非释放才会继续有信号执行.

而现在的通知类型如果为TRUE. 那么wait函数执行的时候.你有信号我不会自动变为无信号了.除非你手动自己更改.

如果通知类型为FALSE 那么则自动设置.有信号使用wait函数接受了.那么就变成无信号了.

2.设置信号状态

上面说了.如果为TRUE. 那么信号就不会自动设置了.那么需要我们手动设置.

具体API

  设置为有信号状态

BOOL SetEvent(
HANDLE hEvent
);

那么相应的也有设置为无信号的状态

BOOL ResetEvent(
HANDLE hEvent
);

3.具体代码例子

初始值为有信号状态. 通知类型为TRUE的情况下.

#include "stdafx.h"
#include <Windows.h>
HANDLE g_hEvent;
DWORD WINAPI MyThreadFun1(LPVOID lparam)
{
WaitForSingleObject(g_hEvent, INFINITE);// 等待事件对象
for (size_t i = ; i < ; i++)
{
printf("A线程执行EIP = %d\r\n", i);
}
return ;
} DWORD WINAPI MyThreadFun2(LPVOID lparam)
{
WaitForSingleObject(g_hEvent, INFINITE);// 等待事件对象 for (size_t i = ; i < ; i++)
{
printf("B线程执行EIP = %d\r\n", i);
}
return ;
} int main(int argc, char *argv[])
{ g_hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); //通知类型为TRUE,则wait函数不手动将信号设置为无信号. HANDLE hThread[] = { NULL };
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, , , NULL);
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, , , NULL); WaitForMultipleObjects(, hThread, TRUE, INFINITE); CloseHandle(hThread[]);
CloseHandle(hThread[]);
CloseHandle(g_hEvent); system("pause");
return ;
}

根据上面代码我们设置的通知类型为TRUE. 那么则AB线程都会执行. 因为wait函数不会将信号设置为无信号了.

结果演示.

Win32线程安全问题.同步函数

但是如果我们设置为信号状态为无信号的情况下.

也就是将上面的创建事件的代码. 的第三个参数设置为FALSE

启动后线程都会被阻塞.

Win32线程安全问题.同步函数

那么如果我们设置通知类型为FALSE .且信号类型是有信号的情况下.

看看其执行结果

Win32线程安全问题.同步函数

可以看到只会执行线程A. 因为通知类型我们改为FALSE. 那么wait函数则会自动设置信号状态了. 也就是说我们创建的事件一开始是有信号的.

首先执行线程A. 线程A 里面的wait函数等待到了有信号. 那么就会执行代码了. 此时信号状态已经设置为无信号了. 所以线程B就会阻塞到wait函数哪里.

如果此时我们想执行完A之后再执行B. 那么可以使用 SetEvent设置信号为有信号.

Win32线程安全问题.同步函数

如果我们用于编程的话.大部分通知类型会改成FALSE. 让它自动设置信号状态为无信号. 我们可以使用API SetEvent设置有信号.

这样编程比较简单. 如果设置通知类型为TRUE. 那么我们就要使用 ResetEvent 跟SetEvent配合了.

如以下图片所示代码:

g_hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);  不截图了.通知类型改为TRUE. 手动设置通知类型.

Win32线程安全问题.同步函数

五丶 预留位.信号量的预留

六丶互斥本质跟同步本质

什么是互斥.什么是同步. 上面我们说了很多线程同步函数.那么是否是真的同步了.不见得. 了解了互斥跟同步的本质.才能更好的编写同步代码.

1.什么是互斥.什么是同步?

  互斥: 互斥就是指一块资源.当前访问的时候有且只有一个线程访问. 比如A访问的时候 B会阻塞.访问不了.

  同步: 同步的意思就是让线程执行顺序是有序的.因为互斥可以保证A访问的时候B访问不了.但有可能A会访问多次.线程无序.此时同步的意思就是 我就想让A执行完在执行B.

这个就是同步.

经典互斥例子.可以以这个例子讲解同步跟互斥.

如下代码:

  

#include "stdafx.h"
#include <Windows.h>
HANDLE g_hMutex;
DWORD g_Money;
DWORD g_MAX = ;
DWORD WINAPI MyThreadFun1(LPVOID lparam)
{
WaitForSingleObject(g_hMutex, INFINITE);// 等待事件对象
for (size_t i = ; i < g_MAX; i++)
{
g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 生产者执行收钱动作.当前金钱 = %d\r\n", dwTid,g_Money);
ReleaseMutex(g_hMutex);
} return ;
} DWORD WINAPI MyThreadFun2(LPVOID lparam)
{
WaitForSingleObject(g_hMutex, INFINITE);// 等待事件对象
//因为通知类型为TRUE.所以我们必须手动设置信号状态为无信号
for (size_t i = ; i < g_MAX; i++)
{
g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 消费者执行花钱动作.当前金钱 = %d\r\n",dwTid, g_Money);
ReleaseMutex(g_hMutex); }
return ;
} int main(int argc, char *argv[])
{ g_hMutex = CreateMutex(NULL, FALSE,NULL); //创建互斥体.信号为有信号 HANDLE hThread[] = { NULL };
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, , , NULL);
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, , , NULL); WaitForMultipleObjects(, hThread, TRUE, INFINITE); CloseHandle(hThread[]);
CloseHandle(hThread[]);
CloseHandle(g_hMutex); system("pause");
return ;
}

一个线程修改为1.一个线程修改为0.那么按照正常逻辑.应该是生产一个.消费一个 .

Win32线程安全问题.同步函数

观看结果.结果是迥然不同的.

并不是生产一个.释放一个.

那如何变成 生产一个消费一个的这种模式那. 那么我们可以写一个简单的例子.判断我们的金钱来进行是否修改.

修改代码为如下代码.

#include "stdafx.h"
#include <Windows.h>
HANDLE g_hMutex;
DWORD g_Money = ;
DWORD g_MAX = ;
DWORD g_IsChange;
DWORD WINAPI MyThreadFun1(LPVOID lparam)
{ for (size_t i = ; i < g_MAX; i++)
{
WaitForSingleObject(g_hMutex, INFINITE);// 等待事件对象
if (g_Money == )
{
g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 生产者执行收钱动作.当前金钱 = %d\r\n", dwTid, g_Money); }
else
{
i--; //因为如果不想等.循环次数会浪费.所以-1次.不让它浪费.
}
ReleaseMutex(g_hMutex);
} return ;
} DWORD WINAPI MyThreadFun2(LPVOID lparam)
{ //因为通知类型为TRUE.所以我们必须手动设置信号状态为无信号
for (size_t i = ; i < g_MAX; i++)
{
WaitForSingleObject(g_hMutex, INFINITE);// 等待信号
if (g_Money == )
{
g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 消费者执行花钱动作.当前金钱 = %d\r\n", dwTid, g_Money);
}
else
{
i--;
} ReleaseMutex(g_hMutex); }
return ;
} int main(int argc, char *argv[])
{ g_hMutex = CreateMutex(NULL, FALSE,NULL); HANDLE hThread[] = { NULL };
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, , , NULL);
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, , , NULL); WaitForMultipleObjects(, hThread, TRUE, INFINITE); CloseHandle(hThread[]);
CloseHandle(hThread[]);
CloseHandle(g_hMutex); system("pause");
return ;
}

代码结果演示

Win32线程安全问题.同步函数

发现已经实现了同步.但是这样写是有问题的. 浪费了大量的时间.

因为当线程执行的时候.如果判断不是1则会继续循环.而没有释放信号. 而我们要实现的则是.如果没有.则给下一个线程继续执行.且保证有序.

所以上面的代码虽然实现了但是还是不能保证同步.会浪费线程的时间.

如果要实现同步.那么只能用事件来实现了. 所以说 同步函数各有优缺点.

实现同步的方法.

1.创建两个Event对象.一个有信号.一个无信号.且通知类型都是自动设置的.也就是参数2为FALSE.

2.当A线程执行完毕之后.使用SetEvent给B线程设置信号状态为有信号.这样B就会执行. B执行完之后给A设置.这样A就执行.相当于交错设置.不浪费时间片.

如下代码演示:

#include "stdafx.h"
#include <Windows.h>
HANDLE g_hEventOne;
HANDLE g_hEventTwo; DWORD g_Money = ;
DWORD g_MAX = ;
DWORD g_IsChange;
DWORD WINAPI MyThreadFun1(LPVOID lparam)
{ for (size_t i = ; i < g_MAX; i++)
{
WaitForSingleObject(g_hEventOne, INFINITE);// 等待事件对象 g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 生产者执行收钱动作.当前金钱 = %d\r\n", dwTid, g_Money); SetEvent(g_hEventTwo); } return ;
} DWORD WINAPI MyThreadFun2(LPVOID lparam)
{ //因为通知类型为TRUE.所以我们必须手动设置信号状态为无信号
for (size_t i = ; i < g_MAX; i++)
{
WaitForSingleObject(g_hEventTwo, INFINITE);// 等待事件对象 g_Money = ; //消费者修改金钱
DWORD dwTid = GetCurrentThreadId();
printf("线程%d 消费者执行消费动作.当前金钱 = %d\r\n", dwTid, g_Money); SetEvent(g_hEventOne); } return ;
} int main(int argc, char *argv[])
{ g_hEventOne = CreateEvent(NULL, FALSE, TRUE, NULL); //A线程设置为有信号则A线程先执行.
g_hEventTwo = CreateEvent(NULL, FALSE, FALSE, NULL);//B线程设置为无信号.则B线程不会先执行.要等A线程通知才可以执行. HANDLE hThread[] = { NULL };
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun1, , , NULL);
hThread[] = CreateThread(NULL, , (LPTHREAD_START_ROUTINE)MyThreadFun2, , , NULL); WaitForMultipleObjects(, hThread, TRUE, INFINITE); CloseHandle(hThread[]);
CloseHandle(hThread[]);
CloseHandle(g_hEventOne);
CloseHandle(g_hEventTwo); system("pause");
return ;
}

代码执行结果

Win32线程安全问题.同步函数

实现了同步有序