在说数据同步控制之前,先提一个小小的概念,线程安全函数,我们在多线程开发工作中,常常要面对这个概念,究竟什么是线程安全函数,我的理解是该函数在多线程环境中输出结果是稳定的(结果唯一),原来c运行库中没有考虑到多线程环境,因此有一些线程不安全函数:strtok,_wcstok,asctime,_strerror等,为了解决这个问题后来推出了多线程C运行库。从本质上来讲,造成这些问题的根本原因是多线程对一些共享资源的访问是非原子操作(非独占操作),因此,要解决这个问题就是我们今天要说的数据同步控制;
用户态同步控制方法:
1)原子操作:互锁的函数家族
InterlockedExchangeAdd(PLONG plAdend, LONG lIncement);
InterlockedExchangePoint(PVOID* ppvTarget,PVOID pvValue);
2) 关键代码段(用户态同步)
CRITICAL_SECTION g_cs;
InitializeCriticalSection(&g_cs);
EnterCriticalSection(&g_cs);
.......
LeaveCriticalSection(&g_cs);
注意点:
(1) EnterCriticalSection(&g_cs)函数阻塞时间是由注册表中(HLM/system/CurrentControlSet/Control/Session Manager)的CriticalSectionTimeout决定;
(2)对于多个共享资源的访问,使用多个关键代码段,函数中调用EnterCriticalSection的顺序相同,否则有死锁的可能;
(3) TryEnterCriticalSection(&g_cs)该函数不允许调用线程进入等待状态;它立即返回表明资源占用情况,被占用返回FALSE,否则返回TRUE;
关键代码与循环锁
如果线程试图占有另一个线程拥有的关键代码段时,调用线程就立即置为等待状态;线程必须从用户态转入为内核态(大约1000个CPU周期),是很大代价的;为了克服这种直接转入内核态所带来的大代价,引入了循环锁,因此当EnterCriticalSection函数被调用时,它先使用循环锁进行循环,以便多次取得该资源,只有所有尝试失败后,该线程才进入等待状态;目前对多处理器机器才有改善性能,单处理器循环锁没有任何性能改进,因为在循环过程中,拥有资源的线程根本无法释放;
引入循环锁的关键代码函数
(1)InitalizeCriticalSectionAndSpinCount(&g_cs,1000);//如果单处理器的话,1000会被忽略成0;
改变关键代码段循环次数
(2)SetCriticalSectionSpinCount(&g_cs,10000);
(3)关键代码的错误处理
建议使用结构化异常处理方法;
上面介绍的都是用户态的同步方式,其局限性1)互锁家族函数只能对单值进行操作,根本不能使线程进入等待状态;2)关键代码段速度快,能使线程进入等待状态,但是无法设置等待超时,容易使之陷入死锁状态;下面介绍内核对象的同步方式
常见的内核对象
1)作业(job),进程,线程
2)文件,文件修改通知
3)互斥对象,事件,信号,可等待的定时器
等待函数 WaitForSingleObject(handle,timeout),WaitForMultipleObject(...)
等待函数可以使线程自愿进入等待状态,直到一个特定的内核对象为已通知状态为止;
等待的副作用
就是等待成功后会改变内核对象的当前通知,将内核对象置为未通知状态;等待的副作用是有用的,它保证多个线程等待同一内核对象时只从中唤醒一个,至于是哪一个,那就去问问microsoft啦:)
作业,进程,线程
一个布尔值表示其状态,运行时为未通知状态,终止时为通知状态
事件
比线程稍微复杂,其包含两个布尔值,除了通知状态外,还有自动重置标志;当自动重置的事件得到通知时,等待该事件的所有线程中一个线程变为可调度线程,当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程(全部激活);
CreateEvent(...),OpenEvent(...),CloseHandle(...),SetEvent(...),ResetEvent(...);
等待定时器
在某个时间或按规定时间间隔发出自己的信号通知的内核对象;
CreateWaitableTimer(),SetWaitableTimer()(只有调用这个函数才能设置定时器的通知发出)
信号内核对象
除了使用计数外,信号还包含两个带符号的32位值,一个是最大资源数量,一个是当前资源数量;其使用规则:
1)如果当前资源数量大于0时,信号发出通知;
2)如果当前资源数量等于0时,则不发出信号通知;
3)系统决不允许当前资源数量为负值;
4)当前资源数量决不能大于最大资源数量;
CreateSemaphore(...);//创建一个信号对象;
ReleaseSemaphore(...);//使信号当前使用资源数量进行递增;
等待信号的副作用是使信号的当前使用资源数递减1,这正是我们希望得到的:);
互斥对象
顾名思义,互斥对象就是确保线程拥有对单个资源的互斥访问权;其行为特性跟关键代码段一样;
CreateMetux(...)//创建一个互斥对象;
OpenMetux(...);//打开一个互斥对象;
ReleaseMetux(...);//释放一个互斥对象;
整个线程同步控制就介绍这么,下面举一个简单例子,利用互斥对象和信号对象实现一个线程安全的queue对象;
class CQueue{
public:
struct ELEMENT{
....
};
private:
PELEMENT m_pElements;// array of elements to be processed;
int m_nMaxElements;
HANDLE m_h[2];//metux & semphore handles;
HANDLE &m_hmtx0;
HANDLE &m_hsemNumElements;
public:
CQueue();
~CQueue();
BOOL Apend(PELEMENT pElement,DWORD dwTimeout);
BOOL Remove(PELEMENT pElement,DWORD dwTimeout);
};
BOOL CQueue::Append(PELEMENT pElement,DWORD dwTimeout)
{
DWORD dw = WaitForSingleObject(m_hmtx0,dwTimeout);
if(dw == WAIT_OBJECT_0)
{
LONG lPrevCount;
fOK = ReleaseSemphore(m_hsemNumElements,1,&lPrevCount);
........
Release(m_hmtx0);
}
return TRUE;
}
BOOL CQueue::Remove(PELEMENT pElement,DWORD dwTimeout)
{
BOOL fok=(WaitForMutipleObjects(2,m_h,TRUE,dwTimeout) == WAIT_OBJECT_0);
if(fok)
{
*pElements=m_pElements[0];
MoveMemory(&m_pElements[0],&m_pElements[1],sizeof(ELEMENT)*(m_nMaxElements -1));
ReleaseMetux(m_hmtx0);
}
return TRUE;
}
that's over! : -)