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个线程都会在下断点的位置停下来不动。

 

 

posted @ 2023-11-01 23:23  一日学一日功  阅读(160)  评论(0)    收藏  举报