您的位置:首页 > 编程语言

Windows高级编程之线程

2009-10-07 21:22 155 查看
线程也是由两个部分组成的:
• 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
• 另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间需要许多系统资源。系统中要保留大量的记录,这要占用大量的内存。另外,由于.exe和.dll文件要加载到一个地址空间,因此也需要文件资源。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。
由于线程需要的开销比进程少,因此始终都应该设法用增加线程来解决编程问题,而要避免创建新的进程。但是,这个建议并不是一成不变的。许多程序设计用多个进程来实现会更好些。应该懂得权衡利弊,经验会指导你的编程实践。
6.1 何时创建线程
线程用于描述进程中的运行路径。每当进程被初始化时,系统就要创建一个主线程。该线程与C/C++运行期库的启动代码一道开始运行,启动代码则调用进入点函数WinMain ,并且继续运行直到进入点函数返回并且C/C++运行期库的启动代码调用ExitProcess为止。对于许多应用程序来说,这个主线程是应用程序需要的唯一线程。不过,进程能够创建更多的线程来帮助执行它们的操作。
多线程的应用举例:
• 电子表格应用程序能够在后台执行各种计算。
• 字处理程序能够执行重新分页、拼写和语法检查及在后台进行打印。
• 文件可以在后台拷贝到其他介质中。
• We b浏览器在后台与它们的服务器进行通信。因此,在来自当前 We b站点的结果输入之前,用户可以缩放浏览器的窗口或者转到另一个We b站点。
6.2 何时不能创建线程
几乎在所有的应用程序中,所有用户界面的组件(窗口)应该共享同一个线程。单个线程应该创建窗口的所有子窗口。有时在不同的线程上创建不同的窗口是有用的,不过这种情况确实非常少见。通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个GetMessage循环。进程中的所有其他线程都是工作线程,它们与计算机或I/O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。
6.3 编写第一个线程函数
每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
    DWORD dwResult = 0;
    ...
    return dwResult;
}
线程函数的几个问题:
• 主线程的进入点函数的名字必须是WinMain,而线程函数可以使用任何名字。实际上,如果在应用程序中拥有多个线程函数,必须为它们赋予不同的名字,否则编译器/链接程序会认为你为单个函数创建了多个实现函数。
• 线程传入参数自主定义。
• 线程函数必须返回一个值,它将成为该线程的退出代码。这与 C / C + +运行期库关于让主线程的退出代码作为进程的退出代码的原则是相似的。
• 线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,这可能破坏变量的内容。然而,
参数和局部变量是在线程堆栈中创建的,因此它们不太可能被另一个线程破坏。
6.4 CreateThread函数
HADNLE CreateThread(PSECURITY_ATTRIBUTES psa,DWORD cbStack,PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam,DWORD fdwCreate,PDWORD pdwThreadID);
当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。
系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
注意:CreateThread函数是用来创建线程的Windows函数。不过,如果你正在编写C/C++代码,决不应该调用CreateThread。相反,应该使用Visual C++运行期库函数_beginthreadex。
psa:
psa参数是指向SECURITY_ATTRIBUTES结构的指针。NULL时采用内核默认安全属性,否则设定一个SECURITY_ATTRIBUTES结构。设定成员bInheritHandle为TRUE
cbStack
cbStack参数用于设定线程可以将多少地址空间用于它自己的堆栈。每个线程拥有它自己的堆栈。
pfnStartAddr和pvParam
pfnStartAddr参数用于指明想要新线程执行的线程函数的地址。线程函数的pvParam参数与原先传递给CreateThread的pvParam参数是相同的。该初始化数据既可以是数字值,也可以是指向包含其他信息的一个数据结构的指针。
创建多个线程,使这些线程拥有与起始点相同的函数地址,这是完全合乎逻辑的并且是非常有用的。Windows是个抢占式多线程系统,这意味着新线程和调用CreateThread的线程可以同时执行。
fdwCreate:
dwCreate参数可以设定用于控制创建线程的其他标志。它可以是两个值中的一个。如果该值是0,那么线程创建后可以立即进行调度。如果该值是CREATE_SUSPENDED,系统可以完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。
pdwThreadID
是DWORD的一个有效地址,,CreateThread使用这个地址来存放系统分配给新线程的ID。
6.5   终止线程的运行
• 线程函数返回(最好使用这种方法) 。
• 通过调用ExitThread函数,线程将自行撤消(最好不要使用这种方法) 。
• 同一个进程或另一个进程中的线程调用TerminateThread函数(应该避免使用这种方法) 。
• 包含线程的进程终止运行(应该避免使用这种方法) 。
6.6   线程的一些性质

