Win32多线程编程学习心得
http://blog.csdn.net/jonathan321/article/details/50782832
博客原文地址:http://jerkwisdom.github.io/study/thread/thread-Summary/
此处博客不再更新。
为什么多线程?
多线程并不一定是最好的,合适才是最好的。
多线程主要的优点是价廉物美,启动快、退出快、与其他线程共享核心对象,很容易实现共产主义的伟大梦想。但是其又有不可预期、测试困难的缺点。
使用好多线程,就是要知道何时应该用多线程,何时不该用。如果应该用多线程,如何解决Race Condition问题?如何共享数据?如何提高效率?如何同步线程和数据?总结起来就是:
有始有终,线程的创建和释放都要靠自己
不抛弃不放弃,等一等线程,让它做完自己的工作
文明有序,资源占用无冲突
但是有时候却不建议使用多线程:
针对于慢速I/O设备,Overlapped I/O更能胜任
程序的健壮性要求很高,值得付出比较多的额外负担,多进程可能更能胜任
操作线程
如何创建线程?
如果要写一个多线程程序,第一步就是创建一个线程,我们可以使用CreateThread API函数,也可以使用_beginthreadex C 函数,其实我大多数时候使用的是Boost库上面的boost::thread对象来创建
线程对象。如果有兴趣可以看看Boost库,这里暂且不讨论Boost库thread。
如果使用上面两个函数,可以去msdn查看。使用上面两种函数创建线程,其线程函数都必须符合以下格式,当然函数名可以更换:
DWORD WINAPI ThreadFunc(LPVOID n);
使用CreateThread API函数或者_beginthreadex函数,可以传回两个值用以识别一个新的线程——返回值Handle(句柄)和输出参数lpThread(线程ID)。为了安全防护的缘故,不能根据一个线程
的ID获得其handle。
如何释放线程?
线程和进程一样,都是核心对象。如何释放线程属于如何释放核心对象的问题。CloseHandle函数在这里起了十分重要的作用。CloseHandle函数的功能是将核心对象的引用计数减1。其不能直接用来
释放核心对象,核心对象只有在其引用计数为0的时候会被操作系统自动销毁。
BOOL CloseHandle(HANDLE hObject);
如果你不调用该函数,即使线程在创建之后执行完毕,引用计数还是不为0,线程无法被销毁。如果一个进程没有在结束之前对它所打开的核心对象调用CloseHandle,操作系统会自动把那些对象的引
用计数减一。虽然操作系统会做这个工作,但是他不知道核心对象实际的意义,也就不可能知道解构顺序是否重要。如果你在循环结构创建了核心对象而没有CloseHandle,好吧!你可能会有几十万个
句柄没有关闭,你的系统会因此没有可用句柄,然后各种异常现象就出现了。记住当你完成你的工作,应该调用CloseHandle函数释放核心对象。
在清理线程产生的核心对象时也要注意这个问题。不要依赖因线程结束而清理所有被这一线程产生的核心对象。面对一个打开的对象,区分其拥有者是进程或是线程是很重要的。这决定了系统何时做清
理工作。程序员不能选择有进程或者线程拥有对象,一切都得视对象类型而定。如果被线程打开的核心对象被进程拥有,线程结束是无法清理这些核心对象的。
线程核心对象与线程
其实这两个是不同的概念。CreateThread函数返回的句柄其实是指向线程核心对象,而不是直接指向线程本身。在创建一个新的线程时,线程本身会开启线程核心对象,引用计数加1,CreateThread
函数返回一个线程核心对象句柄,引用计数再加1,所以线程核心对象一开始引用计数就是2。
调用CloseHandle函数,该线程核心对象引用计数减一,线程执行完成之后,引用计数再减一为零,该核心对象被自动销毁。
结束主线程
首先得了解哪个线程是主线程:程序启动后就执行的线程。主线程有两个特点:
负责GUI主消息循环
主线程结束时,强迫其他所有线程被迫结束,其他线程没有机会执行清理工作
第二个特点也就意味着,如果你不等待其他线程结束,它们没有机会执行完自己的操作,也没有机会做最后的cleanup操作。我遇到过由于没有等待,而出现程序奔溃的情况。反正很危险。
结束线程并获取其结束代码
这个没什么好说的,可以使用ExitThread函数退出线程,返回一个结束代码。GetExitCodeThread函数获取ExitThread函数或者return语句返回的结束代码。不过想通过GetExitCodeThread来等待线
程结束是个很糟糕的注意——CPU被浪费了。下一节提及的WaitForSingleObject才是正道。
终止其他线程
终止其他线程可以使用TerminateThread()函数,也可以使用全局标记。
TerminateThread()函数的缺点是:
1、线程没有机会在结束前清理自己,其堆栈也没有被释放掉,出现内存泄露;
2、任何与此线程有附着关系的DLLs也没有机会获得线程解除附着通知;
3、线程进入的Critical Section将永远处于锁定状态(Mutex会返回wait_abandoned状态)。
4、线程正在处理的数据会处于不稳定状态。
TerminateThread()唯一可以预期的是:线程handle变成激发状态,并且传回dwExitCode所指定的结束代码。
设立全局标记的优点:保证目标线程在结束之前安全而一致的状态
设立全局标记的缺点:线程需要一个polling机制,时时检查标记值。(可以使用一个手动重置的event对象)
等一等线程
等待一个线程的结束
使用WaitForSingleObject最显而易见的好处是你终于可以把以下代码精简成一句了。
for(;;)
{
int rc;
rc = GetExitCodeThread(hThread, &exitCode);
if(!rc && exitCode != STILL_ACTIVE)
break;
}
→ → → → → →
WaitForSingleObject(hThread, INFINITE);
其他好处就是:
busy loop浪费太多CPU时间
可以设定等待时间
等待多个线程的结束
WaitForSingleObject函数不好同时判断多个线程的状态,WaitForMultipleObjects可以同时等待多个线程,可以设定是否等待所有线程执行结束还是只要一个线程执行完立马返回。
在GUI线程中等待
在GUI线程中总是要常常回到主消息循环,上述两个wait api函数会阻塞主消息循环。MsgWaitForMultipleObjects函数可以在对象呗激发或者消息到达时被唤醒而返回。
线程同步
线程同步主要有Critical Sections、Mutex、Semaphores、Event,除了Critical Section是存在于进程内存空间内,其他都是核心对象。
Critical Sections
Critical Section用来实现排他性占有,适用范围时单一进程的各个线程之间。
使用示例:
CRITICAL_SECTION cs ; // here must be global attributes to related thread
InitializeCriticalSection (&cs );
EnterCriticalSection(&cs );
LeaveCriticalSection(&cs );
DeleteCriticalSection(&cs );
Critical Sections注意事项:
一旦线程进入一个Critical Section,再调用LeaveCriticalSection函数之前,就能一直重复的进入该Critical Section。
千万不要在一个Critical section之中调用Sleep()或者任何Wait... API函数。
如果进入Critical section的那个线程结束了或者当掉了,而没有调用LeaveCriticalSection函数,系统就没有办法将该Critical Section清除。
Critical Section的优点:
相对于Mutex来说,其速度很快。锁住一个未被拥有的mutex要比锁住一个未被拥有的critical section,需要花费几乎100倍时间。(critical section不需要进入操作系统核心)
Critical Section的缺陷:
Critical Section不是核心对象,无法WaitForSingleObject,没有办法解决死锁问题(一个著名的死锁问题:哲学家进餐问题)
Critical Section不可跨进程
无法指定等待结束的时间长度
不能够同时有一个Critical section被等待
无法侦测是否已被某个线程放弃
Mutex
Mutex可以在不同的线程之间实现排他性战友,甚至即使那些线程属于不同进程。
使用示例:
HANDLE hMutex ; // global attributes
hMutex = CreateMutex (
NULL, // default event attributes
false, // default not initially owned
NULL // unnamed
);
DWORD dwWaitResult = WaitForSingleObject (hMutex , INFINITE );
if (dwWaitResult == WAIT_OBJECT_0 )
{
// wait succeed, do what you want
...
}
ReleaseMutex(hMutex );
示例解释:
1、HMutex在创建时为未被拥有和未激发状态;
2、调用Wait...()函数,线程获得hMutex的拥有权,HMutex短暂变成激发状态,然后Wait...()函数返回,此时HMutex的状态是被拥有和未激发;
3、ReleaseMutex之后,HMutex的状态变为未被拥有和未激发状态
Mutex注意事项:
Mutex的拥有权并非属于哪个产生它的哪个线程,而是那个最后对此mutex进行Wait...()操作并且尚未进行ReleaseMutex()操作的线程。
如果线程拥有一个mutex而在结束前没有调用ReleaseMutex(),mutex不会被摧毁,取而代之,该mutex会被视为“未被拥有”以及“未被激发”,而下一个等待中的线程会被以
WAIT_ABANDONED_0通知。
Wait...()函数在Mutex处于未被拥有和未被激发状态时返回。
将CreateMutex的第二个参数设为true,可以阻止race condition,否则调用CreateMutex的线程还未拥有Mutex,发生了context switch,就被别的线程拥有了。
Mutex优点
核心对象,可以调用Wait...() API函数
跨线程、跨进程、跨用户(将CreateMutex的第三个参数前加上"Global//")
可以具名,可以被其他进程开启
只能被拥有它的哪个线程释放
Mutex缺点
等待代价比较大
Semaphores
Semaphore被用来追踪有限的资源。
和Mutex的对比
mutex是semaphore的退化,令semahpore的最大值为1,那就是一个mutex
semaphore没有拥有权的概念,也没有wait_abandoned状态,一个线程可以反复调用Wait...()函数以产生锁定,而拥有mutex的线程不论在调用多少次Wait...()函数也不会被阻塞。
在许多系统中都有semaphore的概念,而mutex则不一定。
调用ReleaseSemaphore()的那个线程并不一定是调用Wait...()的那个线程,任何线程都可以在任何时间调用ReleaseSemaphore,解除被任何线程锁定的Semaphore。
Semaphore优点
核心对象
可以具名,可以被其他进程开启
可以被任何一个线程释放
Semaphore缺点
Event
Event通常用于overlapped I/O,或者用来设计某些自定义的同步对象。
使用示例:
HANDLE hEvent ; // global attributes
hEvent = CreateEvent (
NULL, // default event attributes
true, // mannual reset
false, // nonsignaled
NULL // unnamed
);
SetEvent(hEvent);
PulseEvent(hEvent);
DWORD dwWaitResult = WaitForSingleObject (hEvent , INFINITE );
ResetEvent(hEvent);
if (dwWaitResult == WAIT_OBJECT_0 )
{
// wait succeed, do what you want
...
ResetEvent(hEvent );
}
示例解释:
1、CreateEvent默认为非激发状态、手动重置
2、SetEvent把hEvent设为激发状态
3、在手动重置情况下(bManualReset=true),PulseEvent把event对象设为激发状态,然而唤醒所有等待中的线程,然后恢复为非激发状态;
4、在自动重置情况下(bManualReset=false),PulseEvent把event对象设为激发状态,然而唤醒一个等待中的线程,然后恢复为非激发状态;
5、ResetEvent将hEvent设为未激发状态
Event注意事项:
CreateEvent函数的第二个参数bManualReset若为false,event会在变成激发状态(因而唤醒一个线程)之后,自动重置为非激发状态;
CreateEvent函数的第二个参数bManualReset若为true,event会在变成激发状态(因而唤醒一个线程)之后,不会自动重置为非激发状态,必须要手动ResetEvent;
Event优点:
核心对象
其状态完全由程序来控制,其状态不会因为Wait...()函数的调用而改变。
适用于设计新的同步对象
可以具名,可以被其他进程开启
Event缺点:
要求苏醒的请求并不会被存储起来,可能会遗失掉。如果一个AutoReset event对象调用SetEvent或PulseEvent,而此时并没有线程在等待,这个event会被遗失。如Wait...()函数还没来得及调用就发
生了Context Switch,这个时候SetEvent,这个要求苏醒的请求会被遗失,然后调用Wait...()函数线程卡死。
替代多线程
Overlapped I/O
Win32之中三个基本的I/O函数:CreateFile()、ReadFile()和WriteFile()。
设置CreateFile()函数的dwFlagsAndAttributes参数为FILE_FLAG_OVERLAPPED,那么对文件的每一个操作都将是Overlapped。此时可以同时读写文件的许多部分,没有目前的文件位置的概念,每
一次读写都要包含其文件位置。
如果发出许多Overlapped请求,那么执行顺序无法保证。
Overlapped I/O不能使用C Runtime Library中的stdio.h函数,只能使用ReadFile()和WriteFile()来执行I/O。
Overlapped I/O函数使用OVERLAPPED结构来识别每一个目前正在进行的Overlapped操作,同时在程序和操作系统之间提供了一个共享区域,参数可以在该区域双向传递。
多进程
如果一个进程死亡,系统中的其他进程还是可以继续执行。多进程程序的健壮性远胜于多线程。因为如果多个线程在同一个进程中运行,那么一个误入歧途的线程就可能把整个进程给毁了。
另一个使用多重进程的理由是,当一个程序从一个作业平台被移植到另一个作业平台,譬如Unix(不支持线程,但进程的产生与结束的代价并不昂贵),Unix应用程序往往使用多个进程,如果移植成
为多线程模式,可能需要大改。
文献
Win32 MultiThread Study A - Thread KeyWord
Win32 MultiThread Study B - Thread Usage
Win32 MultiThread Study C - Wait
Win32 MultiThread Study D - Synchronization
Win32 MultiThread Study E - Handle Thread
Win32 MultiThread Study E - Handle Process
欢迎访问我的个人博客click me
博客原文地址:Win32 MultiThread Study Summary - Let's Thread
========
win32多线程 (一) 线程创建与结束等待
http://www.cnblogs.com/zhidao-chen/p/3851526.html
#include "stdafx.h"
#include
#include
using namespace std;
DWORD WINAPI ThreadFuncFirst(LPVOID param)
{
int iCount = 50;
while(iCount--){
cout<<"\nThreadFuncFirst:"< }
return 0;
}
DWORD WINAPI ThreadFuncSecond(LPVOID param)
{
int iCount = 50;
while(iCount--){
cout<<"\nThreadFuncSecond:"< }
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwThreadID = 0;
HANDLE handleFirst = CreateThread(NULL, 0, ThreadFuncFirst, 0, 0, &dwThreadID);
if (!handleFirst)
{
cout<<"create thread 1 error:"< }
HANDLE handleSecond = CreateThread(NULL, 0, ThreadFuncSecond, 0, 0, &dwThreadID);
if (!handleSecond)
{
cout<<"create thread 2 error:"< }
//HANDLE arrayHandle[] = {handleFirst, handleSecond};
//WaitForMultipleObjects(2, arrayHandle, TRUE, INFINITE);
WaitForSingleObject(handleFirst, INFINITE);//等待线程返回,用sleep()就太山寨了
WaitForSingleObject(handleSecond, INFINITE);
CloseHandle(handleFirst);//句柄默认值2 这里减1,线程函数执行完后释放资源。
CloseHandle(handleSecond);
return 0;
}
========
Win32 多线程和线程同步
http://blog.csdn.net/zuishikonghuan/article/details/48208357
版权声明:本文为博主原创文章,本文作者授权在知识共享"署名-非商业性使用-相同方式共享" 4.0 (CC BY-NC-SA 4.0) 许可证下发布,您可以自由地在任何媒介以任何形式复制、发行本作品、修改、
转换或以本作品为基础进行创作;您将必须同样提供原作者信息以及协议声明,您不得将本作品用于商业目的,并且您只能采用与本协议相同的许可协议发布基于本作品的演绎作品。
本博文由CSDN博主zuishikonghuan所作,版权归zuishikonghuan所有,转载请注明出处:http://blog.csdn.net/zuishikonghuan/article/details/48208357
多线程:一个进程创建时,默认情况下系统会为它创建一个主线程,(如果使用Native API创建的线程就没有主线程,是空的,必须自己创建主线程),应用程序可以自己创建线程,还有以前写过的一
篇“DLL注入技术”,就是远程在其他进程中创建线程,然后让远程线程load我们的dll。
系统是如何实现多线程的?其实,对于单CPU单核心的设备上,在一个确定的时刻,只能执行内存中的一个指令。所谓的“多任务抢占式操作系统”,其实是将CPU划分了“时间段”,并分配给每一
个线程,系统的任务调度程序会根据时间段切换线程上下文和进程上下文(比如线程的寄存器和状态等等信息就存储在上下文中),这个时间段不能太短,否则一个线程还没干什么事呢就切换走了,浪
费效率,更不能太长,否则用户就感觉程序不是同时运行的。
创建一个线程,标准的Win32 API是CreateThread。
CreateThread函数:
[cpp] view plain copy
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
);
第1个参数:线程内核对象的安全属性,一般置NULL,使用默认设置。
第2个参数:线程栈空间大小。0表示使用默认大小。
第3个参数:新线程所执行的线程函数地址。
线程函数原型:
[cpp] view plain copy
DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
);
lpParameter:通过CreateThread传人的第四个参数。
第4个参数:传给线程函数的参数。
第5个参数:为0表示线程创建之后直接运行,CREATE_SUSPENDED表示线程创建后暂停运行,通过调用ResumeThread事线程运行。
第6个参数:返回线程的ID。
返回值:成功返回新线程的句柄,失败返回NULL。
特别说明:CreateThread后,如果不需要操作线程,可以直接CloseHandle掉这个线程句柄,关闭这个线程句柄不会影响线程运行。
关于_beginthreadex函数:
另外还有一个_beginthreadex函数,也是用来创建线程的
很多资料都一再强调“应该使用_beginthreadex函数,不要使用CreateThread函数”,这样说其实是有道理的。
MSDN上说:
A thread in an executable that calls the C run-time library (CRT) should use the _beginthreadex and _endthreadex functions for thread management rather than CreateThread and
ExitThread; this requires the use of the multithreaded version of the CRT. If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory
conditions.
因为_beginthreadex设计出来的目的就是为了使C/C++的运行时函数支持多线程的,它先构建了一个C/C++的运行时函数的环境,之后从内部调用了CreateThread。因此如果不需要使用C/C++的运
行时函数,那么,建议使用CreateThread而不要使用_beginthreadex!因为_beginthreadex会造成CPU和内存资源浪费,如果不需要使用C/C++的运行时函数,在对程序效率要求很高的条件下,应
该使用CreateThread。
其实,绝大多数C/C++的运行时函数都是调用了系统的API!
关于关闭一个线程:
在用户模式下(比如在Win32子系统下)关闭线程的正确做法是让线程自己返回,强制结束线程是不可取的。
在内核模式下(驱动程序),还需要做一些其他工作。
线程同步:
当我们创建了多个线程的时候,如果线程访问同一个资源,结果会如何?
假设我们一个全局变量x,创建了两个线程a,b。a和b都要读写x,结果就无法得知了,为何?因为系统会随时调度线程,而又有两个很难克服的原因:
1。对变量的操作,并非是直接访问内存,而是先把内存单元读入寄存器,修改寄存器,然后把寄存器的数据写回内存。
如果线程a刚把x读入寄存器,这时候系统把CPU调度到b上了,b改完x,回到a,a无法知道内存已经变了,于是把寄存器改了之后写入内存,这样就造成了线程b做了无用功!以后程序也可能会出现问
题。
不信可以反汇编你的程序看看汇编代码。。
2。CPU读写内存不是直接读写的,其实,CPU为了提高连续读写内存的效率,引入了一个“高速缓存行”,是将一段内存读到缓存行中再写回内存,因此如果线程调度刚调度到读入缓存行时切换走线
程上下文的话和上面的问题一样。
所以我们需要进行“线程同步”,实现原子访问。
用户模式下常见的线程同步的方法有:事件(Event)、互斥体(Mutex)、信号量(Semaphore)等
其中最常用的是互斥体
关于互斥体:当一个线程获取了互斥体,那么其他线程就不能获取,一个互斥体只能同时被一个线程获得,而其他试图获取互斥体的线程将会等待互斥体的释放,释放后,再有一个线程获取互斥体。
这样,我们就可以在线程访问同一个资源时获取互斥体,访问完了释放,就可以避免上面的问题了,这就是线程同步。
创建互斥体 CreateMutex:
[cpp] view plain copy
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
);
参数1:安全属性,NULL表示默认
参数2:是否被占有。
参数3:命名
返回值:成功返回互斥体的句柄
释放互斥体 ReleaseMutex:
[cpp] view plain copy
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
参数:互斥体句柄
得到互斥体 WaitForSingleObject:
[cpp] view plain copy
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
参数1:要等待对象的句柄(要获取的互斥体的句柄)
参数2:超时间隔,以毫秒为单位。如果指定一个非零值,则该函数等待,直到该对象处于终止状态或到达时间间隔。INFINITE表示一直等待到对象处于终止状态。
等待线程完成:使用WaitForSingleObject等待线程句柄即可。
例子:
有必要说一句,这里因为只有两个线程,每个线程只操作一次,效果不明显,你可以用for循环,让两个个线程分别输出不同的内容很多次,那么加不加线程同步效果就很明显了。
[cpp] view plain copy
#include
#include
int x = 0;
DWORD WINAPI ThreadProc(LPVOID lpParameter){
//获取主线程传来的互斥体句柄
HANDLE* pMutex = (HANDLE*)lpParameter;
//获取互斥体,如果被其他线程获取了就一直等待下去
WaitForSingleObject(*pMutex, INFINITE);
//这些操作可视为原子访问
x++;
//释放互斥体
ReleaseMutex(*pMutex);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//创建互斥体
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("MyMutex1"));
//创建线程
HANDLE t1 = CreateThread(NULL, 0, ThreadProc, &hMutex, 0, NULL);
HANDLE t2 = CreateThread(NULL, 0, ThreadProc, &hMutex, 0, NULL);
//等待线程退出
WaitForSingleObject(t1, INFINITE);
WaitForSingleObject(t2, INFINITE);
//关闭句柄,释放资源
CloseHandle(hMutex);
printf("%d", x);
getchar();
return 0;
}
========
Win32多线程 创建线程、获取线程执行状态、退出线程、错误处理
http://blog.csdn.net/xiaoding133/article/details/7770579
产生一个线程:
[cpp] view plain copy
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD描述施行与这一新线程的security属性,NULL表示使用缺省值,在windows 95中忽略该参数
DWORD dwStackSize, // initial stack size新线程拥有的堆栈大小,0表示缺省大小,1MB
LPTHREAD_START_ROUTINE lpStartAddress, // thread function 函数指针
LPVOID lpParameter, // thread argument 传递到线程函数的参数
DWORD dwCreationFlags, // creation option 允许产生一个暂时挂起的线程,默认为立即运行
LPDWORD lpThreadId // thread identifier //新的线程ID会被传回到这里
);
第一个使用线程范例:
//多线程出现的执行混乱问题
[cpp] view plain copy
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
DWORD WINAPI ThreadFunc(LPVOID); //线程标准函数形式
//#define WINAPI _stdcall
int main()
{
HANDLE hThrd;
DWORD threadId;
int i;
for(i=0;i<5;i++)
{
hThrd=CreateThread(NULL,0,ThreadFunc,(LPVOID)i,0,&threadId);
//返回一个核心对象hThrd
if(hThrd)
{
printf("Thread Start %d\n",i);
CloseHandle(hThrd);
}
}
Sleep(2000); //等待这些线程完成,不加这句,主线程将结束,其他线程将无法完成
return EXIT_SUCCESS;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
int i;
for(i=0;i<10;i++)
{
printf("%d%d%d%d%d%d%d%d\n",n,n,n,n,n,n,n,n);
}
return 0;
}
核心对象:CreateThread()返回的handle被称为一个核心对象(kernel object).其和所谓的GDI对象,如画笔、画刷或DC差不多,前者由KERNEL32.DLL管理,后者由GDI32.DLL管理。所谓handle就是
一个指针,指向操作系统内存空间的某样东西,那东西不允许你直接 取得。
Win32核心对象清单:
1.进程(processes),线程(threads),文件(files),事件(events),信号量(semaphores),互斥器(mutexes),管道(Pipes,分为named和anonymous两种)
这些核心对象可以用来整合许多的线程或进程。
GDI对象和核心对象的主要区别:GDI的对象有单一拥护者,不是进程就是线程。核心对象可以有一个以上的拥有者,甚至可以跨进程。
[cpp] view plain copy
BOOL CloseHandle(
HANDLE hObject // handle to object
);
不关闭可能导致资源泄露。不可以依赖“因线程的结束而清理所有被这一线程产生的核心对象”。许多对象,如文件,是被进程所拥有,而非线程拥有,在进程结束之前不能够清理它们。
线程对象与线程的不同:
线程的handle是指向“线程核心对象”,而不是指向线程本身。当调用CloseHandle()并给一个线程handle时候,就是吧引用计数减1.如果该值变为0,对象会自动被操作系统销毁。
判断线程是否结束:
[cpp] view plain copy
BOOL GetExitCodeThread(
HANDLE hThread, // handle to the thread
LPDWORD lpExitCode // termination status
);
如果线程结束,结束代码会被放在lpExitCode参数中带回来。如果没有结束,lpExitCode的值是STILL_ACTIVE.
如果成功,返回True,否则返回False;
GetExitCodeThread将传回线程函数ThreadFunc的返回值。
GetExitCodeThread等待一个线程的结束,但这并不是最好的方法
第二个线程范例:
//示例GetExitCodeThread的用法,获取线程的状态
[cpp] view plain copy
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#include
DWORD WINAPI ThreadFunc(LPVOID); //线程标准函数形式
int main()
{
HANDLE hThrd1;
HANDLE hThrd2;
DWORD exitCode1=0;
DWORD exitCode2=0;
DWORD threadId;
hThrd1=CreateThread(NULL,0,ThreadFunc,(LPVOID)1,0,&threadId);
if(hThrd1)
printf("Thread 1 launched \n");
hThrd2=CreateThread(NULL,0,ThreadFunc,(LPVOID)2,0,&threadId);
if(hThrd2)
printf("Thread 2 launched \n");
for(;;)
{
printf("Press any key to exit..\n");
getch();
//GetExitCodeThread等待一个线程的结束,但这并不是最好的方法
GetExitCodeThread(hThrd1,&exitCode1); //GetExitCodeThread将传回线程函数ThreadFunc的返回值
GetExitCodeThread(hThrd2,&exitCode2);
if(exitCode1==STILL_ACTIVE)
puts("Thread 1 is still running ...");
if(exitCode2==STILL_ACTIVE)
puts("Thread 2 is still running ...");
if(exitCode1!=STILL_ACTIVE&&exitCode2!=STILL_ACTIVE)
break;
}
CloseHandle(hThrd1);
CloseHandle(hThrd2);
printf("Thread 1 returned %d\n",exitCode1);
printf("Thread 2 returned %d\n",exitCode2);
return EXIT_SUCCESS;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
Sleep((DWORD)n*1000*2);
return (DWORD)n*2;
}
结束一个线程:
前面是靠线程函数的结束而结束线程。有时候需要更强制性的手法结束一个线程要用ExitThread();
[cpp] view plain copy
VOID ExitThread(
DWORD dwExitCode // exit code for this thread 指示此线程之结束代码
);
它可以在任何时候被调用并且绝对不会返回。任何代码若放在此行之下,保证不会被执行。
如果线程还在运行而我的程序的结束了,会怎么样??
结束主线程:程序启动后就执行一个线程称为主线程,主线程有两个特点,第一,负责GUI程序中的主消息循环。第二,这一线程的结束(不论是因为返回或因为调用了ExitThread())会使程序中的所
有线程都被强迫结束,程序也因此结束。其他线程没有机会做清理工作。故在main或WinMain结束之前,总是先等待所有的线程都结束。
建议在主线程中不要调用ExitThread();
第三个线程范例:怎么结束一个线程
[cpp] view plain copy
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
DWORD WINAPI ThreadFunc(LPVOID); //线程标准函数形式
void AnotherFunc(void);
int main()
{
HANDLE hThrd;
DWORD exitCode=0;
DWORD threadId;
hThrd=CreateThread(NULL,0,ThreadFunc,(LPVOID)1,0,&threadId);
if(hThrd)
printf("Thread launched.. \n");
for(;;)
{
bool rc;
rc=GetExitCodeThread(hThrd,&exitCode);
if(rc&&exitCode!=STILL_ACTIVE)
break;
}
CloseHandle(hThrd);
printf("Thread returned %d\n",exitCode);
return EXIT_SUCCESS;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
printf("Thread running ..\n");
AnotherFunc(); //调用一个函数,退出该线程
return 0;
}
void AnotherFunc()
{
printf("About to exit Thread ..\n");
ExitThread(4);
printf("This will never print ..\n"); //这一行不会被打印
}
/****************错误处理*********************/
可以调用GetLastError获得描述
什么是MTVERIFY?
它是一个宏,这个宏内部记录并解释Win32 GetLastError()的结果。如果Win32函数失败,MTVERIFY()会打印一段简短的文字说明。
如何使用MTVERIFY的范例:
错误处理函数
[cpp] view plain copy
//示例错误处理
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
//MTVERIFY宏进入
#include "MtVerify.h"
DWORD WINAPI ThreadFunc(LPVOID);
int main()
{
HANDLE hThrd;
DWORD exitCode=0;
DWORD threadId;
MTVERIFY(hThrd=CreateThread(NULL,0,ThreadFunc,(LPVOID)1,0,&threadId));
if(hThrd)
printf("Thread launched.. \n");
MTVERIFY(CloseHandle(hThrd));
for(;;)
{
bool rc;
MTVERIFY(rc=GetExitCodeThread(hThrd,&exitCode));
if(rc&&exitCode!=STILL_ACTIVE)
break;
}
printf("Thread returned %d\n",exitCode);
return EXIT_SUCCESS;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
printf("Thread running ..\n");
return 0;
}
/**
* MtVerify.h
*
* Error handling for applications in
* "Multitheading Applications in Win32"
*
* The function PrintError() is marked as __inline so that it can be
* included from one or more C or C++ files without multiple definition
* errors. For the examples in this book, this works fine.
* To use the PrintError() in an application, it should be taken out,
* placed in its own source file, and the "__inline" declaration removed
* so the function will be globally available.
*/
#pragma comment( lib, "USER32" )
#include
#include
#define MTASSERT(a) _ASSERTE(a)
// 宏定义 __FILE__ 与__LINE__都是预处理符号提供错误信息的描述
// 如果a返回FALSE就执行PrintError函数
#define MTVERIFY(a) if (!(a)) PrintError(#a,__FILE__,__LINE__,GetLastError())
__inline void PrintError(LPSTR linedesc, LPSTR filename, int lineno, DWORD errnum)
{
LPSTR lpBuffer;
char errbuf[256];
#ifdef _WINDOWS
char modulename[MAX_PATH];
#else // _WINDOWS
DWORD numread;
#endif // _WINDOWS
// 把从GetLastError()返回的错误码转化为错误信息
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
errnum,
LANG_NEUTRAL,
(LPTSTR)&lpBuffer,
0,
NULL );
wsprintfA(errbuf, "\nThe following call failed at line %d in %s:\n\n"
" %s\n\nReason: %s\n", lineno, filename, linedesc, lpBuffer);
// 如果是console程序就输出信息到控制台上
#ifndef _WINDOWS
WriteFile(GetStdHandle(STD_ERROR_HANDLE), errbuf, strlen(errbuf), &numread, FALSE );
// 等待3秒钟是为了使用者看到出错信息
Sleep(3000);
// 如果是窗口程序就一弹出对话框的形式输出错误信息
#else
// 当前exe文件的全路径
GetModuleFileName(NULL, modulename, MAX_PATH);
// 置弹出窗口在最上层以免被忽略
MessageBox(NULL, errbuf, modulename, MB_ICONWARNING|MB_OK|MB_TASKMODAL|MB_SETFOREGROUND);
#endif
// 把结束代码EXIT_FAILURE 交给操作系统
exit(EXIT_FAILURE);
}
多线程综合实例:后台打印,建立一个Win32 Application
[cpp] view plain copy
/*多线程后台打印范例*/
/******************************/
/*
*
*
* Sample code for "Multithreading Applications in Win32"
* This is from Chapter 2, Listing 2-3
*
* Demonstrates background printing
*/
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#include
#include
#include "resource.h"
#include "MtVerify.h"
//
// Macro definitions
//
#define WM_SHOWBITMAP WM_APP
#define MAX_PRINT_JOBS 64
//
// Structures
//
typedef struct
{ // Information passed to background thread for printing
HWND hDlg;
HWND hWndParent;
HDC hDc;
BOOL bPrint; // TRUE if printing;
char szText[256];
} ThreadPrintInfo;
//
// Global variables
//
HANDLE hInst;
HBITMAP gbmpDisplay;
RECT gDisplayRect;
int gNumPrinting = 0;
// Handle to each created thread
HANDLE gPrintJobs[64]; //保存已经创建的线程
// Height of bitmap returned by DrawText
int iHeight;
// HWND of the dialog so other threads can find it.
HWND hDlgMain;
//
// Function declarations
//
int APIENTRY WinMain(HINSTANCE hInstance/*当前程序运行实例的句柄,每个程序可以运行多个实例*/,
HINSTANCE hPrevInstance, /*当前实例前一个实例的句柄 ,win32下为NULL*/
LPSTR lpCmdLine, /*指定传递给命令行的参数*/
int nCmdShow/*指定程序窗口应该如何显示,如最大化等*/);
LRESULT CALLBACK MainWndProc(HWND hWnd/*标志接受消息的具体窗口*/, unsigned msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK PrintDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL PrintDlg_OnInitDialog(HWND hwndDlg, HWND hwndFocus, LPARAM lParam);
void PrintDlg_OnCommand(HWND hDlg, int id, HWND hwndCtl, UINT codeNotify);
void PrintDlg_OnPaint(HWND hwnd);
void PrintText(HWND hwndParent, char *pszText);
void PrintToDisplay(HWND hwndParent, char *pszText);
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
DWORD WINAPI BackgroundPrintThread(LPVOID pVoid);
///
//
// WinMain
//
// Main entry point of application. This will be a
// dialog based app, not a normal window, so this
// routine acts a little differently than "normal".
//
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
HWND hWnd;
WNDCLASS wc;
int index;
hInst = hInstance;
//1.设计一个窗口类
if (!hPrevInstance)
{
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = MainWndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon (hInstance, "GenIco");
wc.hCursor = LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground= GetSysColorBrush(COLOR_BACKGROUND);
wc.lpszMenuName = "PRINTING_MENU";
wc.lpszClassName= "PrintDlgClass";
if (!RegisterClass(&wc))
return FALSE;
}
//2.创建窗口,并返回创建成功后的窗口句柄
hWnd = CreateWindow(
"PrintDlgClass",
"Background Printing",
WS_OVERLAPPED|WS_CAPTION|WS_MINIMIZEBOX|WS_SYSMENU,
CW_USEDEFAULT, // At this point we do not want to
0, // show the window until we know
0, // how big the Dialog Box is so
0, // that we can fit the main window
NULL, // around it.
NULL,
hInstance,
NULL);
//创建打印对话框
hDlgMain = CreateDialog(hInst,
MAKEINTRESOURCE(IDD_PRINT),
hWnd, PrintDlgProc);
//3.显示及刷新窗口
ShowWindow(hWnd, nCmdShow);
ShowWindow(hDlgMain, SW_SHOW);
//4.消息循环
while (GetMessage(&msg, NULL, 0, 0))
{ // Get Next message in queue
if(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg))
{
TranslateMessage(&msg); /* Translate virtual key codes */
DispatchMessage(&msg); /* Dispatches message to window */
}
} // end while
// Wait for all threads to terminate. The Window will
// have already disappeared by this point.
//等待各个线程结束
for (index = 0; index < gNumPrinting; index++)
{
DWORD status;
do
{ // Wait for thread to terminate
GetExitCodeThread(gPrintJobs[index], &status);
Sleep(10);
} while (status == STILL_ACTIVE);
} // end for
return (msg.wParam); /* Returns the value from PostQuitMessage */
}
//主窗口回调函数
LRESULT CALLBACK MainWndProc(HWND hWnd, unsigned msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
break;
case WM_COMMAND:
switch (wParam)
{
case IDM_ABOUT:
DialogBox(hInst, "AboutBox", hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
default:
return (DefWindowProc(hWnd, msg, wParam, lParam));
}
case WM_SETFOCUS:
// ensure that the Dialog Box has the focus
SetFocus(hDlgMain);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
return 0;
}
LRESULT CALLBACK PrintDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hDlg);
hDlgMain = NULL;
break;
case WM_DESTROY:
return TRUE;
break;
case WM_SHOWBITMAP:
if (gbmpDisplay)
DeleteObject(gbmpDisplay);
gDisplayRect = *(RECT*)wParam;
gbmpDisplay = (HBITMAP) lParam;
InvalidateRect(hDlgMain, NULL, TRUE);
break;
HANDLE_MSG(hDlg, WM_INITDIALOG, PrintDlg_OnInitDialog);
HANDLE_MSG(hDlg, WM_COMMAND, PrintDlg_OnCommand);
HANDLE_MSG(hDlg, WM_PAINT, PrintDlg_OnPaint);
default:
return (FALSE);
}
return 0;
}
BOOL PrintDlg_OnInitDialog(HWND hwndDlg, HWND hwndFocus, LPARAM lParam)
{
RECT rect;
// Size parent to fit this dialog
GetWindowRect(hwndDlg, &rect);
SetWindowPos(GetParent(hwndDlg),NULL,
0,0,
rect.right-rect.left,
rect.bottom-rect.top+GetSystemMetrics(SM_CYMENU)
+GetSystemMetrics(SM_CYCAPTION),
SWP_NOMOVE | SWP_NOZORDER);
return TRUE;
}
void PrintDlg_OnCommand(HWND hDlg, int id,HWND hwndCtl, UINT codeNotify)
{
char szText[256];
switch (id)
{
case IDC_PRINT:
GetDlgItemText(hDlg, IDC_EDIT_TEXT, szText, 256);
PrintText(hDlg, szText);
break;
case IDC_DISPLAY:
GetDlgItemText(hDlg, IDC_EDIT_TEXT, szText, 256);
PrintToDisplay(hDlg, szText);
break;
case IDCANCEL:
case IDM_EXIT:
PostMessage(GetParent(hDlg),WM_DESTROY,
(WPARAM)0, (LPARAM)0);
DestroyWindow(hDlgMain);
hDlgMain = NULL;
break;
default:
break;
}
}
void PrintDlg_OnPaint( HWND hwnd )
{
PAINTSTRUCT paint;
HWND hwndCtrl;
HDC hdc;
HDC hDcMem;
HBITMAP bmpOld;
RECT rect;
POINT point;
if (!gbmpDisplay)
return;
hwndCtrl = GetDlgItem(hwnd, IDC_OUTPUT);
hdc = BeginPaint(hwnd, &paint);
GetWindowRect(hwndCtrl, &rect);
point = *((POINT *)&rect);
ScreenToClient(hwnd, &point);
hDcMem = CreateCompatibleDC(NULL);
bmpOld = SelectObject(hDcMem, gbmpDisplay);
// Copy bitmap to screen
MTVERIFY( BitBlt(hdc, point.x+10, point.y+40,
gDisplayRect.right-gDisplayRect.left, gDisplayRect.bottom-gDisplayRect.top,
hDcMem, iHeight, 0, SRCCOPY) );
SelectObject(hDcMem, bmpOld);
DeleteDC(hDcMem);
EndPaint(hwnd, &paint);
}
//
// Asks user which printer to use, then creates
// background printing thread.
//
void PrintText(HWND hwndParent, char *pszText)
{
ThreadPrintInfo *pInfo;
HANDLE hThread;
DWORD dwThreadId;
int result;
DOCINFO docInfo;
PRINTDLG dlgPrint;
// Put up Common Dialog for Printing and get hDC.
memset(&dlgPrint, 0, sizeof(PRINTDLG));
dlgPrint.lStructSize = sizeof(PRINTDLG);
dlgPrint.hwndOwner = hwndParent;
dlgPrint.Flags = PD_ALLPAGES | PD_USEDEVMODECOPIES
| PD_NOPAGENUMS | PD_NOSELECTION | PD_RETURNDC;
dlgPrint.hInstance = hInst;
if (!PrintDlg(&dlgPrint))
return;
// Initialize Printer device
docInfo.cbSize = sizeof(DOCINFO);
docInfo.lpszDocName = "Background Printing Example";
docInfo.lpszOutput = NULL;
docInfo.lpszDatatype = NULL;
docInfo.fwType = 0;
result = StartDoc(dlgPrint.hDC, &docInfo);
result = StartPage(dlgPrint.hDC);
pInfo = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(ThreadPrintInfo));
pInfo->hDlg = hwndParent;
pInfo->hWndParent = hwndParent;
pInfo->hDc = dlgPrint.hDC;
pInfo->bPrint = TRUE;
strcpy(pInfo->szText, pszText);
MTVERIFY( hThread = CreateThread(NULL, 0,
BackgroundPrintThread, (LPVOID)pInfo,
0, &dwThreadId ));
// keep track of all background printing threads
gPrintJobs[gNumPrinting++] = hThread;
}
//
// Shows output on the dialog box.
//
void PrintToDisplay(HWND hwndParent, char *pszText)
{
ThreadPrintInfo *pInfo;
DWORD dwThreadId;
HANDLE hThread;
pInfo = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(ThreadPrintInfo));
pInfo->hDlg = hwndParent;
pInfo->hWndParent = GetDlgItem(hwndParent, IDC_OUTPUT);
pInfo->hDc = GetDC(pInfo->hWndParent);
pInfo->bPrint = FALSE;
strcpy(pInfo->szText, pszText);
MTVERIFY( hThread = CreateThread(NULL, 0,
BackgroundPrintThread,
(LPVOID)pInfo,
0, &dwThreadId ));
// keep track of all background printing threads
gPrintJobs[gNumPrinting++] = hThread;
}
//---------------------------------------------------------
// About Box Handling
//---------------------------------------------------------
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_COMMAND:
if (LOWORD(wParam) == IDOK
|| LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, TRUE);
return (TRUE);
}
break;
default:
return (DefWindowProc(hDlg, message, wParam, lParam));
}
return FALSE;
}
//---------------------------------------------------------
// Background Printing Code后台线程打印函数
//---------------------------------------------------------
DWORD WINAPI BackgroundPrintThread(LPVOID pVoid)
{
ThreadPrintInfo *pInfo = (ThreadPrintInfo*) pVoid;
RECT rect;
RECT rectMem;
HDC hDcMem;
HBITMAP bmpMem;
HBITMAP bmpOld;
int x, y;
int counter = 0;
int nHeight;
HFONT hFont;
HFONT hFontOld;
// Get dimensions of paper into rect
rect.left = 0;
rect.top = 0;
rect.right = GetDeviceCaps(pInfo->hDc, HORZRES);
rect.bottom = GetDeviceCaps(pInfo->hDc, VERTRES);
nHeight = -MulDiv(36, GetDeviceCaps(pInfo->hDc, LOGPIXELSY), 72);
// Create Font
hFont = CreateFont(nHeight, 0,
0, 0, FW_DONTCARE,
FALSE, FALSE, FALSE,
ANSI_CHARSET,
OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS,
PROOF_QUALITY,
VARIABLE_PITCH,
NULL);
MTASSERT( hFont != 0);
// Draw into memory device context
hDcMem = CreateCompatibleDC(pInfo->hDc);
hFontOld = SelectObject(hDcMem, hFont);
iHeight = DrawText(hDcMem, pInfo->szText, -1, &rect, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK | DT_CALCRECT);
rectMem = rect;
rectMem.left = rect.left + iHeight;
rectMem.right = rect.right + (iHeight*2);
bmpMem = CreateCompatibleBitmap(hDcMem,
rectMem.right, rect.bottom);
bmpOld = SelectObject(hDcMem, bmpMem);
OffsetRect(&rect, iHeight, 0);
DrawText(hDcMem, pInfo->szText, -1, &rect,
DT_LEFT | DT_NOPREFIX | DT_WORDBREAK);
// Italicize bitmap. We use GetPixel and
// SetPixel because they are horribly inefficient,
// thereby causing the thread to run for awhile.
for (y = 0; y < iHeight; y++)
{ // Italicize line y
for (x = rectMem.right; x > iHeight; x--)
{ // Move specified pixel to the right.
COLORREF color;
int offset;
offset = y - iHeight;
color = GetPixel(hDcMem, x + offset, y);
if (color != 0)
counter++;
SetPixel(hDcMem, x, y, color);
} // end for x
} // end for y
MTASSERT( counter > 0);
// Copy bitmap of italicized text from memory to device
if (pInfo->bPrint)
{
BitBlt(pInfo->hDc, 50, 50, rectMem.right-rect.left, rectMem.bottom-rect.top,
hDcMem, iHeight, 0, SRCCOPY);
}
SelectObject(hDcMem, hFontOld);
SelectObject(hDcMem, bmpOld);
DeleteDC(hDcMem);
if (!pInfo->bPrint)
{
// We can't just write to the global variable where the
// bitmap is kept or we might overwrite the work of
// another thread, thereby "losing" a bitmap
// Also, if we used PostMessage instead of SendMessage, then
// the rectangle could have been deleted (it's on the stack)
// by the time the main message loop is reached.
SendMessage(pInfo->hDlg, WM_SHOWBITMAP, (WPARAM)&rectMem, (LPARAM) bmpMem);
}
if (pInfo->bPrint)
{ // Finish printing
int result;
result = EndPage(pInfo->hDc);
MTASSERT (result != SP_ERROR);
result = EndDoc(pInfo->hDc);
MTASSERT (result != SP_ERROR);
DeleteDC(pInfo->hDc);
// If we are printing, we are done with the bitmap.
DeleteObject(bmpMem);
}
else
{
ReleaseDC(pInfo->hWndParent, pInfo->hDc);
}
// free data structure passed in.
HeapFree(GetProcessHeap(), 0, pInfo);
return 0;
}
多线程应该遵循下面原则:
1.各个线程的数据要分离开来,避免使用全局变量
2.不要在线程之间共享GDI对象
3.让主线程处理GUI界面
4.要等其他线程结束在结束程序
========