多线程(Multi-Thread)
一、程序、进程、线程的概念
1、程序(Program)
?程序是一个具体的文件,是计算机指令的集合,存储在磁盘上,如EXE文件。
2、进程(Process)
?进程:是一个正在运行程序的实例,是程序在其自身的地址空间中的一次执行活动。
?进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不
能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,程序不占用系统的运行资源。
?进程由两个部分组成:
内核对象:操作系统用它来管理进程。是系统用来存放进程的统计信息的地方。
地址空间:包含所有可执行模块或DLL模块的代码和数据,以及动态内存所分配的空间,如堆空间和栈空间。
?进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项
操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程地址空间中的代码。
?单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。
?每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当操作系统创建一个进
程时,会自动创建这个进程的第一个线程,称为主线程。此后,主线程可以创建其他的线程。如main()、WinMain()所在的线程一般就是主线程。
?系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB。
?每个进程有它自己的私有地址空间。
3、线程(Thread)
?线程也由两个部分组成:
内核对象:操作系统用它来管理线程。是系统用来存放线程的统计信息的地方。
线程堆栈:它用于维护线程在执行代码时需要的所有参数和局部变量。
?当创建线程时,系统创建一个线程的内核对象。该内核对象不是线程本身,而是操作系
统用来管理线程的较小的数据结构。该数据结构保存了线程的相关统计信息。
?线程总是在某个进程中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。
新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存,以及同一进程中的所有其他线程的堆栈。这使得单个进程中的多个线程能够非常容易地互相通信。
?线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。
?因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量
避免创建新的进程。
?操作系统为每一个运行的线程分配一定的CPU时间----时间片。系统通过一种循环的方
式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。
?Sleep()函数会主动暂停当前线程的时间片,暂时交出控制权,自己去“睡觉”。
?如果计算机拥有多个CPU,多个线程就能真正意义上同时运行了。
?使同一进程中的各线程协调一致地工作称为线程的同步。系统提供了多种同步方法,如:
临界区(CriticalSection),事件(Event),互斥量(Mutex),信号量(Semaphore)等。
?可以使用PostThreadMessage()函数进行线程间的通讯。
4、使用多线程的场合
?帮助理解:一个理发师要为ABC三位贵宾理发,为了不使三个贵宾感到自己有先后之
分,理发师可以为A服务n秒,之后为B服务n秒,再为C服务n秒,然后再为A服务n秒,如此循环;只要n足够小,ABC就感觉到自己没有被怠慢。如果有3个理发师,当然是最理想的了,可以每个理发师真正的为一个贵宾服务。这里的理发师就相当于CPU,一个理发师就是单CPU,三个理发师就是多CPU了;而为三个贵宾理发,就是3个工作任务。
?QQ多人同时聊天。
?火车站多窗口售票。
?大批量文件复制:复制文件本身使用一个线程,显示进度使用一个线程。如果用一个线
程的话,则主界面会失去反应,给用户感觉是死机了。
二、Win32线程函数
Win32 提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。下面选取一些重要函数进行说明。
2.1、创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性,一般NULL
DWORD dwStackSize, //栈的大小,一般设为0
LPTHREAD_START_ROUTINE lpStartAddress, //线程入口函数指针
LPVOID lpParameter, //传递给线程函数的参数
DWORD dwCreationFlags, //创建后挂起或立即执行
LPDWORD lpThreadId //线程的ID,一般NULL );
该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄。
各参数说明如下:
?lpThreadAttributes:指向一个SECURITY_ATTRIBUTES 结构的指针,该结构决定了线
程的安全属性,一般置为NULL;
?dwStackSize:指定了线程的堆栈深度,一般都设置为0;
?lpStartAddress:表示新线程开始执行时代码所在函数的地址,即线程的起始地址。一般
情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc是线程函数名,其原型如下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter //线程创建者传递给线程的参数
);
?lpParameter:指定了线程执行时传送给线程的32位参数,即线程函数的参数;
?dwCreationFlags:控制线程创建后的状态,可以取两种值。如果该参数为0,线程在被
创建后就会立即开始执行;如果为CREATE_SUSPENDED,则创建线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread()被调用;
?lpThreadId:该参数返回所创建线程的ID,一般设为NULL;
2.2、挂起线程
DWORD SuspendThread(HANDLE hThread);
该函数用于挂起参数hThread指定的线程,如果函数执行成功,则线程的执行被挂起。
2.3、唤醒线程
DWORD ResumeThread(HANDLE hThread);
该函数用于唤醒参数hThread指定的线程,结束该线程的挂起状态,并开始执行该线程。
2.4、结束线程
VOID ExitThread(DWORD dwExitCode);
该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。参数dwExitCode 用来设置线程的退出码。只能在线程内部调用该函数,谁调用就会结束谁。
2.5、终止线程
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
一般情况下,线程运行结束之后,线程函数会正常返回,但是应用程序可以调用该函数来强行终止某一线程的执行。各参数含义如下:
hThread:将被终结的线程的句柄;
dwExitCode:用于指定线程的退出码。
2.6、关闭线程句柄
BOOL CloseHandle(HANDLE hObject);
关闭一个已经打开对象的句柄,用在这里,指关闭一个线程的句柄。注意,只是关闭句柄,并不是关闭线程,线程依然在运行中。只是调用本函数的线程不再需要对该线程进行操作,放弃对它的控制,使线程的内部计数减一,当线程的内部计数为0时,线程会自动关闭。
Here 示例0:讲解0MultiThread工程,理解线程的各个步骤。
2.7、设置线程的优先级
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
各参数含义如下:
hThread:线程的句柄;
nPriority:指定线程的优先级,如THREAD_PRIORITY_NORMAL等。
2.8、C运行时的线程创建
C运行时是在Windows操作系统尚未面世时就已经存在的一套C语言的函数库,因为当时并未考虑到多线程的情况,所以在Windows操作系统下用CreateThread创建的线程中调用了某些C运行时函数,如asctime等,则有可能出现问题,为此后来特地增加了CreateThread的C运行时版本_beginthreadex,原型为:
unsigned int _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
2.9、示例1:火车站售票系统(“1Mutex”工程)
创建一个MFC对话框工程MultiThread1,界面布局如下图所示:
映射2个编辑框为变量m_str1和m_str2,代码放在MultiThread1Dlg.cpp的最下方,如下:int nTickets;
HANDLE hThread1;
HANDLE hThread2;
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
CMultiThread1Dlg *pDlg = (CMultiThread1Dlg *)lpParameter;
while (TRUE)
{
if (nTickets > 0)
{
//::Sleep(1);
CString str;
str.Format("%d", nTickets);
pDlg->m_str1 += str + "\r\n"; //如果刚好在此时本线程的时间片用完了
nTickets--; //票号就不会-1,而是去执行线程2,于是就重票了。
}
else
{
break;
}
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
CMultiThread1Dlg *pDlg = (CMultiThread1Dlg *)lpParameter;
while (TRUE)
{
if (nTickets > 0)
{
//::Sleep(1);
CString str;
str.Format("%d", nTickets);
pDlg->m_str2 += str + "\r\n"; //如果刚好在此时本线程的时间片用完了
nTickets--; //票号就不会-1,而是去执行线程2,于是就重票了。
}
else
{
break;
}
}
return 0;
}
void CMultiThread1Dlg::OnBtnStart()
{
nTickets = 30;
m_str1 = "";
m_str2 = "";
hThread1 = ::CreateThread(NULL, 0, ThreadProc1, this, 0, NULL);
hThread2 = ::CreateThread(NULL, 0, ThreadProc2, this, 0, NULL);
//hThread2 = ::CreateThread(NULL, 0, ThreadProc2, this, CREATE_SUSPENDED, NULL);
::CloseHandle(hThread1); //这并不会关闭线程,只是这里不再需要对线程进行
::CloseHandle(hThread2); //操作,放弃对它们的控制,使线程的内部计数减一。
Sleep(1000);
this->UpdateData(FALSE);
}
点击“开始售票”,发现2个窗口都进行了售票,说明多线程代码都进行工作了。
将上述创建线程2的代码倒数第二个参数改成CREATE_SUSPENDED后运行,2号窗口不再售票。
三、线程同步
上述程序有个明显的问题:一票多售。例如:如果在线程1售票到20号后,已经打印出了票号,但还没有执行到nTickets - -语句时,线程1的时间片刚好用完了,线程2得到运行权,但此时nTickets还是20号,于是售出20号票。这样就出现了一票多售的情况。因此,多线程编程有时需要处理同步问题。
要同步多个线程,可以使用临界区(CriticalSection),互斥量(Mutex),事件(Event),信号量(Semaphore)、互锁函数等。
临界区非常适合于在同一个进程内部以序列化的方式访问共享的数据。然而,有时用户希望一个线程与其他线程执行的某些操作取得同步,这就需要使用内核对象来同步线程。常用的内核对象有互斥量、事件和信号量,其他的还包括文件、控制台输入、文件变化通知、可等待的计时器。
每一个内核对象在任何时候都处于两种状态之一:信号态(signaled)和无信号态(nonsignaled)。线程在等待其中的一个或多个内核对象时,如果内核对象处于无信号态,线程自身将被系统挂起,直到等待的内核对象变为有信号状态时,线程才恢复运行。
常用的等待函数有2个:WaitForSingleObject和WaitForMultipleObjects
DWORD WaitForSingleObject( //等待单个内核对象
HANDLE hHandle, //指向内核对象的句柄
DWORD dwMilliseconds //等待的毫秒数,如果为INFINITE,则无限期等待。
);
WaitForSingleObject函数返回值
DWORD WaitForMultipleObjects(//等待多个对象
DWORD nCount, //对象的个数
CONST HANDLE *lpHandles,//对象句柄数组
BOOL bWaitAll, //是否要等到所有的对象都变为信号态
DWORD dwMilliseconds //等待的毫秒数,如果为INFINITE,则无限期等待。)
3.1、使用互斥量(Mutex)同步多线程
相关函数有CreateMutex,ReleaseMutex,WaitForSingleObject等。
3.1.1、创建互斥量
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
互斥量能够同步多个进程间的数据访问。各参数说明如下:
?lpMutexAttributes:指向一个SECURITY_A TTRIBUTES 结构的指针,该结构决定
了线程的安全属性,一般置为NULL;
?bInitialOwner:BOOL类型,如果为真,则创建该互斥量的线程获得该对象的所有
权,否则,该线程不获得其所有权。
?lpName:互斥量对象的名称。如果为NULL,则创建一个匿名的互斥对象。如果不
为空,则在函数调用成功后,调用GetLastError函数将会返回ERROR_ALREADY_EXISTS,可以利用该特性阻止一个进程多次启动。
3.1.2、释放互斥量
BOOL ReleaseMutex(HANDLE hMutex);
访问完共享资源后,利用该函数释放对互斥量的控制权。
3.1.3、请求互斥量的使用权,进而锁定对共享资源的访问。
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
参数含义:
hHandle:所请求的互斥量对象的句柄;
dwMilliseconds:指定等待时间,单位为毫秒。一般设为INFINITE,表示无限等待。
该函数调用后,会一直等到参数dwMilliseconds指定的时间已过,或者在该时间内,互斥对象变成了有信号状态,函数才会返回。
返回值:
WAIT_OBJECT_0:所请求的对象是有信号状态
WAIT_TIMEROUT:指定的时间以过,并且所请求的对象是无信号状态。
WAIT_ABANDONED:所请求的对象是一个互斥对象,并且掀起拥有该对象的线程在终止前没有释放该对象,这时,该对象的所有权将授予当前调用线程,并且该互斥对象被置为无信号状态。
3.1.4、使用互斥量同步方法修改上述代码,结果完全正常了。代码如下,红色部分为新添加的代码:
int nTickets;
HANDLE hThread1;
HANDLE hThread2;
HANDLE hMutex;
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
CMultiThread1Dlg *pDlg = (CMultiThread1Dlg *)lpParameter;
while (TRUE)
{
::WaitForSingleObject(hMutex, INFINITE);
if (nTickets > 0)
{
//::Sleep(1);
CString str;
str.Format("%d", nTickets);
pDlg->m_str1 += str + "\r\n"; //如果刚好在此时本线程的时间片用完了
nTickets--; //票号就不会-1,而是去执行线程2,于是就重票了。
::ReleaseMutex(hMutex);
}
else
{
::ReleaseMutex(hMutex);
break;
}
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
CMultiThread1Dlg *pDlg = (CMultiThread1Dlg *)lpParameter;
while (TRUE)
{
::WaitForSingleObject(hMutex, INFINITE);
if (nTickets > 0)
{
//::Sleep(1);
CString str;
str.Format("%d", nTickets);
pDlg->m_str2 += str + "\r\n"; //如果刚好在此时本线程的时间片用完了
nTickets--; //票号就不会-1,而是去执行线程2,于是就重票了。
::ReleaseMutex(hMutex);
}
else
{
::ReleaseMutex(hMutex);
break;
}
}
return 0;
}
void CMultiThread1Dlg::OnBtnStart()
{
nTickets = 30;
m_str1 = "";
m_str2 = "";
hThread1 = ::CreateThread(NULL, 0, ThreadProc1, this, 0, NULL);
hThread2 = ::CreateThread(NULL, 0, ThreadProc2, this, 0, NULL);
//hThread2 = ::CreateThread(NULL, 0, ThreadProc2, this, CREATE_SUSPENDED, NULL);
hMutex = ::CreateMutex(NULL, FALSE, NULL);
::ReleaseMutex(hMutex); //主线程并不需要hMutex的控制权,释放掉
::CloseHandle(hThread1); //这并不会关闭线程,只是这里不再需要对线程进行
::CloseHandle(hThread2); //操作,放弃对它们的控制,使线程的内部计数减一。
Sleep(1000);
this->UpdateData(FALSE);
::CloseHandle(hMutex);
}
3.1.5、使用命名的互斥量阻止进程多次运行
BOOL CMultiThread1App::InitInstance()
{
HANDLE hMutex = ::CreateMutex(NULL, FALSE, "tickets");
if (hMutex && ERROR_ALREADY_EXISTS == ::GetLastError())
return FALSE;
::ReleaseMutex(hMutex);
//::CloseHandle(hMutex); //必须注释掉,否则不能阻止多次运行
//……
}
3.2、使用事件(Event)同步多线程
与互斥量和信号量不同,互斥量和信号量用于控制对共享数据的访问,而事件发送信号表示某一操作已经完成。有两种事件对象:手动重置事件和自动重置事件。手动重置事件用于同时向多个线程发送信号;自动重置事件用于向一个线程发送信号。
如果有多个线程调用WaitForSingleObject或者WaitForMultipleObjects等待一个自
动重置事件,那么当该自动重置事件变为信号态时,其中的一个线程会被唤醒,被唤醒的线程开始继续运行,同时自动重置事件又被置为无信号态,其他线程依旧处于挂起状态。从这一点看,自动重置事件有点类似于互斥量。
手动重置事件不会被WaitForSingleObject和WaitForMultipleObjects自动重置为无信号态,需要调用相应的函数才能将手动重置事件重置为无信号态。因此,当手工重置事件有信号时,所有等待该事件的线程都将被激活。
事件对象使用CreateEvent函数创建:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,一般为NULL
BOOL bManualReset, //是手动重置吗?
BOOL bInitialState, //初始化成有信号态吗?
LPCTSTR lpName //名称,为NULL则匿名);
参数bManualReset为TRUE时,指定创建的是手动重置事件,否则为自动重置事件;
参数bInitialState表示事件对象被初始化时是信号态还是无信号态;
参数lpName指定事件对象的名称,其他进程中的线程可以通过该名称调用CreateEvent 或者OpenEvent函数得到该事件对象的句柄。
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
无论自动重置事件对象还是手工重置事件对象,都可以通过SetEvent函数设置为有信号态:BOOL SetEvent(HANDLE hEvent);也可以通过ResetEvent函数设置为无信号态:BOOL ResetEvent(HANDLE hEvent);
不过对于自动重置事件不必执行ResetEvent,因为系统会在WaitForSingleObject或者WaitForMultipleObjects返回前,自动将事件对象置为无信号态。
事件对象使用完毕后,应调用CloseHandle函数关闭它。
示例代码见2Event工程。
3.3、使用临界区(Critical Section)同步多线程
临界区是一段连续的代码区域,它要求在执行前获得对某些共享数据的独占的访问权。如果一个进程中的所有线程中访问这些共享数据的代码都放在临界区中,就能够实现对该共享数据的同步访问。临界区只能用于同步单个进程中的线程。
使用前调用InitializeCriticalSection,使用完毕调用DeleteCriticalSection;进入保护代码前调用EnterCriticalSection,离开保护代码时调用LeaveCriticalSection。
示例代码见3CriticalSection工程。
3.4、使用信号量(Semaphore)同步多线程(选修)
信号量内核对象用于资源计数。每当线程调用WaitForSingleObject函数并传入一个信号量对象的句柄,系统将检查该信号量的资源计数是否大于0,如果大于0,系统就将资源计数减去1,并唤醒线程。如果资源计数为0,系统就将线程挂起,直到另外一个线程释放了该对象,释放信号量意味着增加它的资源计数。
信号量与临界区和互斥量不同,它不属于任何线程。因此可以在一个线程中增加信号量的计数,而在另一个线程中减少信号量的计数。
但是在使用过程中,信号量的使用与互斥量非常相似,互斥量可以看作是信号量的一个特殊版本,即可以将互斥量看作最大资源计数为1的信号量。
通过调用CreateSemaphore函数可以创建一个信号量:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes; //安全属性,默认NULL
LONG lInitialCount, //信号量的初始计数
LONG lMaximumCount, //信号量的最大计数
LPCTSTR lpName //对象的名称);
参数lInitialCount指定信号量的初始计数。参数lMaximumCount指定信号量的最大计数。参数lpName指定对象的名称,其他进程中的线程使用该名称调用CreateSemaphore函数或OpenSemaphore函数得到信号量的句柄:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
线程使用ReleaseSemaphore函数释放信号量。
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
该函数可以一次增加信号量的大于1的计数,参数lReleaseCount指出一次增加的量。函数在参数lpPreviousCount中返回该函数调用前的信号量的计数。
信号量常用在如下情况下。M个线程对N个共享资源的访问,其中M>N
下面示例模拟32个线程对4个数据库连接对象的访问:
//数据库连接对象的包装
class CMyConnection
{
private:
BOOL m_bUse;
public:
LPVOID m_pObj; //模拟连接对象
friend class CMyConnectionPool;
};
//数据库缓冲池
class CMyConnectionPool
{
protected:
CMyConnection *m_pConn; //
int m_iCount;
HANDLE m_hSemaphore; //信号量
public:
CMyConnectionPool(int iCount)
{
m_iCount=iCount;
m_pConn=new CMyConnection[iCount];
for(int i=0; i { m_pConn[i].m_bUse=FALSE; //m_pConn[i].m_pObj=...模拟连接对象的初始化 m_hSemaphore=CreateSemaphore(NULL,iCount,iCount, _T("SemaphoreForMyConnectionPool")); } } ~CMyConnectionPool() { delete []m_pConn; CloseHandle(m_hSemaphore); } CMyConnection *GetConnection() { WaitForSingleObject(m_hSemaphore,INFINITE); for(int i=0; i { if(m_pConn[i].m_bUse==FALSE) { m_pConn[i].m_bUse=TRUE; return m_pConn+i; } } ASSERT(FALSE);//应该永远都不会执行到这里 return NULL; } void ReleaseConnection(CMyConnection *pConn) { pConn->m_bUse=FALSE; ReleaseSemaphore(m_hSemaphore,1,NULL); } }; //实例化4个数据库连接对象,并放进缓冲池中使用。 CMyConnectionPool g_pool(4); DWORD WINAPI SubThread(LPVOID lpParam) { CMyConnection *pConn=g_pool.GetConnection(); static long lSubThreadCount=0; InterlockedIncrement(&lSubThreadCount); TRACE(_T("当前线程ID:%X, 子线程数:%d\n"), GetCurrentThreadId(), lSubThreadCount); Sleep(2000);//模拟数据库的访问 InterlockedDecrement(&lSubThreadCount); g_pool.ReleaseConnection(pConn); return 0; } void Test() { const int iThreadCount=32; HANDLE hThreads[iThreadCount]; for(int i=0; i hThreads[i]=CreateThread(NULL,0,SubThread,NULL,0,NULL); WaitForMultipleObjects(iThreadCount,hThreads,TRUE,INFINITE); } 3.5、互锁函数(选修) 互锁函数提供了一套多个线程同步访问一个简单变量的处理机制。 LONG InterlockedIncrement(LONG volatile* lpAddend); 该函数提供多线程情况下,对一个变量以原子操作方式增加1 LONG InterlockedDecrement(LONG volatile* lpAddend); 该函数提供多线程情况下,对一个变量以原子操作方式减少1 LONG InterlockedExchange(LONG volatile* lpTarget,LONG lValue); 该函数提供在多线程情况下,以原子操作方式用lValue给lpTarget指向的目标变量赋值,并返回赋值以前的lpTarget指向的值。 LONG InterlockedExchangeAdd(LONG volatile* lpAddend,LONG lValue) 该函数提供在多线程情况下,以原子的操作方式将lpAddend指向的变量增加lValue, 并返回调用前的lpAddend指向的目标变量的值。 示例: long g_iNum1,g_iNum2; DWORD WINAPI SubThread(LPVOID lpParam) { for(int i=0; i<100000000; i++) { InterlockedIncrement(&g_iNum1); InterlockedExchangeAdd(&g_iNum2,2); } return 0; } void Test() { HANDLE hThreads[2]; g_iNum1=0; g_iNum2=0; hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL); hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL); SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST); SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST); WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); } 3.6、互斥量、事件、临界区三个同步对象的比较 互斥对象和事件对象同步速度较慢,但可以在多个进程的各个线程间进行同步。 临界区对象同步速度较快,但容易死锁,因为在等待进入临界区时无法设置超时值。 通常在编写需要同步的多线程程序时,首选临界区,使用前调用InitializeCriticalSection,使用完毕调用DeleteCriticalSection;进入保护代码前调用EnterCriticalSection,离开保护代码时调用LeaveCriticalSection。都是成对使用的,容易记忆,使用也比较简单,但一定要防止下面要讲到的线程死锁问题。 3.7、线程的死锁 多个线程间如果相互等待对方拥有的资源,将可能发生死锁。因此开发时必须防止死锁的发生。 示例见“4Critical死锁”工程。 3.8、线程间的消息通讯 可以使用PostThreadMessage函数在各个线程之间进行消息传递,进而实现线程间的通讯。 BOOL PostThreadMessage( DWORD idThread, //线程ID,消息将被传递给它 UINT Msg, //消息类型 WPARAM wParam, //消息附加参数(高位) LPARAM lParam //消息附加参数(低位) ); 该函数执行后会将指定消息放进该线程的消息队列中去,并立即返回。在线程函数内部可以通过GetMessage或PeekMessage函数来获取传递来的消息,此时获取到的MSG的hwnd 成员为NULL值。 3.9、实现用户自定义消息的4个步骤: ?定义消息:如#define WM_TIMER_CLOCK WM_USER+2 ?映射消息:如ON_MESSAGE(WM_TIMER_CLOCK, OnClock) ?在.h文件中声明消息函数:如afx_msg LRESULT OnClock(WPARAM wParam, LPARAM lParam); ?在.cpp文件中实现消息函数。 示例见“5ThreadMsg”工程 四、MFC工作者线程和用户界面线程 4.1、工作者线程和用户界面线程的概念 工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。 用户界面线程有自己的消息队列和消息循环,一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。 但对于操作系统(Win32 API)来讲,这两种线程是没有什么本质区别的,和前文所讲的线程概念是一回事。它们都只需线程的启动地址即可执行任务。这里之所以分成两种线程,是由MFC造成的:MFC为了不同的使用目的,把“线程”包装成两种不同的形式;所谓“工作者线程”,只有简单的启动、挂起、终止等控制,而“用户界面线程”,则仿造CWinApp 的形式,创建了主窗口,构造了消息循环,于是,就能够处理“用户界面”了。 工作者线程并不是不可以有窗口,如果你自己写代码创建了窗口,并构造消息循环来处理它,那么,完全也可以用来处理“用户界面”。工作者线程一定是可以发送消息的,这个没有问题,只不过要注意,如果消息携带了用户数据(比如字符串),那就涉及到线程间的数据共享问题了,主要是并发访问冲突的问题。 4.2、创建MFC线程 在MFC中,一般用全局函数AfxBeginThread()来创建并初始化一个线程的运行,该函数有两种重载形式,分别用于创建工作者线程和用户界面线程。 4.2.1、使用AfxBeginThread形式一创建工作者线程: CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, UINT nPriority =THREAD_PRIORITY_NORMAL, UINT nStackSize =0, DWORD dwCreateFlags =0, LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL ); 各参数含义如下: PfnThreadProc:指向工作者线程的执行函数的指针,线程函数原型必须声明如下:UINT ThreadProc(LPVOID pParam); 请注意,ThreadProc()应返回一个UINT类型的值,用以指明该函数结束的原因。一般情况下,返回0表明执行成功。 pParam:传递给线程函数的一个32位参数,执行函数将用某种方式解释该值。它可以是数值,或是指向一个结构的指针,甚至可以被忽略; nPriority:线程的优先级。如果为0,则线程与其父线程具有相同的优先级;nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈相同大小; dwCreateFlags:如果为0,则线程在创建后立刻开始执行。如果为CREATE_SUSPENDED,则线程在创建后立刻被挂起,之后需要用ResumeThread()唤醒。 lpSecurityAttrs:线程的安全属性指针,一般为NULL。 4.2.2、使用AfxBeginThread形式二创建用户界面线程: CWinThread* AfxBeginThread( CRuntimeClass *pThreadClass, int nPriority=THREAD_PRIORITY_NORMAL, UINT nStackSize=0, DWORD dwCreateFlags=0, LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL ); pThreadClass是指向CWinThread的一个派生类的运行时类对象的指针,该类定义了被创建的用户界面线程的启动、退出等;一般使用RUNTIME_CLASS(CMyWinThread) 宏来填充该参数。详见后续的介绍。 其它参数的意义同形式一。其中指定dwCreateFlags参数为CREATE_SUSPENDED,则在适当的地方调用函数ResumeThread()开始执行线程;如果指定为0,则开始执行线程。 使用函数的这个原型生成的线程也有消息机制,在随后的例子“6MFC_Worker_UI”中我们将发现同主线程的机制几乎一样。 创建用户界面线程的步骤: 1.从CWinThread派生一个有动态创建能力的类,如CMyWinThread。 2.在头文件中使用DECLARE_DYNCREATE宏: class CMyWinThread : public CWinThread { DECLARE_DYNCREATE(CMyWinThread) ...... } 3.在CPP文件中使用IMPLEMENT_DYNCREATE宏: IMPLEMENT_DYNCREATE(CMyWinThread, CWinThread) CMyWinThread::CMyWinThread() { } 4.覆盖虚函数InitInstance(),必须覆盖。如: BOOL CMyWinThread::InitInstance() { m_pMainWnd = new CFrameWnd; //见下面的介绍 m_pMainWnd->CreateEx(0, NULL, _T("UI 线程主窗口"), WS_OVERLAPPEDWINDOW, CRect(0,0,300,200),NULL,0); m_pMainWnd->CenterWindow(); m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); return TRUE; } 5.覆盖虚函数ExitInstance(),通常需要覆盖。一般在这里做一些回收和释放工作。 4.2.3、CwinThread类 下面我们对CWinThread类的数据成员及常用函数进行简要说明。 m_hThread:当前线程的句柄; m_nThreadID:当前线程的ID; m_pMainWnd:指向应用程序主窗口的指针 m_bAutoDelete:指示线程终止时是否自动删除CWinThread对象 一般情况下,调用AfxBeginThread()来一次性地创建并启动一个线程,但是也可以通过两步法来创建线程:首先创建CWinThread类的一个对象,然后调用该对象的成员函数CreateThread()来启动该线程。 BOOL CWinThread::CreateThread( DWORD dwCreateFlags=0, UINT nStackSize=0, LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL ); 该函数中的dwCreateFlags、nStackSize、lpSecurityAttrs参数和API函数CreateThread 中的对应参数有相同含义,该函数执行成功,返回非0值,否则返回0。 virtual BOOL CWinThread::InitInstance(); 重载该函数以控制用户界面线程实例的初始化。初始化成功则返回非0值,否则返回0。用户界面线程经常重载该函数,工作者线程一般不使用InitInstance()。 virtual int CWinThread::ExitInstance(); 在线程终结前重载该函数进行一些必要的清理工作。该函数返回线程的退出码,0表示执行成功,非0值用来标识各种错误。同InitInstance()成员函数一样,该函数也只适用于用户界面线程。 示例代码见“6WorkerUI”,其具体创建过程如下: 1.创建一个MFC对话框工程WorkerUI,界面布局如下: 2.为类CWorkerUIDlg添加如下成员变量 CWinThread * m_pWorker; CWinThread * m_pUI; 3.并在CWorkerUIDlg类的构造函数中初始化为NULL CWorkerUIDlg::CWorkerUIDlg(CWnd* pParent /*=NULL*/) : CDialog(CWorkerUIDlg::IDD, pParent) { m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); m_pWorker = NULL; m_pUI = NULL; } 4.添加按钮的响应函数如下: void CWorkerUIDlg::OnBtnStartWorker() //创建Worker线程 { if (m_pWorker == NULL) m_pWorker = AfxBeginThread(WorkerThreadProc, this); } void CWorkerUIDlg::OnBtnStopWorker() //关闭Worker线程 { if (m_pWorker) { ::TerminateThread(m_pWorker->m_hThread, 0); delete m_pWorker; m_pWorker = NULL; } } 5.定义工作者线程的响应函数WorkerThreadProc: UINT WorkerThreadProc(LPVOID pParam) { CWorkerUIDlg *pDlg = (CWorkerUIDlg *)pParam; for (int i = 0; i < 10000; i++) { for (int j = 0; j < 100; j++) { pDlg->m_ctrlProgress.SetPos(j); Sleep(30); } } return 0; } 上面5步演示了工作者线程的使用方法,下面演示用户界面线程的用法。 6.新建一个对话框资源IDD_UI_DIALOG,标题命名为“用户界面线程窗口”,并使用类 向导为其生成一个CUIThreadDlg类 7.新建一个派生于CWinThread类的MFC类:CUIThread类 8.定义一个成员变量:CUIThreadDlg * m_pDlg; 注意要包含UIThreadDlg.h头文件。 9.重载InitInstance函数如下: BOOL CUIThread::InitInstance() { m_pDlg = new CUIThreadDlg; m_pDlg->Create(IDD_UI_DIALOG, CWnd::FromHandle(::GetDesktopWindow())); m_pDlg->ShowWindow(SW_SHOW); m_pDlg->UpdateWindow(); m_pDlg->CenterWindow(); m_pMainWnd = m_pDlg; return TRUE; } 10.重载ExitInstance函数如下: int CUIThread::ExitInstance() { m_pDlg->DestroyWindow(); return CWinThread::ExitInstance(); } 11.为CWorkerUIDlg类添加按钮响应函数: #include "UIThread.h" void CWorkerUIDlg::OnBtnStartUi() { m_pUI = ::AfxBeginThread(RUNTIME_CLASS(CUIThread)); } 12.在类CWorkerUIDlg中添加PostNcDestroy消息响应函数: void CWorkerUIDlg::PostNcDestroy() { if (m_pWorker) { ::TerminateThread(m_pWorker->m_hThread, 0); delete m_pWorker; m_pWorker = NULL; } if (m_pUI) { ::TerminateThread(m_pUI->m_hThread, 0); delete m_pUI; m_pUI = NULL; } CDialog::PostNcDestroy(); } 13.在类CUIThreadDlg中完成相关任务,如添加如下鼠标右键响应函数: void CUIThreadDlg::OnRButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default this->MessageBox("鼠标右键被按下了"); CDialog::OnRButtonDown(nFlags, point); } 说明: 用户界面线程的执行次序与应用程序主线程相同,首先调用用户界面线程类的InitInstance()函数,如果返回TRUE,继续调用线程的Run()函数;该函数的作用是运行一个标准的消息循环,并且当收到WM_QUIT消息后中断,在消息循环过程中,Run()函数检测到线程空闲时(没有消息),也将调用OnIdle()函数。最后Run()函数返回,MFC调用ExitInstance()函数清理资源。 你可以创建一个没有界面而有消息循环的线程,例如:你可以从CWinThread派生一个新类,在InitInstance函数中完成某项任务并返回FALSE,这表示仅执行InitInstance函数中的任务而不执行消息循环,你可以通过这种方法,完成一个工作者线程的功能。