线程同步是指同一进程中的多个线程相互协调工作达到一致性。当我们编写程序时,有时会使用多个代码段同时读取或修改相同地址空间中的共享数据。此时,操作系统中可能会出现一个代码段在读取数据,而另一个代码段却在修改数据的情况。这样的情况会导致程序发生读写错误,这显然不是我期望看到的。为了避免出现类似的情况,我们需要用到线程同步技术。即当一个线程对资源正在进行读写时,其它的线程则需等待。
线程的同步常用的有三种方法,分别是:临界区对象,事件对象和互斥对象。下面我们就对每一种方式分别使用Windows API和MFC类来进行一下实践。
1. 临界区对象
临界区对象是指当前用户使用某个线程访问共享资源时,必须使该线程独享该资源,而不允许其它线程访问。只有等待占有资源的线程释放该资源,其它的线程才能开始访问。
使用API的方式操作临界区的主要函数如下:
VOID InitializeCriticalSection( //初始化临界区
LPCRITICAL SECTION lpCriticalSection
);
VOID EnterCriticalSection( //进入临界区
LPCRITICAL SECTION lpCriticalSection
);
VOID LeaveCriticalSection( //离开临界区
LPCRITICAL SECTION lpCriticalSection
);
VOID DeleteCriticalSection( //删除临界区
LPCRITICAL SECTION lpCriticalSection
);
以上4个API的参数都是指向CRITICAL_SECTION结构体的指针。我们可以如下使用它们:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
static int a = 0;
CRITICAL_SECTION Section; //定义临界区对象
int main(int argc, char* argv[])
{
InitializeCriticalSection(&Section);
HANDLE h1, h2;
//关于CreateThread API的用法可以查看MSDN
h1 = ::CreateThread(NULL,0,myfun1,NULL,0,NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL,0,myfun2,NULL,0,NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1); //关闭线程句柄
::CloseHandle(h2);
::Sleep(1000);
printf("如需退出程序,请输入'q'!\n");
while(1)
{
if(getchar() == 'q')
{
DeleteCriticalSection(&Section);
return 0;
}
}
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
EnterCriticalSection(&Section);
++a;
if(a < 10000)
{
printf("线程1正在计数:%d\n",a);
LeaveCriticalSection(&Section);
::Sleep(1000);
}
else
{
LeaveCriticalSection(&Section);
break;
}
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
EnterCriticalSection(&Section);
++a;
if(a < 10000)
{
printf("线程2正在计数:%d\n",a);
LeaveCriticalSection(&Section);
::Sleep(1000);
}
else
{
LeaveCriticalSection(&Section);
break;
}
}
return 0;
}
运行情况如下:
MFC中使用CCriticalSection类实现临界区。用户在在进入临界区时调用CCriticalSection对象的Lock成员函数,离开时调用Unlock成员函数。于是,上面的临界区使用代码可以改为下面这种形式:
#include <afxmt.h>
#include <stdio.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
static int a = 0;
CCriticalSection m_Sec; //定义临界区对象
int main(int argc, char* argv[])
{
HANDLE h1, h2;
//关于CreateThread API的用法可以查看MSDN
h1 = ::CreateThread(NULL,0,myfun1,NULL,0,NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL,0,myfun2,NULL,0,NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1); //关闭线程句柄
::CloseHandle(h2);
::Sleep(10000);
return 0;
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
m_Sec.Lock();
++a;
printf("线程1正在计数:%d\n",a);
m_Sec.Unlock();
::Sleep(1000);
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
m_Sec.Lock();
++a;
printf("线程2正在计数:%d\n",a);
m_Sec.Unlock();
::Sleep(1000);
}
return 0;
}
有一点需要注意:控制台程序直接运行时需要进行工程的相关设置。打开project->settings->general->microsoft foundation classes->选use MFC in a static library。
2. 事件对象
事件对象是指用户在程序中使用内核对象的有无信号状态实现线程的同步。
在使用事件对象之前,我们需要创建事件对象,可以使用CreateEvent创建并返回一个事件对象。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
lpEventAttributes: 结构体LPSECURITY_ATTRIBUTES的指针,表示新创建的事件对象的安全属性。NULL表示默认安全属性。
bManualReset:标识事件对象是否人工重置。TRUE,则事件对象为人工重置(当调用线程获得所有权后,需要显示调用ResetEvent函数将事件对象设置为无信号);否则为自动重置。
bInitialState:表示时间对象的初始状态。TRUE表示有信号;否则,初始化为无信号状态。
lpName:事件对象的名称。如果为NULL,表示程序创建的是一个匿名的事件对象。
BOOL SetEvent( //将指定的事件对象设置为有信号状态
HANDLE hEvent
);
设置事件对象成功返回true,否则返回false。
BOOL ResetEvent( //将指定事件对象设置为无信号状态
HANDLE hEvent
);
设置事件对象成功返回true,否则返回false。
线程可通过调用WaitForSingleObject函数来主动请求事件对象。
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
hHandle:表示函数等待事件对象的句柄
dwMilliseconds:指定函数等待该事件对象的时间,如果指定为INFINITE,则该函数将永远等待。
该函数的返回值有以下几种情况:
WAIT_TIMEOUT:用户指定的等待时间已过。
WAIT_OBJECT_0:线程请求的对象为有信号状态。
WAIT_ABANDONED:当hHandle为mutex时,如果拥有mutex的线程在结束时没有释放核心对象会引发此返回值。
WAIT_FAILED:出现错误,可通过GetLastError得到错误代码。
API调用的例子如下:
#include <stdio.h>
#include <windows.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
HANDLE hEvent; //定义全局变量hEvent
int a = 0;
int main(int argc, char* argv[])
{
HANDLE h1, h2; //定义线程句柄
//创建事件对象,并把初始状态设为无信号
hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
::SetEvent(hEvent); //设置事件变量为有信号
h1 = ::CreateThread(NULL, 0, myfun1, NULL, 0, NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL, 0, myfun2, NULL, 0, NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1);
::CloseHandle(h2);
::Sleep(10000);
return 0;
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
::WaitForSingleObject(hEvent, INFINITE); //请求事件对象
::ResetEvent(hEvent); //设置事件对象为无信号,事件对象为自动重置,其实这句也可以不要
++a;
printf("线程1正在计数:%d\n",a);
::SetEvent(hEvent); //设置事件对象为有信号
::Sleep(1000);
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
::WaitForSingleObject(hEvent, INFINITE); //请求事件对象
::ResetEvent(hEvent); //设置事件对象为无信号,事件对象为自动重置,其实这句也可以不要
++a;
printf("线程2正在计数:%d\n",a);
::SetEvent(hEvent); //设置事件对象为有信号
::Sleep(1000);
}
return 0;
}
效果图如下:
在MFC中则是使用CEvent类来实现事件对象的。
首先,其构造函数的原型如下:
CEvent(
BOOL bInitiallyOwn = FALSE,//用来指定事件对象初始状态是否为发信状态(默认值为未发信)
BOOL bManualReset = FALSE,//用来指定创建的事件对象是自动事件还是手动事件对象(默认值为自动事件对象)
LPCTSTR lpszNAme = NULL,//用来定义事件对象的名称
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL //指向一个LPSECURITY_ATTRIBUTES结构的指针
)
需要将事件对象设置为有信号或者无信号可以使用SetEvent和ResetEvent两个函数。看MFC类的使用代码如下:
#include <afxmt.h>
#include <windows.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
static int a = 0;
CEvent event(false, false, NULL, NULL); //定义事件对象
int main(int argc, char* argv[])
{
event.SetEvent(); //将事件对象设置为有信号
HANDLE h1, h2;
//关于CreateThread API的用法可以查看MSDN
h1 = ::CreateThread(NULL,0,myfun1,NULL,0,NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL,0,myfun2,NULL,0,NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1); //关闭线程句柄
::CloseHandle(h2);
::Sleep(10000);
return 0;
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
//请求事件对象
::WaitForSingleObject(event.m_hObject, INFINITE);
event.ResetEvent(); //设置事件对象为无信号
++a;
printf("线程1正在计数:%d\n",a);
event.SetEvent(); //设置事件对象为有信号
::Sleep(1000);
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
//请求事件对象
::WaitForSingleObject(event.m_hObject, INFINITE);
event.ResetEvent(); //设置事件对象为无信号
++a;
printf("线程2正在计数:%d\n",a);
event.SetEvent(); //设置事件对象为有信号
::Sleep(1000);
}
return 0;
}
3. 互斥对象
互斥对象不仅可以用于线程间的同步,还可以用于进程间的同步。在互斥对象中,包含一个线程ID和一个计数器。线程ID表示当前拥有该互斥对象的线程,计数器用于表示该互斥对象被同一线程所使用的次数。在程序中依然可以分别用API或MFC类来操作。
调用API创建并返回互斥对象:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
lpMutexAttributes: 指定新创建互斥对象的安全属性。
bInitialOwner:表示该互斥对象的拥有者;若为true,表示创建该互斥对象的线程拥有其所有权;若为false,则创建者不拥有其所有权。
lpName: 表示互斥对象的名称,NULL则为匿名对象。如果用户为该参数指定值,则在程序中可以调用函数OpenMutex()打开一个命名的互斥对象。
用户在使用完互斥对象后,应该调用ReleaseMutex()函数释放该互斥对象的所有权,也就是让互斥对象处于有信号的状态。
BOOL ReleaseMutex(
HANDLE hMutex //将要释放的互斥对象句柄
);
如果函数调用成功返回true,否则返回false。
在使用互斥对象时,线程也可以调用WaitForSingleObject()对该对象进行请求。当互斥对象无信号时,该函数将一直等待,直到该互斥对象有信号或用户指定的等待时间已过;否则函数将返回。
以API的方式使用互斥对象代码如下:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
HANDLE hMutex; //定义全局变量hEvent
int a = 0;
int main(int argc, char* argv[])
{
HANDLE h1, h2; //定义线程句柄
//创建互斥对象
hMutex = ::CreateMutex(NULL, FALSE, NULL);
h1 = ::CreateThread(NULL, 0, myfun1, NULL, 0, NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL, 0, myfun2, NULL, 0, NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1);
::CloseHandle(h2);
::Sleep(10000);
return 0;
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
::WaitForSingleObject(hMutex, INFINITE); //请求事件对象
++a;
printf("线程1正在计数:%d\n",a);
::ReleaseMutex(hMutex); //释放互斥对象句柄
::Sleep(1000);
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
::WaitForSingleObject(hMutex, INFINITE); //请求事件对象
++a;
printf("线程2正在计数:%d\n",a);
::ReleaseMutex(hMutex); //释放互斥对象句柄
::Sleep(1000);
}
return 0;
}
同样能达到前面的效果:
CMutex(
BOOL bInitiallyOwn = FALSE, //用来指定互斥体对象初始状态是锁定(TRUE,创建者拥有所有权)还是非锁定(FALSE,创建者不拥有所有权)
LPCTSTR lpszName = NULL, //用来指定互斥体的名称
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL //为一个指向SECURITY_ATTRIBUTES结构的指针
)
可以在线程函数中调用Lock()和Unlock()对该互斥对象所保护的区域进行锁定和解锁,控制其他线程对保护区域的访问权限。
MFC类的使用代码如下:
#include <afxmt.h>
#include <stdio.h>
DWORD WINAPI myfun1(LPVOID lpParameter); //声明线程函数
DWORD WINAPI myfun2(LPVOID lpParameter);
int a = 0;
CMutex mutex(NULL, false, NULL); //定义互斥对象
int main(int argc, char* argv[])
{
HANDLE h1, h2;
//关于CreateThread API的用法可以查看MSDN
h1 = ::CreateThread(NULL,0,myfun1,NULL,0,NULL);
printf("线程1开始运行!\n");
h2 = ::CreateThread(NULL,0,myfun2,NULL,0,NULL);
printf("线程2开始运行!\n");
::CloseHandle(h1); //关闭线程句柄
::CloseHandle(h2);
::Sleep(10000);
return 0;
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
while(1)
{
mutex.Lock(); //对互斥对象加锁
++a;
printf("线程1正在计数:%d\n",a);
mutex.Unlock(); //对互斥对象解锁
::Sleep(1000);
}
return 0;
}
DWORD WINAPI myfun2(LPVOID lpParameter)
{
while(1)
{
mutex.Lock(); //对互斥对象加锁
++a;
printf("线程2正在计数:%d\n",a);
mutex.Unlock(); //对互斥对象解锁
::Sleep(1000);
}
return 0;
}
以上实现的是同一进程中线程的同步。前面也介绍了,互斥对象还可以用于进程间的互斥。利用此特性,我们可以创建一个只允许唯一程序实例运行的代码。如下:
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
HANDLE hMutex;
//创建互斥对象并返回句柄
hMutex = ::CreateMutex(NULL, true, "唯一实例");
if(hMutex)
{
if(ERROR_ALREADY_EXISTS == GetLastError())
{
::MessageBox(NULL, "只允许一个程序实例运行!", NULL, MB_OK);
exit(0);
}
else
{
::MessageBox(NULL, "程序运行成功!", "提示", MB_OK);
}
}
::ReleaseMutex(hMutex);
::Sleep(100000);
return 0;
}
CreateMutex返回值说明:如执行成功,就返回互斥体对象的句柄;零表示出错,会设置GetLastError。即使返回的是一个有效句柄,但倘若指定的名字已经存在,GetLastError也会设为ERROR_ALREADY_EXISTS。
运行效果如下: