多线程 AfxBeginThread 与 CreateThread 的区别
简言之:
AfxBeginThread是MFC的全局函数,是对CreateThread的封装。
CreateThread是Win32 API函数,前者最终要调到后者。
1.
具体说来,CreateThread这个 函数是windows提供给用户的 API函数,是SDK的标准形式,在使用的过程中要考虑到进程的同步与互斥的关系,进程间的同步互斥等一系列会导致操作系统死锁的因素,用起来比较繁琐一些,初学的人在用到的时候可能会产生不可预料的错误,建议多使用AfxBeginThread,是编译器对原来的CreateThread函数的封装,用于MFC编程(当然,只要修改了项目属性,console和win32项目都能调用)而_beginthread是C的运行库函数。
2.
在使用AfxBeginThread时,
线程函数的定义为:UINT _yourThreadFun(LPVOID pParam)参数必须如此
在使用CreateThread时,
线程的函数定义为: DWORD WINAPI _yourThreadFun(LPVOID pParameter)
两者实质是一样的,
不过AfxBeginThread返回CWinThread指针,就是说它会new一个CWinThread对象,而这个对象在线程运行结束时是会自动删除的,CreatThread,它返回的是一个句柄,如果你不使用CloseHandle的话就可以通过它安全的了解线程状态,最后不要的时候CloseHandle,Windows才会释放资源。
另外有两点需要注意:
1、AfxBeginThread函数参数1的回调函数要么是全局函数要么是类内的静态函数;
2、可以将类指针作为参数传入调用函数,通过此指针就可以访问类中的成员变量和成员函数,否则被访问的成员变量和成员函数也必须是静态的。
例如:
static UINT Thread1(void *param);
或者单独在其他函数外声明为全局函数。
UINT Thread1(void *param);
MFC多线程例子:https://jingyan.baidu.com/article/597a064363ba76312b5243a1.html
MFC多线程例子工程:http://download.csdn.net/download/l198738655/10128669
*******************************************
AfxBeginThread函数
功能是创建用户界面线程和工作者线程。
MFC提供了两个重载版的AfxBeginThread,一个为用户界面线程,另一个为工作者线程,二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。二者的函数原型分别如下:
用户界面线程的AfxBeginThread
用户界面线程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
其中:
参数1是从CWinThread派生的RUNTIME_CLASS类;
参数2指定线程优先级,如果为0,则与创建该线程的线程相同;
参数3指定线程的堆栈大小,如果为0,则与创建该线程的线程相同;
参数4是一个创建标识,如果是CREATE_SUSPENDED,则在悬挂状态创建线程,在线程创建后线程挂起,否则线程在创建后开始线程的执行。
参数5表示线程的安全属性,NT下有用。
工作者线程的AfxBeginThread
工作者线程的AfxBeginThread的原型如下:
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,
LPVOID lParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);//用于创建工作者线程
返回值: 成功时返回一个指向新线程的线程对象的指针,否则NULL。
pfnThreadProc : 线程的入口函数,声明一定要如下: UINT MyThreadFunction(LPVOID pParam),不能设置为NULL; 例:UINT CManyDlg::Thread1(void *param)
pParam : 传递入线程的参数,注意它的类型为:LPVOID,所以我们可以传递一个结构体入线程;LPVOID类型其实就是一个指针,参数2的作用就是允许用户把一个线程外部的变量传入线程内,可以在线程回调函数内取出这个作为指针传入的变量,让此线程可以和主线程或者其他线程通信。传入的这个变量最好是全局变量,或者是用new 关键字申请的动态内存变量,如果传入一个局部指针变量会出错。
nPriority : 线程的优先级,一般设置为 0 .让它和主线程具有共同的优先级.
nStackSize : 指定新创建的线程的栈的大小.如果为 0,新创建的线程具有和主线程一样的大小的栈
dwCreateFlags : 指定创建线程以后,线程有怎么样的标志.可以指定两个值:
CREATE_SUSPENDED : 线程创建以后,会处于挂起状态,直到调用:ResumeThread 设置为0,创建线程后就开始运行。
lpSecurityAttrs : 指向一个 SECURITY_ATTRIBUTES 的结构体,用它来标志新创建线程的安全性.如果为 NULL,
那么新创建的线程就具有和主线程一样的安全性.如果要在线程内结束线程,可以在线程内调用 AfxEndThread.
*********************************************
AfxBeginThread 创建,挂起,唤醒,终止线程
MFC使用AfxBeginThread()函数创建线程
CWinThread *AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
)
CWinThread *AfxBeginThread(
CRuntimeClass *pThreadClass,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
)
pfnThreadProc:表示线程函数指针,函数原型是 UINT ControllingFunction(LPVOID pParameter);
pParam:线程函数的参数 nPriority线程的优先级 nStackSize堆栈大小 dwCreateFlags线程创建标记 lpSecurityAttrs安全属性 pThreadClass派生于CWinThread类的运行时类对象
1、创建挂起线程
CWinThread* pThread;
pThreads = AfxBeginThread(AcceptData,&m_sendUdp, 0, 0, CREATE_SUSPENDED, NULL);//线程挂起的标志CREATE_SUSPENDED
2、挂起一个运行的线程
DWORD SuspendThread(HANDLE hThread);
hThread: 表示线程句柄
返回值: 如果函数执行成功,返回值为之前挂起的线程次数;如果函数执行失败,返回值为0xFFFFFFFF
SuspendThread(pThreads->m_hThread);
3、唤醒挂起的线程
ResumeThread
该函数用于检测线程挂起的次数,如果线程挂起的次数为0,将唤醒线程.语法格式如下:
DWORD ResumeThread(HANDLE hThread);
hThread: 表示线程句柄
返回值: 如果函数执行成功,返回值为之前挂起的线程次数;如果函数执行失败,返回值为0xFFFFFFFF
ResumeThread(pThreads->m_hThread)
ExitThread
该函数用于结束当前线程.语法格式如下:
VOID ExitThread(DWORD dwExitCode);
dwExitCode: 表示线程退出代码
TerminateThread
该函数用于强制终止线程的执行.语法格式如下:
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
hThread: 表示待终止的线程句柄
dwExitCode: 表示线程退出代码
******************************************
AfxBeginThread函数创建的线程如何结束
AfxBeginThread函数参数1调用的回调函数只要正常return;后就会自动调用AfxEndThread()函数结束掉线程,自动回收资源。
例如:
UINT CManyDlg::Thread1(void *param) //线程1
{
CManyDlg *dlg = (CManyDlg*)param;
CEdit *randBox = (CEdit*)dlg->GetDlgItem(IDC_Random);
CString str;
while (dlg->flag)
{
Sleep(100);
str.Format(_T("%i"), rand()%1000);
randBox->SetWindowTextW(str);
}
return 0;
}
******************************************
AfxBeginThread函数多参数传入线程,一般有两种方式:
方法一:定义全局结构体,在线程函数内直接调用
struct Sok //结构体
{
SOCKET s;
SOCKADDR_IN addrClient;
int len;
};
Sok sok_test ;//结构体声明
static UINT whileass()//全局结构体传入参数。
{
while(s_d=accept(sok_test.s,(sockaddr*)&sok_test.addrClient,&sok_test.len))
{
AfxBeginThread((AFX_THREADPROC)Open,(LPVOID)IDC_BUTTON1,0,0,0,0);
}
return 0;
}
AfxBeginThread((AFX_THREADPROC)whileass,(LPVOID)IDC_BUTTON1,0,0,0,0);//调用whileass函数
方法二:利用 AfxBeginThread自带方法传入参数,然后进行类型转换。
如:
DWORD WINAPI ThreadProc(LPVOID pParam)//利用mfc AfxBeginThread自带方法传入参数
{
Sok rect=*(Sok*)pParam;
while(s_d=accept(rect.s,(sockaddr*)&rect.addrClient,&rect.len))
{
AfxBeginThread((AFX_THREADPROC)Open,(LPVOID)IDC_BUTTON1,0,0,0,0);
}
return 0;
}
AfxBeginThread((AFX_THREADPROC)ThreadProc,&_sok,THREAD_PRIORITY_IDLE);//调用ThreadProc
******************************************
线程创建函数
创建线程可以用系统提供的API函数:CreateThread来完成,该函数将创建一个线程。
函数原型:
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
下面介绍具体参数:
lpThreadAttributes
指向SECURITY_ATTRIBUTES结构体的指针,这个结构体允许用户设置自定义的安全策略,如果设置为NULL则默认该线程使用默认的安全性。但是如果希望所有子进程能够继承线程对象的句柄,就必须设置一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE。
dwStackSize
设置线程初始栈的大小,即线程可以将多少地址空间用于它自己的栈,以字节为单位。系统会把这个参数值四舍五入
为最接近的页面大小。页面是系统管理内存时使用的内存单位,不同CPU其页面大小不同,x86使用的页面大小是4KB。当保留地址空间的一块区域时,系统要确保该区域的大小是系统页面大小的倍数。例如,希望保留10KB的地址空间区域,系统会自动对这个请求进行四舍五入,使保留的区域大小是页面大小的倍数,在x86平台下,系统将保留一块12KB的区域,即4KB的整数倍。如果这个值为0,或者小于默认提交的大小,那么默认将使用与调用该函数的线程相同的栈空间大小。
lpStartAddress
指向应用程序定义的THREAD_START_ROUTINE类型的函数指针,这个函数将由新线程执行,表明新线程的起始地址。我们知道main函数是主线程的入口函数,同样的,新创建的线程也需要一个入口函数,这个函数的地址就由此参数指定。这就要求在程序中定义一个函数作为新线程的入口函数,该函数的名称任意,但函数类型必须遵照以下形式声明:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
即新线程入口函数有一个LPVOID类型的参数,并且返回值是DWORD类型。许多初学者不知道这个函数名"ThreadProc"其实可以改变。实际上,在调用CreateThread创建新线程时,我们只需要传递线程函数的入口地址,而线程函数的名称是无所谓的。
lpParameter
对于main函数来说,可以接受命令行参数。同样,我们可以通过这个参数给创建的新线程传递参数。该参数提供了一
种将初始化值传递给线程函数的手段。这个参数的值既可以是一个数值,也可以是一个指向其他信息的指针。
dwCreationFlags
设置用于控制线程创建的附加标记。它可以是两个值中的一个:CREATE_SUSPENDED或0。如果该值是CREATE_SUSPENDED,那么线程创建后处于暂停状态,直到程序调用了ResumeThread函数为止;如果该值是0,那么线程在创建之后就立即运行。
lpThreadId
这个参数是一个返回值,它指向一个变量,用来接收线程ID。当创建一个线程时,系统会为该线程分配一个ID。
***************************************
CloseHandle()函数
关闭新线程的句柄,参数只有一个,要关闭的线程句柄变量。关闭线程句柄不等于结束线程;关闭线程句柄可以释放
一些资源。
***************************************
利用互斥对象实现线程同步
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权(一次只有一个)。互斥对象包含一
个使用数量,一个线程ID和一个计数器。其中ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线
程拥有互斥对象的次数。
为了创建互斥对象,需要调用函数:CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就
可以利用该互斥对象完成线程的同步。
CreateMutex函数原型与参数说明:
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
);
lpMutexAttributes
一个指向SECURITY_ATTRIBUTES结构的指针,可以给该参数传递NULL值,让互斥对象使用默认安全性。
bInitialOwner
BOOL类型,指定互斥对象初始值的拥有者。如果该值为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权。
lpName
指定互斥对象的名称。如果此参数为NULL,则创建一个匿名互斥对象。如果调用成功,该函数将返回所创建的互斥对象句柄。如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,那么该函数将返回已经存在的这个互斥对象的句柄,而这时调用GetLastError函数将返回ERROR_ALREADY_EXISTS。
***************************************
当线程对共享资源访问结束后,应释放该对象的所有权,也就是让该对象处于已通知状态。这时需要调用ReleaseMutex函数,该函数将释放指定对象的所有权。
ReleaseMutex()
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
ReleaseMutex函数只有一个HANDLE类型的参数,即需要释放的互斥对象句柄。返回值为布尔值,如果函数调用成功返
回非0值,失败则返回0。
另外,线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过调用WaitForSingleObject函数来实现,该函数的原型声明如下:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
该函数有2个参数,其定义如下:
hHandle
所请求的对象的句柄。本例将传递已创建的互斥对象的句柄:hMutex。一旦互斥对象处于有信号状态,则该函数就返回。如果该互斥对象始终处于无信号状态,即未通知的状态,则该函数就会一直等待,这样就会暂停线程的执行。
dwMilliseconds
指定等待的时间间隔,以毫秒为单位。如果指定的时间间隔已过去,即使所请求的对象仍处于无信号状态,WaitForSingleObject函数也会返回。如果将此参数设置为0,那么WaitForSingleObject函数将测试该对象的状态并立即返回;如果将此参数设置为INFINITE,则该函数会永远等待,直到等待的对象处于有信号状态才会返回。
调用WaitForSingleObject函数后,该函数会一直等待,只有在以下两种情况下才会返回:
1.指定的对象变成有信号状态
2.指定的等待时间已过
如果函数调用成功,那么WaitForSingleObject函数的返回值将表明引起该函数返回的事件,详情如下:
返回值 说明
WAIT_OBJECT_0 所请求的对象是有信号状态
WAIT_TIMEOUT 指定的时间间隔已过,并且所请求的对象是无信号状态
WAIT_ABANDONED 所请求的对象是一个互斥对象,并且先前拥有该对象的线程在终止前没有释放该对象。这时,该对象的所有权将授予当前调用线程,并且将该互斥对象(原来拥有互斥对象的线程)被设置为无信号状态
互斥对象线程同步例子:
#include
#include
int index=0;
int tickets=10;
HANDLE hMutex; //句柄接收变量
using namespace std;
DWORD WINAPI Fun1Proc(
LPVOID lpParameter // thread data
);
DWORD WINAPI Fun2Proc(
LPVOID lpParameter // thread data
);
void main()
{
HANDLE hThread1;
HANDLE hThread2; //存放线程句柄
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);//创建一个线程,返回一个线程句柄,线程函数入口是Fun1Proc
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);//创建一个线程,返回一个线程句柄,线程函数入口是Fun2Proc
CloseHandle(hThread1);
CloseHandle(hThread2); //如果主线程不在需要再操作线程,就释放掉线程句柄,节省资源,释放线程句柄不等于关闭线程
//cout<<"我是主线程\n"<0)
{
Sleep(1);
cout<<"我是线程1\n"<0)
{
Sleep(1);
cout<<"我是线程2\n"<
***************************************
多次在主线程中调用互斥对象问题
如果调用ReleaseMutex()在主线程中创建互斥体,且第二个参数为TRUE,表示互斥体的拥有者是创建互斥体的线程。如果此时马上调用WaitForSingleObject()函数申请互斥体所有权后,又调用ReleaseMutex()函数释放所有权后,其他线程依旧无法取得互斥体所有权。这是因为对同一个线程多次拥有互斥体对象来说,该互斥对象内部的计数器记录了该线程拥有的次数。在本例中,当第一次创建互斥对象时,主线程拥有了这个互斥对象,除了将互斥对象的线程ID设置为主线程的ID以外,同时还将互斥对象内部的计数器变为1。紧接着又在此调用WaitForSingleObject()函数申请互斥对象控制权,此时互斥对象内部计数器+1,变为2。接下来马上又调用ReleaseMutex()函数释放互斥对象,此时互斥对象内部计数器-1,但是计数器仍为1。主线程依旧拥有互斥对象控制权。其他线程申请控制权的话将会失败。此时只能再调用一次ReleaseMutex()函数,再次释放控制权,让互斥对象内计数器-1。此时计数器为0。其他线程就可以申请到互斥对象的控制权了。
另外需要说明的是,当主线程用ReleaseMutex(),创建互斥对象时,且第二个参数为TRUE,互斥对象就处于占用状态,此时再用WaitForSingleObject()函数申请控制权也能通过,因为系统会判断当前请求互斥对象的线程ID是否与互斥对象当前拥有者的线程ID相同,如果相同,即使互斥对象处于占用状态,调用线程仍然能获得其所有权。
***************************************
GetLastError()函数
这是一个很简单的函数,它的作用就是取得上一个函数操作时所产生的错误代码。
通过错误代码,就可以在winerror.h头文件中查找到每一中错误代码所表示的含义。也可以通过VC++自带的Error Lookup工具来查找其所表示的含义,其结果是一样的。
此函数的说明如下:
DWORD GetLastError(void);
这是一个没有参数的函数,通过调用,就返回一个32位的数值。
下面编写了一个很简单的VC++例子来说明此函数
#include
int WINAPI wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
HANDLE hfile=CreateFile("c://a.txt",0,0,NULL,OPEN_EXISTING,0,NULL);
DWORD k=GetLastError();
return 0;
}
互斥对象对于限制程序多开的方法《VC++深入详解》 P588
***************************************
创建事件对象
在程序中可以通过CreateEvent函数创建或打开一个命名的或匿名的事件对象,该函数原型声明如下:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
该函数有4个参数
lpEventAttributes
指向SECURITY_ATTRIBUTES结构体的指针。如果其值为NULL。则使用默认的安全性。
bManualReset
BOOL类型,指定创建的是人工重置事件对象或自动重置事件对象。如果此参数为TRUE,表示该函数将创建一个人工重置事件对象;如果此参数为FALSE,表示该函数将创建一个自动重置事件对象;如果是人工重置事件对象,当线程等待到该对象的所有权之后,需要调用ResetEvent函数手动将该事件对象设置为无信号状态;如果是自动重置事件对象,当线程等到该对象的所有权之后,系统会自动将该对象设置为无信号状态。
bInitialState
BOOL类型,指定事件对象的初始状态。如果此参数值为真,那么该事件对象初始是有信号状态;否则是无信号状态。
lpName
指定事件对象的名称。如果此参数为NULL,那么将创建一个匿名事件对象。
***************************************
设置事件对象状态
SetEvent函数将把指定的事件对象设置为有信号状态,该函数的原型声明如下所示:
BOOL SetEvent(HANDLE hEvent);
SetEvent函数有一个HANDLE类型的参数,该参数指定将要设置其状态的事件对象的句柄。
***************************************
重置事件对象状态
ResetEvent函数将把指定的事件对象设置为无信号状态,该函数的原型声明如下所示:
BOOL ResetEvent(HANDLE hEvent);
ResetEvent函数有一个HANDLE类型的参数,该参数指定将要重置其状态的事件对象的句柄。如果调用成功,该函数返回非0值;否则返回0值。
***************************************
在使用事件对象实现线程间同步时,一定要注意区分人工重置事件对象和自动重置事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程,同时操作系统会将该事件对象设置为无信号状态,这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事件对象后,操作系统并不会将该事件对象设置为无信号状态,除非显式的调用ResetEvent函数将其设置为无信号状态,否则该对象会一直处于有信号状态。
***************************************
关键代码段
关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码段当做关键代码段。
首先,需要初始化关键代码段,可以调用InitializeCriticalSection函数实现
以下是函数原型:
void WINAPI InitializeCriticalSection( //创建一个临界区
_Out_ LPCRITICAL_SECTION lpCriticalSection
);
该函数只有一个参数,是一个指向CriticalSection结构体的指针。该参数是OUT类型,意即作为返回值使用。因此在使用时,需要构造一个CriticalSection结构体对象,然后将该对象的地址传递给InitializeCriticalSection函数,系统自动维护该对象,我们不需要了解或访问该结构体对象的内部成员。
如果想要进入关键代码段,首先需要调用EnterCriticalSection函数,以获得指定的临界区对象的所有权。该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。
函数原型
void WINAPI EnterCriticalSection( //判断临界区状态,是否有信号
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection [in,out]
指向关键部分对象的指针。
当调用线程获得了指定的临界区对象的所有权后,该线程就进入关键代码段,对所保护的资源进行访问。就好像公用电话亭没人时,我们可以进去访问电话这一资源。当使用完电话之后,我们会离开电话亭。同样,线程使用完所保护的资源后需要调用LeaveCriticalSection函数,释放指定临界区对象的所有权,之后其他想要获取所有权的线程就可以获得该所有权。
函数原型:
void WINAPI LeaveCriticalSection( //释放对临界区对象的所有权
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection [in,out]
指向关键部分对象的指针。
当不再需要临界区对象时需要调用DeleteCriticalSection函数释放该对象。
void WINAPI DeleteCriticalSection( //删除临界区对象
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection [in,out]
指向关键部分对象的指针。必须使用InitializeCriticalSection函数先前初始化对象 。
***************************************
互斥对象、事件对象与关键代码段的对比
互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
关键代码段工作在用户模式下,同步速度较快,但在使用关键代码时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
通常,在编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果是在MFC程序中使用的话,可以在类的构造函数中调用InitializeCriticalSection()函数创建关键代码段,在该类的析构函数中调用
DeleteCriticalSection函数,在所需保护的代码段前面调用EnterCriticalSection函数,在访问完所需保护的资源之后,调用LeaveCriticalSection函数释放临界区所有权。
可见,关键代码段在使用上是非常方便的,但是又几点需要注意:一是在程序中调用了EnterCriticalSection函数之后,一定要相应的调用LeaveCriticalSection函数释放所有权,否则其他等待该临界区对象所有权的线程将无法执行。二是如果访问关键代码段时,使用了多个临界区对象,就要注意防止线程死锁问题发生。另外,如果需要在多个进程间的各个线程间实现同步的话,可以使用互斥对象和事件对象。
***************************************
进程间数据通讯
用剪贴板实现进程间通信
在把数据放置到剪贴板之前,首先需要打开剪贴板,这就可以利用CWnd类的OpenClipBoard成员函数实现,该函数原型如下:
BOOL OpenClipboard() //打开剪贴板
BOOL WINAPI CloseClipboard(void); //关闭剪贴板
BOOL WINAPI EmptyClipboard(void); //清空剪贴板并释放剪贴板中数据的句柄。该功能然后将剪贴板的所有权分 //配给当前已打开剪贴板的窗口。
OpenClipboard函数的返回值是BOOL类型。如果打开剪贴板操作成功,则该函数返回非0值;如果其他程序,或者当前
窗口已经打开了剪贴板,则该函数返回0值。如果某个程序已经打开了剪贴板,则其他应用程序将不能修改剪贴板,直到前者调用了CloseCilpboard函数。并且只有调用了EmptyClipboard函数之后,打开剪贴板的当前窗口才拥有剪贴板。EmptyClipboard函数将清空剪贴板,并释放剪贴板中数据的句柄,然后将剪贴板的所有权分配给当前打开剪贴板的窗口。因为剪贴板是所有进程都可以访问的,所以在我们编写的这个Cilpboard进程使用剪贴板之前,可能已经有其他进程把数据放置到了剪贴板上,那么该进程打开剪贴板之后,需要调用EmptyClipboard函数,清空剪贴板,释放剪贴板上数据的句柄,并将剪贴板的所有权分配给当前打开剪贴板的窗口,之后就可以向剪贴板中放置数据了。向剪贴板中放置数据,可以通过SetClipboardData函数实现。这个函数是以指定的剪贴板格式向剪贴板上放置数据,该函数原型如下:
HANDLE SetClipboardData(UINT uFormat,HANDLE hMem) //以指定的剪贴板格式将数据放在剪贴板上。
需要注意的是,当前调用SetClipboardData()函数的窗口必须是剪贴板的拥有者,而且在这之前,该程序必须已经调用了OpenClipboard函数打开了剪贴板。在随后响应WM_REDERFORMAT和WM_RENDERALLFORMATS消息时,当前剪贴板的拥有者在调用SetClipboardData()函数之前就不必再调用OpenClipboard()函数了。SetClipboardData函数有两个参数,其含义分别如下。
HANDLE WINAPI SetClipboardData( //以指定的剪贴板格式将数据放在剪贴板上。
_In_ UINT uFormat,
_In_opt_ HANDLE hMem
);
uFormat
指定剪贴板格式,这个格式可以是已注册的格式,或者是任一标准的剪贴板格式,具体信息科查询MSDN。
hMem
具有指定格式的数据的句柄。该参数可以是NULL,指示调用窗口直到有对剪贴板数据的请求时,才提供指定剪贴板格式的数据。如果窗口才用延迟提交技术,则该窗口必须处理WM_RENDERFORMAT和WM_RENDERALLFORMATS消息。
GlobalAlloc函数
HGLOBAL WINAPI GlobalAlloc( //从堆中分配指定的字节数。
_In_ UINT uFlags,
_In_ SIZE_T dwBytes
);
uFlags
是一个标记,用来指定分配内存的方式
dwBytes
指定分配的字节数
GlobalLock函数
LPVOID WINAPI GlobalLock(
_In_ HGLOBAL hMem
);
对全局内存对象加锁,然后返回该对象内存第一个字节的指针。
GlobalUnlock函数
BOOL WINAPI GlobalUnlock( //减少与使用GMEM_MOVEABLE分配的内存对象关联的锁定计数。
_In_ HGLOBAL hMem
);
每个内存对象的内部数据结构都包含了一个初始值为0的锁计数,对于可移动的内存对象来说,GlobalLock函数将其锁计数+1,而GlobalUnlock函数将该锁计数-1。对一个进程来说,每次调用GlobalLock函数后,都一定要记得调用
GlobalUnlock函数。被锁定的内存对象将保持锁定,直到它的锁计数为0,这时,该内存块才能被移动,或者被抛弃。另外,已被加锁的内存不能被移动,或者被废弃,除非调用了GlobalRealloc函数重新分配了该内存对象。
GlobalReAlloc函数
HGLOBAL WINAPI GlobalReAlloc( //更改指定的全局内存对象的大小或属性。尺寸可以增减。
_In_ HGLOBAL hMem,
_In_ SIZE_T dwBytes,
_In_ UINT uFlags
);
参数
hMem [in]
要重新分配的全局内存对象的句柄。GlobalAlloc或 GlobalReAlloc函数返回此句柄 。
dwBytes [in]
内存块的新大小,以字节为单位。如果uFlags指定了GMEM_MODIFY,则此参数将被忽略。
uFlags [in]
重新分配选项。如果指定了GMEM_MODIFY,则该函数仅修改内存对象的属性(dwBytes参数被忽略)。否则,该函数将重新分配内存对象。
***************************************
匿名管道
匿名管道是一个未命名的、单向管道,通常用来在一个父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上2个进程间的通信,而不能实现跨网络的通信。
为了创建匿名管道,需要调用CreatePipe函数,该函数的原型声明如下所示:
BOOL WINAPI CreatePipe(
_Out_ PHANDLE hReadPipe,
_Out_ PHANDLE hWritePipe,
_In_opt_ LPSECURITY_ATTRIBUTES lpPipeAttributes,
_In_ DWORD nSize
);
此函数将创建一个匿名管道,返回该匿名管道的读写句柄。该函数有四个参数,其含义分别如下所述。
hReadPipe与hWritePipe
这2个参数都是OUT类型,即作为返回值来使用。前者返回管道的读取句柄,后者接收管道的写入句柄。也就是说,在程序中需要定义2个句柄变量,将它们的地址分别传递给这2个参数,然后CreatePipe函数将通过这两个参数返回创建的匿名管道读写句柄。
lpPipeAttributes
一个指向SECURITY_ATTRIBUTES结构体的指针,检测返回的句柄是否被子进程继承。如果此参数为NULL,则句柄不能被继承。在前面的章节中,凡是需要SECURITY_ATTRIBUTES结构体指针的地方,我们传递的都是NULL,让系统为创建对象赋予默认的安全描述符,而函数所返回的句柄将不能被子进程所继承。但在本章匿名管道的例子中,不能再为此参数传递NULL值了,因为匿名管道只能在父子进程之间进行通信。子进程如果想要获得匿名管道的句柄,只能从父进程继承而来。当一个子进程从其父进程继承了匿名管道的句柄后,这2个进程就可以通过该句柄进行通信了。所以在本章匿名管道的例子中,必须构造一个SECURITY_ATTRIBUTES结构体变量,该结构体定义如下:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
SECURITY_ATTRIBUTES结构体有三个成员,第一个成员nLength指定该结构体的大小;第二个成员lpSecurityDescriptor是一个指向安全描述符的指针,在本章匿名管道的例子中,可以给这个成员传递NULL值,让系统为创建的匿名管道赋予默认的安全描述符;第三个成员bInheritHandle很关键,该成员指定所返回的句柄是否能被一个新的进程所继承,如果此成员为TRUE,那么返回的句柄能够被新进程继承。在本章匿名管道的例子中,需要将此成员设置为TRUE,让子进程可以继承父进程创建的匿名管道的读写句柄。
nSize
指定管道的缓冲区大小,该大小仅仅是一个建议值,系统将使用这个值来计算一个适当的缓冲区大小。如果此参数为0,系统则使用默认的缓冲区大小。
进程的创建
为了启动一个进程,可以调用CreateProcess函数,该函数原型声明如下:
BOOL WINAPI CreateProcess(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName
一个指向NULL终止的字符串,用来指定可执行程序的名称。该名称可以是该程序的完整路径和文件名,也可以是部分名称。如果是后者,CreateProcess函数就在当前路径下搜索可执行文件名,但不会使用搜索路劲进行搜索。注意:一定要加上扩展名,系统将会自动假设文件名有一个".exe"扩展名。此参数可以为NULL,这时,文件名必须是lpCommandLine指向的字符串中的第一个空格界定的标记。如果使用了包含空格的长文件名,那么应该使用引号将该名称包含起来,以表明文件名的结束和参数的开始。否则文件名会产生歧义。例如"c:\program files\sub dir\program name"这个字符串会被解释为多种形式,系统将按如下顺序来进行处理:
c:\program.exe files\sub dir\program name
c:\program files\sub.exe dir\program name
c:\program files\sub dir\program.exe name
c:\program files\sub dir\program name.exe
lpCommandLine
一个指向NULL终止符的字符串,用来指定传递给新进程的命令行字符串。系统会在该字符串的最后增加一个NULL字符,并且如有必要,它会去掉首位空格。我们可以在lpApplicationName参数中传递可执行文件的名称,在lpCommandLine参数中传递命令行的参数。但应注意,如果在lpCommandLine参数中传递了一个可执行的文件名,并且没有包含路径,那么这时CreateProcess函数将按照预设值在系统中查找这个文件。
P640
lpProcessAttributes和lpThreadAttributes
这两个参数都是指向SECURITY_ATTRIBUTES结构体的指针。当调用CreateProcess函数创建新进程时,系统将为新进程创建一个进程内核对象和一个线程内核对象,后者用于进程的主线程。而lpProcessAttributes和lpThreadAttributes这两个参数就是分别用来设置新进程对象和线程对象的安全性,以及指定父进程将来创建的其他子进程是否可以继承这两个对象的句柄,在我们的程序中不需要创建其他的子进程,可以为这两个参数传递NULL,让系统为这两个对象赋予默认的安全描述符。
bInheritHandles
该参数用来指定进程随后创建的子进程是否能够继承父进程的对象句柄。如果该参数为TRUE,那么父进程的每个可继承的打开句柄都能被子进程继承。继承的句柄与原始句柄拥有同样的值和访问特权。
dwCreationFlags
指定空间优先级类和进程创建的附加标记。如果只是为了启动子进程,并不需要设置它的创建的标记,可以直接将此参数设置为0。该参数也可用于控制新进程的优先级别。
P642
lpEnvironment
一个指向环境块的指针,如果此参数为NULL,那么新进程使用调用进程的环境。通常都是给此参数传递NULL。
lpCurrentDirectory
一个指向空终止的字符串,用来指定子进程当前的路径,这个字符串必须是一个完整的路径名,包括驱动器的标识符,如果此参数为NULL,那么新的子进程将与调用进程,即父进程拥有相同的驱动器和目录。
lpStartupInfo
一个指向STARTUPINFO结构体的指针,用来指定新进程的主窗口将如何显示。
typedef struct _STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
P643
lpProcessInformation
这个参数作为返回值使用,是一个指向ProcessInformation结构的指针,用于接收关于新进程的标识信息。
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
PROCESS_INFORMATION结构体有4个成员.前两个成员hProcess和hThread分别是标识新创建的进程句柄和新创建进程的主线程句柄;后两个成员:dwProcessId和dwThreadId分别是全局进程标识符和全局线程标识符,前者可以用来标识一个进程,后者可以用来标识一个线程。