Windows中线程间同步的方法主要有:事件(Event)、临界区(Critical Section)、互斥量(Mutex)和信号灯(Semaphore)。
1)使用事件对象进行线程间同步:
在使用CreateEvent函数创建事件对象时,将bManualReset参数设置为FALSE,然后在需要独占操作的代码前面加上一个WaitForSingleObject函数,后面加上一个SetEvent即可。
由于bManualReset参数为FALSE,这样当某个线程等待到Event后,Event对象的状态马上就变为复位状态,这样其他线程执行到WaitForSingleObject时就全部处于等待中了,当活动的线程操作完毕后,执行SetEvent函数,Event对象的状态才恢复到置位,这样其他等待的线程才会有一个能继续操作。
SetEvent函数原型如下:
HANDLE WINAPI CreateEvent(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, //安全描述符
__in BOOL bManualReset, //指定事件对象是否需要手动复位
__in BOOL bInitialState, //指定事件对象创建时的初始状态,为TRUE表示初始状态是置位状态;
//为FALSE表示初始状态是复位状态
__in_opt LPCTSTR lpName //指定事件对象名称
);
当一个事件被创建后,程序就可以通过SetEvent和ResetEvent函数来设置事件的状态:
BOOL WINAPI SetEvent(
__in HANDLE hEvent
);
BOOL WINAPI ResetEvent(
__in HANDLE hEvent
);
事件对象不仅可以用于一个进程中多线程同步,还可以用于多进程中的线程同步,只要在创建事件对象时指定事件名字,在其他进程中可以使用OpenEvent函数根据名称打开该事件对象进行使用:
HANDLE WINAPI OpenEvent(
__in DWORD dwDesiredAccess, //对事件对象的访问限制
__in BOOL bInheritHandle, //子进程是否继承该事件句柄
__in LPCTSTR lpName //事件对象的名字
);
实例代码如下:
#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 4
HANDLE ghWriteEvent;
HANDLE ghThreads[THREADCOUNT];
DWORD WINAPI ThreadProc(LPVOID);
void CreateEventsAndThreads(void)
{
int i;
DWORD dwThreadID;
// Create a manual-reset event object. The write thread sets this
// object to the nonsignaled state when it finishes writing to a
// shared buffer.
ghWriteEvent = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
TEXT("WriteEvent") // object name
);
if (ghWriteEvent == NULL)
{
printf("CreateEvent failed (%d)/n", GetLastError());
return;
}
// Create multiple threads to read from the buffer.
for(i = 0; i < THREADCOUNT; i++)
{
// TODO: More complex scenarios may require use of a parameter
// to the thread procedure, such as an event per thread to
// be used for synchronization.
ghThreads[i] = CreateThread(
NULL, // default security
0, // default stack size
ThreadProc, // name of the thread function
NULL, // no thread parameters
0, // default startup flags
&dwThreadID);
if (ghThreads[i] == NULL)
{
printf("CreateThread failed (%d)/n", GetLastError());
return;
}
}
}
void WriteToBuffer(VOID)
{
// TODO: Write to the shared buffer.
printf("Main thread writing to the shared buffer.../n");
// Set ghWriteEvent to signaled
if (! SetEvent(ghWriteEvent) )
{
printf("SetEvent failed (%d)/n", GetLastError());
return;
}
}
void CloseEvents()
{
// Close all event handles (currently, only one global handle).
CloseHandle(ghWriteEvent);
}
void main()
{
DWORD dwWaitResult;
// TODO: Create the shared buffer
// Create events and THREADCOUNT threads to read from the buffer
CreateEventsAndThreads();
// At this point, the reader threads have started and are most
// likely waiting for the global event to be signaled. However,
// it is safe to write to the buffer because the event is a
// manual-reset event.
WriteToBuffer();
printf("Main thread waiting for threads to exit.../n");
// The handle for each thread is signaled when the thread is
// terminated.
dwWaitResult = WaitForMultipleObjects(
THREADCOUNT, // number of handles in array
ghThreads, // array of thread handles
TRUE, // wait until all are signaled
INFINITE);
switch (dwWaitResult)
{
// All thread objects were signaled
case WAIT_OBJECT_0:
printf("All threads ended, cleaning up for application exit.../n");
break;
// An error occurred
default:
printf("WaitForMultipleObjects failed (%d)/n", GetLastError());
return;
}
// Close the events to clean up
CloseEvents();
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
DWORD dwWaitResult;
printf("Thread %d waiting for write event.../n", GetCurrentThreadId());
dwWaitResult = WaitForSingleObject(
ghWriteEvent, // event handle
INFINITE); // indefinite wait
switch (dwWaitResult)
{
// Event object was signaled
case WAIT_OBJECT_0:
//
// TODO: Read from the shared buffer
//
printf("Thread %d reading from buffer/n",
GetCurrentThreadId());
break;
// An error occurred
default:
printf("Wait error (%d)/n", GetLastError());
return 0;
}
// Now that we are done reading the buffer, we could use another
// event to signal that this thread is no longer reading. This
// example simply uses the thread handle for synchronization (the
// handle is signaled when the thread terminates.)
printf("Thread %d exiting/n", GetCurrentThreadId());
return 1;
}
2)使用临界区对象进行线程间同步:
临界区对象(Critical Section)是定义在数据段中的一个CRITICAL_SECTION结构,它的维护和测试工作都是由Windows来完成的。结构应该定义成全局变量,因为在各线程中都有测试它。
定义了CRITICAL_SECTION结构后,必须首先对其进行初始化:
void WINAPI InitializeCriticalSection(
__out LPCRITICAL_SECTION lpCriticalSection
);
由于Windows系统保证临界区对象同时只能被一个线程进入,所以在需要独占操作的代码前加上进入临界区的操作,在代码后面加上离开临界区的操起,就可以保证操作的独占性了。
进入临界区的工作由函数EnterCriticalSection来完成:
void WINAPI EnterCriticalSection(
__inout LPCRITICAL_SECTION lpCriticalSection
);
如果当前其他线程拥有临界区,函数不会返回;反之如果函数返回就表示现在可以独占数据了。
当完成操作时,将临界区交还给Windows,以便其他线程可以申请使用,这个工作由函数LeaveCriticalSection来完成:
void WINAPI LeaveCriticalSection(
__inout LPCRITICAL_SECTION lpCriticalSection
);
最后,当程序不再需要临界区时,必须使用函数DeleteCriticalSection将它删除:
void WINAPI DeleteCriticalSection(
__inout LPCRITICAL_SECTION lpCriticalSection
);
与事件对象不同,由于临界区对象无法命名,所以无法跨进程使用,但正是因为事件对象可以跨进程使用,需要占用更多的资源,所以相比之下临界区对象在速度上的优势很明显。
使用临界区的缺点在于,如果某个线程进入临界区后挂掉了,那么将无法被其他等待的线程检测到,因为这些线程都被陷在EnterCriticalSection函数中了。而使用事件则不同,它可以在WaitForSingleObject函数中指定一个超时时间。
一般在对速度要求比较高,且不必跨进程进行同步的情况下,建议使用临界区对象。
实例代码片段如下:
// Global variable
CRITICAL_SECTION CriticalSection;
void main()
{
...
// Initialize the critical section one time only.
if (!InitializeCriticalSectionAndSpinCount(&CriticalSection,
0x80000400) )
return;
...
// Release resources used by the critical section object.
DeleteCriticalSection(&CriticalSection);
}
DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...
// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);
// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);
...
}
3)使用互斥量对象进行线程间同步:
和临界区类似,互斥量也是一种只允许一个线程获取的对象,但它是可以命名的,因而是可以跨越进程使用的。
互斥量使用CreateMutex函数创建:
HANDLE WINAPI CreateMutex(
__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全描述符
__in BOOL bInitialOwner, //为TRUE,表示创建它的线程直接获取了该互斥量;
// 为FALSE,表示互斥量创建后处于空闲状态
__in_opt LPCTSTR lpName //指定互斥对象的名字,可以使用OpenMutex来打开
);
释放互斥量使用函数ReleaseMutex,但是首先调用该函数的线程必须拥有该互斥量,这一点和使用Event不同,在任何线程中都可以使用SetEvent函数来将Event对象置位:
BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);
互斥量是靠WaitForSingleObject函数来获取的,当互斥量被某个线程获取(即等待成功)后,它的状态是复位的,否则是置位的。
当不再使用互斥量时,可以使用CloseHandle来关闭它。
使用互斥量进行同步的缺点和使用事件对象类似,也是在效率上远远低于使用临界区;优点也是类似的,就是可以跨进程使用,可以检测到其他线程是否挂掉。
实例代码如下:
#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 2
HANDLE ghMutex;
DWORD WINAPI WriteToDatabase( LPVOID );
void main()
{
HANDLE aThread[THREADCOUNT];
DWORD ThreadID;
int i;
// Create a mutex with no initial owner
ghMutex = CreateMutex(
NULL, // default security attributes
FALSE, // initially not owned
NULL); // unnamed mutex
if (ghMutex == NULL)
{
printf("CreateMutex error: %d/n", GetLastError());
return;
}
// Create worker threads
for( i=0; i < THREADCOUNT; i++ )
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE) WriteToDatabase,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
if( aThread[i] == NULL )
{
printf("CreateThread error: %d/n", GetLastError());
return;
}
}
// Wait for all threads to terminate
WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
// Close thread and mutex handles
for( i=0; i < THREADCOUNT; i++ )
CloseHandle(aThread[i]);
CloseHandle(ghMutex);
}
DWORD WINAPI WriteToDatabase( LPVOID lpParam )
{
DWORD dwCount=0, dwWaitResult;
// Request ownership of mutex.
while( dwCount < 20 )
{
dwWaitResult = WaitForSingleObject(
ghMutex, // handle to mutex
INFINITE); // no time-out interval
switch (dwWaitResult)
{
// The thread got ownership of the mutex
case WAIT_OBJECT_0:
__try {
// TODO: Write to the database
printf("Thread %d writing to database.../n",
GetCurrentThreadId());
dwCount++;
}
__finally {
// Release ownership of the mutex object
if (! ReleaseMutex(ghMutex))
{
// Handle error.
}
}
break;
// The thread got ownership of an abandoned mutex
// The database is in an indeterminate state
case WAIT_ABANDONED:
return FALSE;
}
}
return TRUE;
}
4)使用信号灯对象进行线程间同步:
信号灯对象是一个允许指定数量的线程获取的对象。信号灯对象一般是用于线程排队,考虑这样一种情况:服务器程序设置了3个工作线程以及n个和客户端连接的服务线程,服务线程需要在工作线程空闲时与之通讯,但3个工作线程都很忙的时候,就必须等待。这种情况下设置一个计数值为3的信号灯对象,服务线程和工作线程通讯前会首先尝试获取信号灯,这样就可以保证同时只有3个服务线程可以通过,而其他服务线程处于等待状态。
当信号灯对象的计数值为1时,就相当于一个互斥对象。
创建信号灯对象可以使用函数CreateSemaphore:
HANDLE WINAPI CreateSemaphore(
__in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //安全描述符
__in LONG lInitialCount, //初始时计数值
__in LONG lMaximumCount, //对象的最大计数值,即最多允许多少个线程获取该对象
__in_opt LPCTSTR lpName //指定对象的名字
);
释放信号灯对象使用函数ReleaseSemaphore:
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore, //
__in LONG lReleaseCount, //指定释放时将计数值增加多少
__out_opt LPLONG lpPreviousCount //函数在此返回释放前的计数值,不需要的话可设为NULL
);
当不再需要信号灯对象时,可以调用CloseHandle函数将它关闭。
信号灯对象是靠WaitForSingleObject函数来获取的,当对象被某个线程获取后,它的计数值减1,当计数值未减到0时,对象的状态是置位的,这意味着还有线程可以继续获取该对象;当计数值减到0时,对象的状态被复位。
实例代码如下:
#include <windows.h>
#include <stdio.h>
#define MAX_SEM_COUNT 10
#define THREADCOUNT 12
HANDLE ghSemaphore;
DWORD WINAPI ThreadProc( LPVOID );
void main()
{
HANDLE aThread[THREADCOUNT];
DWORD ThreadID;
int i;
// Create a semaphore with initial and max counts of MAX_SEM_COUNT
ghSemaphore = CreateSemaphore(
NULL, // default security attributes
MAX_SEM_COUNT, // initial count
MAX_SEM_COUNT, // maximum count
NULL); // unnamed semaphore
if (ghSemaphore == NULL)
{
printf("CreateSemaphore error: %d/n", GetLastError());
return;
}
// Create worker threads
for( i=0; i < THREADCOUNT; i++ )
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE) ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
if( aThread[i] == NULL )
{
printf("CreateThread error: %d/n", GetLastError());
return;
}
}
// Wait for all threads to terminate
WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
// Close thread and semaphore handles
for( i=0; i < THREADCOUNT; i++ )
CloseHandle(aThread[i]);
CloseHandle(ghSemaphore);
}
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
DWORD dwWaitResult;
BOOL bContinue=TRUE;
while(bContinue)
{
// Try to enter the semaphore gate.
dwWaitResult = WaitForSingleObject(
ghSemaphore, // handle to semaphore
0L); // zero-second time-out interval
switch (dwWaitResult)
{
// The semaphore object was signaled.
case WAIT_OBJECT_0:
// TODO: Perform task
printf("Thread %d: wait succeeded/n", GetCurrentThreadId());
bContinue=FALSE;
// Simulate thread spending time on task
Sleep(5);
// Release the semaphore when task is finished
if (!ReleaseSemaphore(
ghSemaphore, // handle to semaphore
1, // increase count by one
NULL) ) // not interested in previous count
{
printf("ReleaseSemaphore error: %d/n", GetLastError());
}
break;
// The semaphore was nonsignaled, so a time-out occurred.
case WAIT_TIMEOUT:
printf("Thread %d: wait timed out/n", GetCurrentThreadId());
break;
}
}
return TRUE;
}