第二章   线程的第一次接触

资源网络收集 感谢原创者

转自http://blog.sina.com.cn/s/blog_5678943c0100d4po.html

本章回答了如下几个问题:

   怎样建立一个线程?怎样终止一个线程?线程的退出码如何获取?

   使用多线程容易引起怎样的问题?如何解决?

   什么是worker线程?什么是GDI线程?它们的区别何在?程序处理上有何不同?各需注意些什么?

       建立线程序

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security attributes

DWORD dwStackSize,                         // initial thread stack size

LPTHREAD_START_ROUTINE lpStartAddress,     // pointer to thread function

LPVOID lpParameter,                        // argument for new thread

DWORD dwCreationFlags,                     // creation flags

LPDWORD lpThreadId                         // pointer to receive thread ID

);

 

调用约定

调用约定定义函数调用时参数传递的方式、堆栈内参数的处理等。

通常使用关键字__stdcall__cdecl__fastcall直接加函数前预以明确。 

#define WINAPI __stdcall

__stdcallPascal程序的缺省调用方式,通常用于Win32 API中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。

C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数vararg的函数(printf)只能使用该调用约定)。

__cdeclCC++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。_cdeclMFC缺省调用约定。

__fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECXEDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈。

thiscall仅仅应用于“C++”成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

关键字__stdcall__cdecl__fastcall可以直接加在要输出的函数前。它们对应的命令行参数分别为/Gz/Gd/Gr。缺省状态为/Gd,即__cdecl

要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs 

几个必须牢记心头的概念

线程之间的执行次序应该视之为随机;

任务切换可能在任何时刻任何地点发生;

线程并不总是立刻启动(即使进程创建时并未设置CREATE_SUSPENDED标志)。 

