临时
现在我们回过头来讲WaitForSingleObject这个函数,从前面的例子中我们看到WaitForSingleObject这个函数将等待一个对象变为有信号状态,那么具有信号状态的对象有哪些呢?下面是一部分:
Mutex
Event
Semaphore
Job
Process
Thread
Waitable timer
Console input
互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以我们可以使用WaitForSingleObject来等待进程和线程退出。(至于信号灯,事件的用法我们接下来会讲)我们在前面的例子中使用了WaitForMultipleObjects函数,这个函数的作用与WaitForSingleObject类似但从名字上我们可以看出,WaitForMultipleObjects将用于等待多个对象变为有信号状态,函数原型如下:
DWORD WaitForMultipleObjects(
DWORD nCount, // 等待的对象数量
CONST HANDLE *lpHandles, // 对象句柄数组指针
BOOL fWaitAll, // 等待方式,
//为TRUE表示等待全部对象都变为有信号状态才返回,为FALSE表示任何一个对象变为有信号状态则返回
DWORD dwMilliseconds // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待
);
返回值意义:
WAIT_OBJECT_0 到 (WAIT_OBJECT_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
WAIT_ABANDONED_0 到 (WAIT_ABANDONED_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时表示对象中有一个对象为互斥量,该互斥量因为被关闭而成为有信号状态,使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
WAIT_TIMEOUT:表示超过规定时间。
前面的例子中的如下代码表示等待三个线程都变为有信号状态,也就是说三个线程都结束。
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
此外,在启动和等待进程结束一文中就利用这个功能等待进程结束。
通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:
Semaphore sem=3;
dword threadA(void*)
{
while(sem <= 0)
{// 相当于 WaitForSingleObject
wait ...
}
// sem > 0
// lock the Semaphore
sem -- ;
do functions ...
// release Semaphore
sem ++ ;
return 0;
}
这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:
创建信号灯:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// 安全属性,NULL表示使用默认的安全描述
LONG lInitialCount, // 初始值
LONG lMaximumCount, // 最大值
LPCTSTR lpName // 名字
);
打开信号灯:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, // 存取方式
BOOL bInheritHandle, // 是否能被继承
LPCTSTR lpName // 名字
);
释放信号灯:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // 句柄
LONG lReleaseCount, // 释放数,让信号灯值增加数
LPLONG lpPreviousCount // 用来得到释放前信号灯的值,可以为NULL
);
关闭信号灯:
BOOL CloseHandle(
HANDLE hObject // 句柄
);
可以看出来信号灯的使用方式和互斥量的使用方式非常相似,下面的代码使用初始值为2的信号灯来保证只有两个线程可以同时进行数据库调用:
DWORD threadA(void* pD)
{
int iID=(int)pD;
//在内部重新打开
HANDLE hCounterIn=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam sp 44");
for(int i=0;i<3;i++)
{
printf("%d wait for object\n",iID);
WaitForSingleObject(hCounterIn,INFINITE);
printf("\t\tthread %d : do database access call\n",iID);
Sleep(100);
printf("\t\tthread %d : do database access call end\n",iID);
ReleaseSemaphore(hCounterIn,1,NULL);
}
CloseHandle(hCounterIn);
return 0;
}
//in main function
{
//创建信号灯
HANDLE hCounter=NULL;
if( (hCounter=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
{
//如果没有其他进程创建这个信号灯,则重新创建
hCounter = CreateSemaphore(NULL,2,2,"sam sp 44");
}
//创建线程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//关闭句柄
CloseHandle(hCounter);
}
信号灯有时用来作为计数器使用,一般来讲将其初始值设置为0,先调用ReleaseSemaphore来增加其计数,然后使用WaitForSingleObject来减小其计数,遗憾的是通常我们都不能得到信号灯的当前值,但是可以通过设置WaitForSingleObject的等待时间为0来检查信号灯当前是否为0。
接下来我们讲最后一种同步对象:事件,前面讲的信号灯和互斥量可以保证资源被正常的分配和使用,而事件是用来通知其他进程/线程某件操作已经完成。例如:现在有三个线程:threadA,threadB,threadC,现在要求他们中的部分功能要顺序执行,也就是说threadA执行完一部分后threadB执行,threadB执行完一部分后threadC开始执行。也许你觉得下面的代码可以满足要求:
要求:A1执行完后执行B2然后执行C3,再假设每个任务的执行时间都为1,而且允许并发操作。
方案一:
dword threadA(void*)
{
do something A1;
create threadB;
do something A2;
do something A3;
}
dword threadB(void*)
{
do something B1;
do something B2;
create threadC;
do something B3;
}
dword threadC(void*)
{
do something C1;
do something C2;
do something C3;
}
方案二:
dword threadA(void*)
{
do something A1;
do something A2;
do something A3;
}
dword threadB(void*)
{
do something B1;
wait for threadA end
do something B2;
do something B3;
}
dword threadC(void*)
{
do something C1;
do something C2;
wait for threadB end
do something C3;
}
main()
{
create threadA;
create threadB;
create threadC;
}
方案三:
dword threadA(void*)
{
do something A1;
release event1;
do something A2;
do something A3;
}
dword threadB(void*)
{
do something B1;
wait for envet1 be released
do something B2;
release event2;
do something B3;
}
dword threadC(void*)
{
do something C1;
do something C2;
wait for event2 be released
do something C3;
}
main()
{
create threadA;
create threadB;
create threadC;
}
比较一下三种方案的执行时间:
方案一 方案二 方案三
1 threadA threadB threadC threadA threadB threadC threadA threadB threadC
2 A1 A1 B1 C1 A1 B1 C1
3 A2 B1 A2 C2 A2 B2 C2
4 A1 B2 A3 A3 B3 C3
5 B3 C1 B2
6 C2 B3
7 C3 C3
8
可以看出来方案三的执行时间是最短的,当然这个例子有些极端,但我们可以看出事件对象用于通知其他进程/线程某件操作已经完成方面的作用是很大的,而且如果有的任务要在进程尖进行协调采用等待其他进程中线程结束的方式是不可能实现的。此外我也希望通过这个例子讲一点关于分析线程执行效率的方法。