30win32编程基础——线程安全之临界区
1、我们知道每次当函数执行的时候,都会把函数的参数压栈
2、然后提升堆栈
3、在堆栈中使用局部变量和参数,参与函数的执行。
那么线程执行的时候,其实就是执行1个线程回调函数,也会进行同样的操作,因此每个线程都会有自己的堆栈的空间。
换句话说:
不管同时启动多少个线程,每个线程都会有自己的堆栈空间。
因此,如果多线程只使用局部变量,是不存在线程安全问题的。
那么问题很明显,由于程序中会使用全局变量的缘故,这样就产生了线程安全的问题。
最简单的就是:2线程访问1个全局变量,这里的访问如何只是读肯定是没问题,就怕存在2个读写的问题,这个时候就要考虑同步的问题。
例如:
2个线程都读 ----------------------->不存在线程安全问题
1个线程在读,1个线程在写。 ------------->存在
2个线程都在写 ----------------------->存在线程安全问题
为什么会存在这种问题呢?
DWORD WINAPI ThreadAdd1Proc(__in LPVOID lpParameter){
TCHAR sz[20];
GetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz,20);
int TT;
swscanf(sz,TEXT("%d"),&TT);
while(TT<10000)
{
TT++;
swprintf(sz,19,TEXT("%d"),TT);
SetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz);
//Sleep(100);
}
ExitThread(0x4);
return 0;
}
很简单,因此程序的执行是要分配时间片的,并且代码是一条一条的执行的,因此涉及线程执行的时候,一定要考虑到代码不是瞬间执行完毕的,
例如上边的线程代码,你要考虑到在执行任何一句代码的时候,都可能发生线程切换的问题(可能正执行的时候,分配的CPU时间片没了),执行另外的线程,这样就会出现线程安全问题。

