首页 > 代码库 > 【详解】消息队列和线程关系
【详解】消息队列和线程关系
1.进程-线程-消息队列
简单的来说,什么是进程?什么是线程?打个比方,你的程序要执行,操作系统就会把你的exe文件加载到内存中,那就生成一个进程了(当然还包含分配到的资源等);对于线程,你可以理解成是一个程序里的不同部分,这有点类似函数,所不同的是各个线程是同时执行的。
例如,你的主线程创建了另一个副线程,那么这两个线程是同时在工作的,不存在调用 - 返回的概念。
一个进程里可以有多个线程在执行,称为执行实例。
shining:因为线程的资源是从进程资源中分配出来的,因此同一个进程中的多个线程会有共享的内存空间,这样可能会引起多个线程的同步问题,调度不好,就会-出问题,比如A线程要用的资源必须等待B线程释放,而B也在等待其他资源释放才能继续。这就是有些网友碰见的问题:同一个测试场景,用线程并发就会超时失败或报-错,而用进程并发就没错。这是因为进程影响互相极小,当然开进程的资源消耗就比较大,这是一个副作用。
根据我的理解,进程应该是比较大的概念,一个进程开始时至少会有一个主线程 ( 即主执行实例 ) ,这是在系统加载你的程序的时候所创建的主执行流程。一般对外部来说只能看到进程,例如在Win2000 的任务管理器里面查看到的只有进程 ( Process ) 而已。用 Ctrl + Shift + ESC 可以在 Win2000里调出任务管理器。而消息队列则是与线程 ( Thread ) 相关的,换句话说,一个线程只能有一个消息队列 (queue) 与之相对应。这跟之前说的有点不同,一个进程里面可以有多个线程;但是一线程里面就不能超出一个消息队列( Win98 里面甚至可以没有消息队列 )。
消息队列是在什么时候生成的呢?在 Win2000里面,从一开始创建线程就已经有了。( 在 Win98里,我估计是在创建过窗口之后,留给你去证实 )
说了半天,可能一些刚入门的朋友还不知道什么是消息队列呢?
其实,Windows操作系统是一个基于事件驱动的系统。它把握诸如鼠标,键盘输入等东西化为事件代号,发送到你的程序的消息队列里面去,你的程序则每次提取一个事件,根据事件的性-质执行相应的操作,不断循环而已。
shining notes:消息机制在unix平台下也是存在的,是进程间通讯的方式,一般由操作系统来提供。微软提倡编程人员使用事件驱动的编程方法。你也可以向自己线程的消息队列里发送假消息,自己骗自己也是可以的( 虚伪)!使用 PostThreadMessage 函数即可。
编出多线程的程序其实并不难,难点其实在于线程同步 ( 线程间协调工作 ),下面的源程序正是为了简单介绍多线程编程的。
( 阅读的时候不要忘了主线程的入口是 WinMain 函数 )
// File Name: WinMain.cpp
#define WIN32_LEAN_AND_MEAN // Say No to MFC !!
#include <windows.h>
char Temp[77] = "";
// 我自定义的新线程入口 MyThread 函数
DWORD WINAPI MyThread( LPVOID lpParameter )
{
long ThrVal = 1 ;
for (ThrVal = 1; ThrVal < 16; ThrVal++ )
{
wsprintf( Temp , "这是副线程第 %ld 次显示" , ThrVal );
MessageBox( NULL, Temp, "第二线程内容 __CopyRight -
`海风 ", MB_OK | MB_TOPMOST );
}
return 1 ;
} // 副线程结束
// Name: WinMain() 主线程的入口
// ------ ---------- ----------- ---------
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
DWORD dwThreadId, dwMyThrdParam = 1; //
第一个参数是新线程的 ID 号,第二个参数略
HANDLE hThread; // 副线程的 handle
// 调用函数创建新的线程,新线程入口是 MyThread函数
hThread = CreateThread( NULL, // 没有(或默认) 的属性
0, // 使用默认堆栈大小
MyThread, // 我的线程入口函数
&dwMyThrdParam, // argument to thread function
0, // 使用默认 creation flags
&dwThreadId); // Win98里不能省略
// 已经创建了新的副线程
MessageBox( NULL, "已经创建了新的副线程、\n" " 按确定结束主线程!",
"主线程信息显示 __CopyRight - `海风 ", MB_OK | MB_TOPMOST );
ExitProcess(0);
return NULL;
}
如果说消息队列只有一个(在一个线程内),那么消息队列可以容纳多少条消息呢?我编程序去验证了一下:
// File Name: WinMain.cpp
#define WIN32_LEAN_AND_MEAN // Say No to MFC !!
#include <windows.h>
char Temp[77]="Hello world";
// Name: WinMain()
// ------ ---------- ----------- ---------
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
MessageBox( NULL, "按确定开始测试", "Simple_Code__CopyRight - `海风 ",
MB_OK | MB_TOPMOST );
//以上的一句请使用 Win98的朋友别删除,因为在调用过创建窗口之类的函数后,你的线程才具有消息队列!
DWORD CurThreadID = GetCurrentThreadId( ); // 取得当前线程的ID (标识号)
long i = 1 ;
for ( i = 1; i < 900000; i++)
{
if (! PostThreadMessage( CurThreadID, WM_USER , 11, 22 ) )
break;
// 上面的一句是如果不能再添加消息就打断 for 循环
}
wsprintf (Temp, "具体的消息队列长度是 %ld ", i);
MessageBox( NULL, Temp, "Sample_Code__CopyRight - `海风 ", MB_OK | MB_TOPMOST );
ExitProcess(0);
return NULL;
}
显示的结果是: 10000 ,有那么大么?!我也有点不太相信。
如果 10000 都还不够用,那么会怎样?
哎,当然是丢失了!
为了证明消息队列是线程的附属品,查看了MSDN ,最后在 PostThreadMessage() 函数的第一个参数解释那里找到证据,原文如下:
" The function fails if the specified thread does not have a message queue. The system creates a thread‘s message queue when the thread makes its first call to one of the Win32 User or GDI functions. For more information, see the Remarks section. "
含义大概是,当一个线程里面第一次调用 Win32 User 类函数 (或图形界面类函数)的时候,系统会为该线程创建一个消息队列,否则就没有消息队列。
shining notes:个人认为,消息队列其实和线程并无任何联系,只是windows系统将二者结合到了一起,成为windows系统的处处可见的消息机制。在 unix-系统下,我看到的是:如果哪个程序需要接受消息,它将获得一个mailbox(不用向系统申请,可直接调用系统层次的API即可),并不断轮询(一个forever的循环)去查收这个mail box是否有消息。这应该是程序和消息队列结合的雏形吧。有不对的地方,请各位指出。
不过,这个说法我认为在 Win2000里面是不适用的,其实在系统创建线程的同时就创建了一个相关消息队列(默认操作)。
我尝试过没有调用任何其他函数的情况下也可以成功发送消息到队列。
这个由系统维护的消息队列也挺自动化的!举个简单例子是系统会自动调整 WM_PAINT 消息的数量使之不会重复;
更复杂一点的例子是你向线程里的一个窗口发送消息,然后Destroy 那个窗口,最后才检测消息队列。系统会认为那个窗口已经不存在而将与那窗口相关的多余消息一并删除掉了,所以你一定没法在消息队列里找到先前你发送的那条消息。
2.线程与消息队列
当一个线程第一次被创建时,系统假定线程不会用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦该线程调用一个与图形用户界面有关的函数 ( 如检查它的消息队列或建立一个窗口 ),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配了一个THREADINFO结构,并将这个数据结构与线程联系起来。
THREADINFO结构体如下:
1.将消息发送到线程的消息队列
当线程有了与之联系的THREADINFO结构时,消息就有自己的消息队列集合。
通过调用函数 BOOL PostMesssage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
可以将消息放置在线程的登记消息队列中。
当一个线程调用这个函数时,系统要确定是哪个线程建立了用 hwnd 参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且该函数还设置QS_POSTMESSAGE唤醒位。函数 PostMesssage 在登记了消息后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。
还可通过调用函数 BOOL PostThreadMesssage(DWORD dwThreadId, UINT uMsg, WPARAM wParam, LPARAM lParam) 将消息放置在线程的登记消息队列中,同 PostMesssage 函数一样,该函数在向线程的队列登记消息后立即返回,调用该函数的线程不知道消息是否被处理。
向线程的队列发送消息的函数还有 VOID PostQuitMesssage(int nExitCode) ;
该函数可以终止线程消息的循环,调用该函数类似于调用:PostThreadMesssage(GetCurrenThreadId( ), WM_QUIT, nExitCode, 0);但 PostQuitMesssage 并不实际登记一个消息到任何队列中。只是在内部,该函数设定 QS_QUIT 唤醒标志,并设置 THREADINFO 结构的 nExitCode 成员。
2.向窗口发送消息
将窗口消息直接发送给一个窗口过程可以使用函数 LRESULT SendMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 窗口过程将处理这个消息,只有当消息被处理后,该函数才能返回。即具有同步的特性。
该函数的工作机制:
2.1 如果调用该函数的线程向该线程所建立的窗口发送了一个消息,SendMessage 就很简单:它只是调用指定窗口的窗口过程,将其作为一个子例程。当窗口过程完成对消息的处理时,它向 SendMessage 返回一个值。SendMessage 再将这个值返回给调用线程。
2.2 当一个线程向其他线程所建立的窗口发送消息时,SendMessage 就复杂很多(即使两个线程在同一个进程中也是如此)。windows 要求建立窗口的线程处理窗口的消息。所以当一个线程调用 SendMessage 向一个由其他进程所建立的窗口发送一个消息,也就是向其他线程发送消息,发送线程不可能处理该窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口的过程的代码和数据。(对于这个,我有点疑问:同一个进程的不同线程是运行在相同进程的地址空间中,它也采用这种机制,又作何解释呢?)实际上,发送线程要挂起,而有另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行一些复杂的动作。
由于windows使用上述方法处理线程之间的发送消息,所以有可能造成线程挂起,严重的会出现死锁。
利用一下4个函数可以编写保护性代码防护出现这种情况。
1. LRESULT SendMessageTimeout( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout , PDWORD_PTR pdwResult);
2. BOOL SendMessageCallback( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, SENDSYNCPROC pfnResultCallback, ULONG_PTR dwData);
3. BOOL SendNotifyMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
4.BOOL ReplyMessage( LRESULT lResult);
另外可以使用函数 BOOL InSendMessage( ) 判断是在处理线程间的消息发送,还是在处理线程内的消息发送
3.CreateEvent和SetEvent函数
当你创建一个线程时,其实那个线程是一个循环,不像上面那样只运行一次的。这样就带来了一个问题,在那个死循环里要找到合适的条件退出那个死循环,那么是怎么样实现它的呢?在Windows里往往是采用事件的方式,当然还可以采用其它的方式。在这里先介绍采用事件的方式来通知从线程运行函数退出来,它的实现原理是这样,在那个死循环里不断地使用WaitForSingleObject函数来检查事件是否满足,如果满足就退出线程,不满足就继续运行。当在线程里运行阻塞的函数时,就需要在退出线程时,先要把阻塞状态变成非阻塞状态,比如使用一个线程去接收网络数据,同时使用阻塞的SOCKET时,那么要先关闭SOCKET,再发送事件信号,才可以退出线程的。下面就来演示怎么样使用事件来通知线程退出来。
函数CreateEvent声明如下:
WINBASEAPI
__out
HANDLE
WINAPI
CreateEventA(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset,
__in BOOL bInitialState,
__in_opt LPCSTR lpName
);
WINBASEAPI
__out
HANDLE
WINAPI
CreateEventW(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset,
__in BOOL bInitialState,
__in_opt LPCWSTR lpName
);
#ifdef UNICODE
#define CreateEvent CreateEventW
#else
#define CreateEvent CreateEventA
#endif // !UNICODE
lpEventAttributes是事件的属性。
bManualReset是指事件手动复位,还是自动复位状态。
bInitialState是初始化的状态是否处于有信号的状态。
lpName是事件的名称,如果有名称,可以跨进程共享事件状态。
调用这个函数的例子如下:
#001 #pragma once
#002
#003 //线程类。
#004 //蔡军生 2007/09/23 QQ:9073204
#005 class CThread
#006 {
#007 public:
#008
#009 CThread(void)
#010 {
#011 m_hThread = NULL;
#012 m_hEventExit = NULL;
#013 }
#014
#015 virtual ~CThread(void)
#016 {
#017 if (m_hThread)
#018 {
#019 //删除的线程资源。
#020 ::CloseHandle(m_hThread);
#021 }
#022
#023 if (m_hEventExit)
#024 {
#025 //删除事件。
#026 ::CloseHandle(m_hEventExit);
#027 }
#028
#029 }
#030
#031 //创建线程
#032 HANDLE CreateThread(void)
#033 {
#034 //创建退出事件。
#035 m_hEventExit = ::CreateEvent(NULL,TRUE,FALSE,NULL);
#036 if (!m_hEventExit)
#037 {
#038 //创建事件失败。
#039 return NULL;
#040 }
#041
#042 //创建线程。
#043 m_hThread = ::CreateThread(
#044 NULL, //安全属性使用缺省。
#045 0, //线程的堆栈大小。
#046 ThreadProc, //线程运行函数地址。
#047 this, //传给线程函数的参数。
#048 0, //创建标志。
#049 &m_dwThreadID); //成功创建后的线程标识码。
#050
#051 return m_hThread;
#052 }
#053
#054 //等待线程结束。
#055 void WaitFor(DWORD dwMilliseconds = INFINITE)
#056 {
#057 //发送退出线程信号。
#058 ::SetEvent(m_hEventExit);
#059
#060 //等待线程结束。
#061 ::WaitForSingleObject(m_hThread,dwMilliseconds);
#062 }
#063
#064 protected:
#065 //
#066 //线程运行函数。
#067 //蔡军生 2007/09/21
#068 //
#069 static DWORD WINAPI ThreadProc(LPVOID lpParameter)
#070 {
#071 //转换传送入来的参数。
#072 CThread* pThread = reinterpret_cast<CThread *>(lpParameter);
#073 if (pThread)
#074 {
#075 //线程返回码。
#076 //调用类的线程处理函数。
#077 return pThread->Run();
#078 }
#079
#080 //
#081 return -1;
#082 }
#083
#084 //线程运行函数。
#085 //在这里可以使用类里的成员,也可以让派生类实现更强大的功能。
#086 //蔡军生 2007/09/25
#087 virtual DWORD Run(void)
#088 {
#089 //输出到调试窗口。
#090 ::OutputDebugString(_T("Run()线程函数运行\r\n"));
#091
#092 //线程循环。
#093 for (;;)
#094 {
#095 DWORD dwRet = WaitForSingleObject(m_hEventExit,0);
#096 if (dwRet == WAIT_TIMEOUT)
#097 {
#098 //可以继续运行。
#099 TCHAR chTemp[128];
#100 wsprintf(chTemp,_T("ThreadID=%d\r\n"),m_dwThreadID);
#101 ::OutputDebugString(chTemp);
#102
#103 //目前没有做什么事情,就让线程释放一下CPU。
#104 Sleep(10);
#105 }
#106 else if (dwRet == WAIT_OBJECT_0)
#107 {
#108 //退出线程。
#109 ::OutputDebugString(_T("Run() 退出线程\r\n"));
#110 break;
#111 }
#112 else if (dwRet == WAIT_ABANDONED)
#113 {
#114 //出错。
#115 ::OutputDebugString(_T("Run() 线程出错\r\n"));
#116 return -1;
#117 }
#118 }
#119
#120 return 0;
#121 }
#122
#123 protected:
#124 HANDLE m_hThread; //线程句柄。
#125 DWORD m_dwThreadID; //线程ID。
#126
#127 HANDLE m_hEventExit; //线程退出事件。
#128 };
上面在第35行创建线程退出事件,第95行检查事件是否可退出线程运行,第58行设置退出线程的事件。
转自:http://blog.163.com/bluesky_07_06_1/blog/static/1644400832010728101414986/