核心对象(Kernal Objects

CreateThread()传回两个值,用以标识一个新线程。一个是线程句柄,一个是线程ID。线程ID是一个全局变量,可以独一无二地表示系统任一进程中的某个线程。   AttachThreadInput()PostThreadMessage()就需要用到线程ID,这两个函数允许你影响其他线程的消息队列。调试器和进程观察器也需要线程ID

为了安全防护的缘故,不可能根据一个线程ID而获得其句柄。 

所谓handle,其实是个指针,指向操作系统内存中的某样东西。为了维护系统的完整性与安全性,那东西不允许你直接取得。 

Win32核心对象清单:

进程(processes)

线程(threads

文件(files

事件(events

信号量(semaphores

互斥器(mutexes)

管道(pipes。分为namedanonymous两种)

 注意临界区不是核心对象! 

核心对象和GDI对象

核心对象由KERNEL32.DLL管理,GDI对象由GDI32.DLL管理。

GDI对象是Windows的基础部分。在Win16Win32中它们都是由操作系统管理。通常你不需要知道其数据格式。Windows隐藏了实现细节,只是给你一个对象句柄。 

GDI对象和核心对象之间有一个主要的不同。GDI对象有单一拥有者,不是进程就是线程。核心对象可以有一个以上的拥有者,甚至可以跨进程。

为了保持对每一位主人的追踪,核心对象保持了一个引用计数(reference count),以记录有多少handles对应到此对象对象中也记录了哪一个进程或线程是拥有者。如果你调用CreateThread()或是其他会传回handle的函数,引用计数便加1。当你调用CloseHandle()时,引用计数便减1。一旦引用计数降至0,这一核心对象便自动销毁。

由于引用计数的设计,对象有可能在产生该对象之进程结束之后还继续幸存(比如用于进程间通讯的事件对象、信号量等)。Win32提供各种机制,让其他进程得以取得一个核心对象的句柄,如果某个进程握有某个核心对象的句柄,而该对象的原创者(进程)已经作古了,此核心对象并不会被摧毁。 

为什么我应该调用CloseHandle()?

如果进程结束之前没有对它所打开的核心对象调用CloseHandle(),操作系统会自动地把那些对象的应用计数减一。虽然可以依赖操作系统作实体(physical)上的清除(cleanup)工作,然后逻辑上的清除操作不是同一回事,特别是你有许多进程的话。

如果一个进程常常产生工作线程(worker thread)而老不关闭线程的句柄,那么这个进程将有许许多多的线程核心对象留给操作系统去清理。这样的资源泄漏(resource leak)可能会对效率带来负面影响。

心得:CloseHandle()实际上进行的是逻辑清除。尽管操作系统会帮我们物理清除,但只有当进程执行完毕才可以,而且操作系统并不能确切地知道这些核心对象的具体含义,无法知道它们的解构的次序,因此可能会造成一些不期待的问题。显然地,核心对象使用完毕及时清除,这有利于系统效率的提高,所以程序员还是养成及时CoseHandle()这一习惯为好。 

需要注意的是:你不可以依赖因线程结束而清理所有被这一线程产生的核心对象。许多核心对象,是被进程所拥有,而非线程所拥有,在进程结束之前不能够清理它们。

  为什么可以在不结束线程的情况下关闭其句柄?

线程句柄是指向线程核心对象,而不是线程本身。

当你调用CloseHandle()时,只不过表示希望自己和此核心对象不再有任何瓜葛。CloseHandle()唯一做的事情就是把引用计数减1。如果该值为0,对象就会自动地被操作系统销毁。

线程核心对象引用到的那个线程也会令核心对象开启。因此,线程的默认引用计数为2当你调用CloseHandle()时,引用计数减1,当线程结束时,引用计数再减1。只有两个事情都发生了(次序不限),这个对象才会被真正地销毁。

  线程结束代码(Exit Code

BOOL GetExitCodeThread(

HANDLE  hThread,      // handle to the thread

LPDWORD  lpExitCode   // address to receive termination status

);

如果成功,GetExitCodeThread()返回TRUE,否则FALSE。如果失败,可以调用GetLastError()找出原因。如果线程已经结束,那么线程的结束码会被存放在lpExitCode参数中带回来。如果线程尚未结束,lpExitCode带回的是STILL_ACTIVE 

如果线程已经结束,lpExitCode参数返回值可能是:

1.     ExitThread()或TerminateThread()函数中定义的值;

2.     线程函数的返回值;

3.     拥有线程的进程的退出值(进程终止会强制线程终止)。

需要注意的是:不可根据GetExitCodeThread()返回值判断线程是否还在运行。如果线程还在运行,尚未有所谓的结束码时,也会传回TRUE(此时lpExitCode返回STILL_ACTIVE)。

 结束一个线程

1  线程函数结束,结束线程;

2  使用ExitThread();

3  主线程结束了

 VOID ExitThread(

DWORD dwExitCode   // exit code for this thread

);

ExitThread()类似于C runtime library中的Exit()函数。放在该函数后的任何代码,肯定不会被执行。 

结束主线程

程序启动后就执行的那个线程被称为主线程。主线程有两个特点:(1)它必须负责GUIGraphic User Interface)程序中的主消息循环;(2)这一线程的结束会使程序中的所有线程都被强迫结束,程序因此而结束。

需要注意的是,一个线程被强行终止可能会导致它没有机会做清理工作

所以,程序员的一个良好的习惯是:主线程结束前,应优雅等待其它所有线程的结束 

GDI线程和Worker线程

GDI线程的定义是:拥有消息队列的线程。任何一个窗口的消息总是被产生这一窗口的线程抓住并处理。所有对此窗口的改变也都应该由该线程完成

一般而言,GUI线程绝不会去做那些不能够马上完成的事情。否则,界面就会住了。 

Worker线程则只完成事务性的处理。也就是说,Worker线程不能够产生窗口、对话框、消息框、或任何其它与UI有关的东西。

如果Worker线程需要输入输出错误消息,它应该授权给UI线程来做(比如发送消息),并且把结果通知给Worker线程。

初学多线程编程的程序员最容易犯的一个错误就是在Worker线程中直接调用GDI函数。比如通知更新对话框界面UpdateDataFALSE),请求在主窗口的状态行显示提示信息,如此等等。

切记:窗口的改变应该由GDI该线程完成,Worker线程中不能直接更新UI

 

MFC内,有工作者线程和界面线程,其中界面线程中其实也就是比工作者线程多了一个消息循环,可在界面线程内的初始化实例函数中创建对话框,或者文档视图,这样整个GDI界面就可由独立的消息循环来处理了,在这种情况下每个线程可独立的处理GDI。当然对于同一个GDI对象的访问最好不要使用SendMessage而应该使用PostMessage,因为第一个同步,而第二个是异步的,使用PostMessage时要求其参数传递的对象为全局对象,或堆中的变量,不能使用局部变量。

经验总结

1  各线程的数据要分离开来,避免使用全局变量;

2  不要在线程之间共享GDI对象;

3  确定你知道你的线程状态,不要径自结束程序而不等待它们的结束;

4  让主线程处理用户界面。