编写多线程程序需要考虑的同步问题,《Windows核心编程》第八章提出了几个简单的方法进行线程同步!
先看一下一段程序:
#include <Windows.h>
#include <process.h>
#include <tchar.h>
LONG g_x;
unsigned int WINAPI threadFun(PVOID pvParam)
{
for(int i=0;i<100000;i++)
{
g_x++;
}
return 0;
}
int _tmain(int argc,TCHAR** argv,TCHAR** env)
{
HANDLE threadGroup[5];
for(int i=0;i<5;i++)
threadGroup[i]=(HANDLE)_beginthreadex(NULL,0,threadFun,NULL,0,NULL);
Sleep(1000);
_tprintf(TEXT("%d\n"),g_x);
system("pause");
return 0;
}
毫无疑问上面的程序对g_x访问存在线程同步问题,主线程不能得到正确输出500000!由于不能保证只有一个线程在访问g_x,因此这个公共资源可能会被破坏!
为了保证访问公共对象的有序性,书中提出了原子访问的概念:一个线程在访问某个资源时能够保证没有其他线程同时访问这个资源!
针对上面的程序,我们应该保证随时只有一个线程在执行g_x++;
Windows提供了一组函数,保证我们访问一个32位或者64位值时是满足原子访问的!
Interlocked系列函数成员有下面主要几个:
int InterlockedExchangeAdd(PLONG volatile plAddend,LONG lIncrement);
//使plAddend指向的长整型,增加lIncrement
int InterlockedExchangeAdd64(PLONG volatile plAddend,LONG lIncrement);
//使plAddend指向的长整型,增加lIncrement
PVOID InterlockedExchange(PLONG volatile plAddend,LONG lValue);
//把plAddend指向的长整型,替换为lValue
PVOID InterlockedExchange64(PLONG volatile plAddend,LONG lValue);
//把plAddend指向的长整型,替换为lValue
PVOID InterlockedExchangePointer(PLONG volatile plAddend,PVOID pvValue);
//把plAddend指向的长整型,替换为pvValue指向的值
PLONG InterlockedCompareExchangePointer(PLONG plDestination,LONG lExchange,LONG lComparand);
//*plDestination==lComparand ? : lExchange *plDestination
上面的函数,可以在X86和X64系统下有不同的表现,这点请注意!
同时关于关键字volatile可以防止编译器对C++函数进行优化,使程序始终从内存读取数据(P206)
对上面的程序用Inetlocked函数修改一下基本就能实现要求了!!
unsigned int WINAPI threadFun(PVOID pvParam)
{
for(int i=0;i<100000;i++)
{
InterlockedExchangeAdd(&g_x,1);
}
return 0;
}
但是,实际问题中我们的共享资源往往不是简单的整数,不能使用Interlocked函数。这时,我们能想到的方法:创建一个bool量flag,如果flag==true,表示有程序在访问共享资源,flag==false表示资源可用!想要访问资源的线程,会不断轮询flag,直到资源可用!
尝试这样修改线程函数:
我们可以用InterLocked函数实现这个要求
unsigned int WINAPI threadFun(PVOID pvParam)
{
for(int i=0;i<100000;i++)
{
while(InterlockedExchange(&flag,TRUE)==TRUE);
g_x++;
InterlockedExchange(&flag,FALSE);
}
return 0;
}
InterlockedExchange把flag甚至为TRUE的同时返回flag之前的值!!!书中称上面的同步方法为“旋转锁”
使用InterLocked函数时我们要保证变量的地址是对齐的,否则函数会失败!P200
“旋转锁”假设线程对公共资源的访问是时间的,否则旋转锁不断轮询flag会大量占用系统的时间!
如果,线程对公共资源的访问是长期,我们需要一种方案:让等待线程不可调度,从而交出CPU,而不是不断的轮询!!关键段提供了这样一种方案!
#include <Windows.h>
#include <process.h>
#include <tchar.h>
LONG g_x;
LONG flag=false;
CRITICAL_SECTION g_cs;//定义个关键段变量
unsigned int WINAPI threadFun(PVOID pvParam)
{
for(int i=0;i<100000;i++)
{
EnterCriticalSection(&g_cs);
g_x++;
LeaveCriticalSection(&g_cs);
}
return 0;
}
int _tmain(int argc,TCHAR** argv,TCHAR** env)
{
InitializeCriticalSectionAndSpinCount(&g_cs,1000);//同时使用关键端和旋转锁
HANDLE threadGroup[5];
for(int i=0;i<5;i++)
threadGroup[i]=(HANDLE)_beginthreadex(NULL,0,threadFun,NULL,0,NULL);
Sleep(1000);
_tprintf(TEXT("%d\n"),g_x);
DeleteCriticalSection(&g_cs);//记住要删除它
system("pause");
return 0;
}
使用关键端是我们定义一个全局的结构,操作系统保证之一个线程在运行同一个关键段EnterCriticalSection和LeaveCriticalSection函数之间的代码!!由于关键端内部通过事件内核对象实现,所以使用完了以后应该记得删除!!
关键段与旋转锁的区别在于:一旦访问受阻,线程会被转入内核模式变为不可调度,直达共享资源就绪,系统会唤醒线程!!通过这样的方式提高了效率!!
如果,对资源的访问是短时的,那么关键段的效果是不如旋转锁的!!!因此我们可以考虑两者结合使用:
InitializeCriticalSectionAndSpinCount(&g_cs,1000);//同时使用关键端和旋转锁
1000表示我们轮询1000次后,如果资源仍未就绪线程就进入内核模式!!
还有一个类似于关键段的结构,称为读写锁SRWLock,它允许读线程共享变量,而写线程独占变量!
SRWLock的使用和关键段类似!!!