一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间分配而来的,
4000
因为线程并不拥有它自己的地址空间。然后系统将两个值写入新线程的堆栈的上端(线程堆栈总是从内存的高地址向低地址建立) 。写入堆栈的第一个值是传递给CreateThread的pvParam参数的值。紧靠它的下面是传递给CreateThread的pfnStartAddr参数的值。
每个线程都有它自己的一组CPU寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的CPU寄存器的状态。线程的这组CPU寄存器保存在一个结构(在WinNT.h头文件中作了定义)中。CONTEXT结构本身则包含在线程的内核对象中。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。记住,线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。当线程的内核对象被初始化时,CPU结构的堆栈指针寄存器被设置为线程堆栈上用来放置pfnStartAddr的地址。指令指针寄存器置为称为BaseThreadStart的未文档化(和未输出)的函数的地址中。该函数包含在Kernel32.dll模块中(这也是实现CreateThread函数的地方)。
6.7   C/C++运行期库的考虑

必须有一种机制,使得线程能够引用它自己的errno变量,但是又不触及另一个线程的errno变量。
若要使多线程 C和C + +程序能够正确地运行,必须创建一个数据结构,并将它与使用C / C + +运行期库函数的每个线程关联起来。当你调用 C / C + +运行期库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。
若要创建一个新线程,绝对不要调用操作系统的CreateThread函数,必须调用C/C++运行期库函数_beginthreadex:
_beginthreadex代码在Threadex.c中
unsigned long _beginthreadex(void *security,unsigned stack_size,unsigned (*start_address)(void *),void *arglist,unsigned initflag,unsigned *thrdaddr);
{
    _ptiddata ptd //pointer to thread's data block
    unsigned long thdl; //Thread's handle
   
    //Allocate data block for the new thread
    if((ptd == _calloc_crt(1,sizeof(struct tiddata))) == NULL)
        goto error_return;
    //Initialize the data block
    initptd(ptd);
    //Save the desired thread function and the parameter
    //we want it to get in the data block
    ptd->_initaddr = (void *)pfnStartAddr;
    ptd->_initarg  = pvParam;
    //Create the new Thread
    thdl = (unsigned long)CreateThread(psa,cbStack,_threadstartex,(PVOID)ptd,fdwCreate,pdwThreadID);
    if(thdl == NULL)
    {
        //Thread couldn't be created,cleanup and return failure
        goto error_return;
    }
    //Create created OK,return the handle
    return(thdl);
error_return:
    //Error:data block or thread couldn't be created
    _free_crt(ptd);
    return (unsigned long)0L);
}
关于_beginthreadex的一些要点:
• 每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构。
• 传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。传递给该函数的参数也保存在该数据块中。
• _beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法。
• 当调用CreateThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。还有,传递给线程函数的参数是tiddata结构而不是pvParam的地址。
• 如果一切顺利,就会像CreateThread那样返回线程句柄。如果任何操作失败了,便返回NULL。
void __cdecl _endthreadex(unsigned retcode);
关于_endthreadex的一些要点:
• C运行期库的_getptd函数内部调用操作系统的TlsGetValue函数,该函数负责检索调用线程的tiddata内存块的地址。
• 然后该数据块被释放,而操作系统的ExitThread函数被调用,以便真正撤消该线程。当然,退出代码要正确地设置和传递。
一旦数据块被初始化并且与线程联系起来,线程调用的任何需要单线程实例数据的 C / C + +运行期库函数都能很容易地(通过TlsGetValue)检索调用线程的数据块地址,并对线程的数据进行操作。
6.8   对自己的ID概念应该有所了解
HANDLE GetCurrentProcess()
HANDLE GetCurrentThread()
返回调用线程的进程的伪句柄或线程内核对象的伪句柄。调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用CloseHandle,将伪句柄作为参数来传递,那么CloseHandle就会忽略该函数的调用并返回FALSE。
当调用一个需要进程句柄或线程句柄的Wi n d o w s函数时,可以传递一个伪句柄,使该函数执行它对调用进程或线程的操作。
DWORD GetCurrentProcessId()
DWORD GetCurrentThreadId()
查询它的进程的唯一I D或它自己的唯一I D:
将伪句柄转换为实句柄
所谓“实句柄” ,我是指用来明确标识一个独一无二的线程的句柄。线程的伪句柄是当前线程的句柄,也就是说,它是调用函数的线程的句柄。
DuplicateHandle执行这一转换
BOOL DuplicateHandle(HANDLE hSourceProcess,HANDLE hSource,HANDLE hTargetProcess,PHANDLE phTarget,DWORD fdwAccess,BOOL bInheritHandle,DWORD fdwOptions);
通常可以使用这个函数, 用与另一个进程相关的内核对象来创建一个与进程相关的新句柄。
由于DuplicateHandle会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给CloseHandle,从而递减对象的使用计数,这一点很重要。
/*转换线程句柄*/
DuplicateHandle(GetCurrentProcess(),GetCurrentThread(),GetCurrentProcess(),&hThreadParent,0,FALSE,DUPLICATE_SAME_ACCESS);
/*转换进程句柄*/
DuplicateHandle(GetCurrentProcess(),GetCurrentProcess(),GetCurrentProcess(),&hProcess,0,FALSE,DUPLICATE_SAME_ACCESS);

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息