例如:2个线程同时对全局变量M=0,每个线程对M都加10000次,最后停下来的结果可能就不是20000。为什么呢?
同时写的问题:
当A线程把改全局变量M先读出来(这个时候M=10),准备改为11,但是在写入之前,A线程时间到了,切换到B线程。
这个时候B线程也把全局变量M读出来,由于A线程还能修改成功,B读出来的也是10,因此当B改的时候。把M改为11。然后再切换去执行A线程,继续写入还是11。
DWORD WINAPI ThreadAdd1Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int TT=0;
int dwCount;
while(TT<10000)//这个值分别改为10、100、1000、10000观察结果
{
GetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz,20);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
SetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz);
TT++;
}
ExitThread(0x4);
return 0;
}
DWORD WINAPI ThreadAdd2Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int TT=0;
int dwCount;
while(TT<10000)//这个值分别改为10、100、1000、10000观察结果
{
GetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz,20);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
SetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz);
TT++;
}
ExitThread(0x5);
return 0;
}
设置2个按钮,同时往编辑框里面加,每次加1,改变循环次数为10、100、1000、10000观察运行结果。
发现当循环次数为10、100、1000的时候,结果是我们想的那样20、200、2000
但是当为10000的时候,为什么结果不是20000,而是11029(你的可能是别的结果)
这就是上边同时写产生的问题。
一个读一个写的问题(类似于生产者消费者问题):
当线程1取出X=0的值,此时X为0,当线程1还没有把X的值写为1的时候,切换到线程2,线程2把X的值读取出来,进行操作。
那怎么解决呢?
使用临界区,临界区是什么?
《windows核心编程》中举的例子:飞机上只有1个厕所,是如何使用的呢?
进入厕所,挂个牌子显示有人,用完之后取下牌子,然后出来。
临界区的设计和这个原理一样:
使用之前,先初始化(InitializeCriticalSection相当于设置1个令牌)
用的时候使用EnterCriticalSection,相当于拿到令牌
…………代码………………
不用的时候使用LeaveCriticalSection(相当于释放令牌)
DeleteCriticalSection删除令牌,当最后程序释放资源的时候。
简单总结 :
InitializeCriticalSection 创建令牌
EnterCriticalSection 拿令牌
LeaveCriticalSection 归还令牌
DeleteCriticalSection 销毁令牌
我们使用临界区来解决上边的2个按钮同时加的问题。
记住:临界区并不是锁住资源了,而是当线程使用临界区会改变临界区结构体中的值,这样当别的线程遇到这个临界区变量,就会检查临界区结构中的值,就会挂起。
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount; //-1表示未占用临界区,大于等于0,表示占用
LONG RecursionCount; //LockCount - (RecursionCount -1),此字段当为0的时候,表示下次申请CRITICAL_SECTION临界区会成功,大于0则表示不会成功。
HANDLE OwningThread; // 当前获取临界区的线程id
HANDLE LockSemaphore;
DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
临界区使用的代码示例:
#include <iostream>#include<windows.h>
#include "OutPut.h"
using namespace std;
CRITICAL_SECTION ics;
int icount=0;
DWORD WINAPI ThreadA(LPVOID lp)
{
for(int i=0;i<1000 ;i++ )
{
EnterCriticalSection(&ics);
Sleep(1);
icount++;
LeaveCriticalSection(&ics);
}
return 0;
}
DWORD WINAPI ThreadB(LPVOID lp)
{
for(int i=0;i<1000 ;i++ )
{
EnterCriticalSection(&ics);
Sleep(1);
icount++;
LeaveCriticalSection(&ics);
}
return 0;
}
DWORD WINAPI ThreadC(LPVOID lp)
{
for(int i=0;i<1000 ;i++ )
{
EnterCriticalSection(&ics);
Sleep(1);
icount++;
LeaveCriticalSection(&ics);
}
return 0;
}
DWORD WINAPI ThreadD(LPVOID lp)
{
for(int i=0;i<1000 ;i++ )
{
EnterCriticalSection(&ics);
Sleep(1);
icount++;
LeaveCriticalSection(&ics);
}
return 0;
}
int __stdcall WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
)
{ InitializeCriticalSection(&ics);
HANDLE ha=CreateThread(NULL,0,ThreadA,NULL,0,NULL);
HANDLE hb=CreateThread(NULL,0,ThreadB,NULL,0,NULL);
HANDLE hC=CreateThread(NULL,0,ThreadC,NULL,0,NULL);
HANDLE hD=CreateThread(NULL,0,ThreadD,NULL,0,NULL);
::WaitForSingleObject(ha,INFINITE);
::WaitForSingleObject(hb,INFINITE);
::WaitForSingleObject(hC,INFINITE);
::WaitForSingleObject(hD,INFINITE);
OutputDebugStringF("%d\n",icount);
DeleteCriticalSection(&ics);
return 0;
}
临界区使用问题:
1、循环外还是循环内使用。这个很重要,一定要在操作全局变量的位置时候,不要扩大使用范围。
DWORD WINAPI ThreadA(LPVOID lp)
{
//EnterCriticalSection(&ics);
for(int i=0;i<1000 ;i++ )
{
EnterCriticalSection(&ics);
OutputDebugStringF("A:%d,%d,%d\n",ics.LockCount,ics.RecursionCount,ics.OwningThread);
Sleep(1000);
icount++;
LeaveCriticalSection(&ics);
}
// LeaveCriticalSection(&ics);
return 0;
}//如果放到循环外边,这起不到多线程的作用,循环外边相当于单线程。即A线程执行完再执行B。但是放在循环内还是多线程。
2、
DWORD WINAPI ThreadA(LPVOID lp)
{
EnterCriticalSection(&ics);
xxxx代码xxxxx
xxxx代码xxxxx
使用全局变量X的地方
xxxx代码xxxxx
xxxx代码xxxxx
LeaveCriticalSection(&ics);
}
应该写成这样:
DWORD WINAPI ThreadA(LPVOID lp)
{
xxxx代码xxxxx
xxxx代码xxxxx
EnterCriticalSection(&ics);
使用全局变量X的地方
LeaveCriticalSection(&ics);
xxxx代码xxxxx
xxxx代码xxxxx
}
3、
DWORD WINAPI ThreadA(LPVOID lp)
{
EnterCriticalSection(&ics);
xxxx代码xxxxx
xxxx代码xxxxx
使用全局变量X的地方
xxxx代码xxxxx
xxxx代码xxxxx
LeaveCriticalSection(&ics);
}
DWORD WINAPI ThreadB(LPVOID lp)
{
使用全局变量X的地方
}
这样是达不到不同时访问的效果的。
因此B中没有临界区代码,因此B还是会竞争访问X。
因此临界区不是锁定,而是遇到挂起,但是B线程中没有这段代码,因此B是不会挂起的。
4、
DWORD WINAPI ThreadA(LPVOID lp)
{
EnterCriticalSection(&ics);
全局变量X
全局变量Y
LeaveCriticalSection(&ics);
}
DWORD WINAPI ThreadB(LPVOID lp)
{
EnterCriticalSection(&ics);
全局变量Y
全局变量Z
LeaveCriticalSection(&ics);
}
DWORD WINAPI ThreadC(LPVOID lp)
{
EnterCriticalSection(&ics);
全局变量X
全局变量Z
LeaveCriticalSection(&ics);
}
这样的不对的,应该写成这样:
DWORD WINAPI ThreadA(LPVOID lp)
{
EnterCriticalSection(&ics_X);
全局变量X
LeaveCriticalSection(&ics_X);
EnterCriticalSection(&ics_Y);
全局变量Y
LeaveCriticalSection(&ics_Y);
}
DWORD WINAPI ThreadB(LPVOID lp)
{
EnterCriticalSection(&ics_Y);
全局变量Y
LeaveCriticalSection(&ics_Y);
EnterCriticalSection(&ics_Z);
全局变量Z
LeaveCriticalSection(&ics_Z);
}
DWORD WINAPI ThreadC(LPVOID lp)
{
EnterCriticalSection(&ics_X);
全局变量X
LeaveCriticalSection(&ics_X);
EnterCriticalSection(&ics_Z);
全局变量Z
LeaveCriticalSection(&ics_Z);
}
死锁程序是什么?
自己设计1个死锁程序,死锁最少是有2个线程,你等我,我等你
1、顺序一致
拿到令牌A 拿到令牌B
//这个位置切换B就会死锁
拿到令牌B 拿到令牌A
释放令牌B 释放令牌A
释放令牌A 释放令牌B
避免死锁的方法:
2、不要嵌套使用
用完就释放
拿到令牌A
释放令牌A
拿到令牌B
释放令牌B
1、多个线程拿令牌的顺序一致
拿到令牌A 拿到令牌A
拿到令牌B 拿到令牌B
释放令牌B 释放令牌B
释放令牌A 释放令牌A
死锁可能很长时间不会发生(不一定时间什么发生),但是最后肯定会发生。
1、考虑前边的程序应该如何设计:2个线程同时往编辑框中加,每次加1。
考虑下边的代码中临界区的使用是否合理?
DWORD WINAPI ThreadAdd1Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int TT=0;
int dwCount;
while(TT<10000)//这个值分别改为10、100、1000、10000观察结果
{
EnterCriticalSection(&cs_input);
GetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz,20);
//LeaveCriticalSection(&cs_input);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
//EnterCriticalSection(&cs_input);
SetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz);
LeaveCriticalSection(&cs_input);
TT++;
}
ExitThread(0x4);
return 0;
}
void TestAdd()
{
hanThread=::CreateThread(NULL,NULL,ThreadAdd1Proc,(LPVOID*)1,0,NULL);
}
DWORD WINAPI ThreadAdd2Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int TT=0;
int dwCount;
while(TT<10000)//这个值分别改为10、100、1000、10000观察结果
{
EnterCriticalSection(&cs_input);
GetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz,20);
//LeaveCriticalSection(&cs_input);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
//EnterCriticalSection(&cs_input);
SetDlgItemText(hwndD,IDC_EDIT_INPUT2,sz);
LeaveCriticalSection(&cs_input);
TT++;
}
ExitThread(0x5);
return 0;
}
可以做实验观察一下。
2、设计1个死锁程序
DWORD WINAPI ThreadLock1Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int dwCount;
int i=0;
EnterCriticalSection(&ics_Lock1);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
OutputDebugStringF("111111111111:\n");
while(i<10000)
{
OutputDebugStringF("11111111111***************************\n");
EnterCriticalSection(&ics_Lock2);//这个位置下断点
GlobalX++;
i++;
LeaveCriticalSection(&ics_Lock2);
OutputDebugStringF("111111111111:%d\n",GlobalX);
}
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
LeaveCriticalSection(&ics_Lock1);
return 0;
}
DWORD WINAPI ThreadLock2Proc(__in LPVOID lpParameter){
TCHAR sz[20];
int dwCount;
int j=0;
EnterCriticalSection(&ics_Lock2);
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
OutputDebugStringF("22222222\n");
while(j<10000)
{
OutputDebugStringF("22222222***************************\n");
EnterCriticalSection(&ics_Lock1);//这个位置下断点
GlobalX++;
j++;
LeaveCriticalSection(&ics_Lock1);
OutputDebugStringF("22222222:%d\n",GlobalX);
}
swscanf(sz,TEXT("%d"),&dwCount);
dwCount++;
swprintf(sz,20,TEXT("%d"),dwCount);
LeaveCriticalSection(&ics_Lock2);
return 0;
}
2个线程都会在下断点的位置停下来不动。


浙公网安备 33010602011771号