文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>Windows via C/C++ 学习(14)线程基础

Windows via C/C++ 学习(14)线程基础

时间:2011-01-28  来源:nrj

进程只是线程的一个容器,它本身不执行任何代码。线程执行代码并维护进程地址空间内的数据,所有线程共享单个进程的上下文,线程可以执行相同的代码,维护相同的数据,线程也共享所有内核对象句柄。

进程比线程使用更多的系统资源,这主要是因为地址空间的缘故,创建一个进程的地址空间需要大量的系统资源,为了维护这些资源的记录需要大量的内存,同时,.exe 和 .dll 文件加载到地址空间中,也需要维护这些文件资源。而创建一个线程就简单多了,它只需要很少的系统资源——仅仅需要维护一个线程内核对象和一个线程栈,所有只需很少的内存来保存少量的记录。

每一个线程都有一个入口点,线程代码都从那个入口点函数开始执行,主线程的入口点函数必须是 _tmain 或 _tWinMain,其他线程的入口点函数有如下形式:

DWORD WINAPI ThreadFunc(PVOID pvParam) {
        DWORD dwResult = 0;
        ...
        return (dwResult);
}

除主线程外,其它线程的入口点函数可以是任意的名称(主线程入口点函数也可以是任意名称,但必须使用 /ENTRY: 连接器选项告诉链接器是使用哪个函数);传递给线程函数的参数可以是任意编码的任意类型;线程函数必须返回一个值,它表示线程的退出码;当你使用静态变量或全局变量时,任意线程都可以访问,但要注意同时访问可能会破坏变量的内容,而在线程中使用局部变量或函数参数是安全的,因为这些变量在线程栈内保存不会被其它线程轻易地访问到。

CreateThread

任何线程都可以使用 CreateThread 函数创建另一个线程,这个函数的原型如下:

HANDLE CreateThread(
        PSECURITY_ATTRIBUTE     psa,
        DWORD   cbStackSize,
        PTHREAD_START_ROUTINE   pfnStartAddr,
        PVIOD   pvParam,
        DWORD   dwCreateFlags,
        PDWORD  pdwThreadID);

当使用这个函数创建一个线程时,系统创建了一个线程内核对象,这个内核对象不是线程自身,它只是内核中的一小块数据结构,系统用它来管理线程,你可以把它看作一个管理线程的统计信息的数据结构。

系统为在进程地址空间中为线程栈分配内存,新线程运行在与创建它的线程的相同进程上下文中,因此新线程可以访问进程的所有内核对象句柄,进程的所有内存和进程中其它线程的线程栈,这样进程内的所有线程就会比较容易地互相通信。

psa 参数

这个参数是一个指向 SECURITY_ATTRIBUTE 结构的指针,NULL 值表示使用默认的安全属性,如果你想子进程可以继承新创建的线程的句柄,必须将这个数据结构的 bInheritHandel 初始化为 TRUE。

cbStackSize 参数

这个参数指定系统为这个线程初始化的已提交内存大小,系统会自动为这个数字舍入为整页的大小。如果这个参数为 0,系统会使用默认的大小,这个默认值在链接器的 /STACK:[reserve] [,commit] 选项指定,一般保留内存大小为 1M,提交内存大小为一个页面。为保留内存大小设置一个最大限值可以避免应用程序因为无限递归耗尽所有内存。

pfnStartAddr 和 pvParam 参数

这两参数分别表示新线程的入口函数的地址和传递给该函数的参数。

dwCreateFlags 参数

这个参数有两个值可供选择,如果这个值为 0,表示创建一个线程后,这个线程立即执行,如果这个值为 CREATE_SUSPENDED 表示创建一个线程,这个新线程不会立即执行。

pdwThreadID 参数

创建一个新线程后,系统会返回给用户一个新线程的 ID,如果你不关心这个线程 ID,可以为这个参数传递一个 NULL 值。

终止一个线程

有四种方式终止一个线程:

一、线程函数返回

这是最正常的终止线程的方式。

当一个线程函数返回时,1. 所有由线程创建的 C++ 对象会通过它们的析构函数释放;2. 操作系统会释放由线程栈使用的内存;3. 系统设置线程的退出码为线程的返回值;4. 系统减少线程内核对象的使用计数。

二、线程通过 ExitThread 终止自身

通过调用

VOID ExitThread(DWORD dwExitCode);

函数来强制终止线程,系统会清理所有线程使用的系统资源,可是 C/C++ 资源不会被释放。所以最好使用返回语句终止线程。

三、使用 TerminateThread 终止线程

任意进程的任意线程,包括线程自身都可以使用这个函数

BOOL TerminateThread(
        HANDLE  hThread,
        DWORD   dwExitCode);

来终止 hThread 标识的线程,任何设计良好的应用程序永也不要使用这个函数终止一个线程。

这个函数是异步执行(类似于 TerminateProcess),也就是说,调用这个函数返回后,线程可能并没有立即终止,可以使用 WaitForSingleobject 来等待线程终止。

注意:使用 ExitThread 会释放线程栈,但 TerminateThread 不会释放线程栈,它会一直保持到拥有线程的进程终止,这样如果执行中的线程引用了终止线程栈的内存,不会引起问题。另外,当一个线程终止时,动态链接库(DLL)通常会收到一个通知,可是使用 TerminateThread 强制终止线程时,DLL 不会收到任何通知。

四、进程终止

当调用 ExitProcess 或 TerminateProess 后,进程中的所有线程都会终止,这个过程就如在每个线程中都调用 TerminateThread 一样,所以,所有相应的清理操作都没有执行,同样,如果进程的主线程退出,C/C++ 运行时启动函数会显式地调用 ExitProcess,所以所有未终止的线程会强制被终止,通常情况下,主线程返回之前,应等待其它线程正常返回。

当一个线程终止后,会发生以下一系列动作:

1. 所有由线程拥有的用户对象被释放,在 Windows 中,大部分的对象由进程拥有。线程拥有两种用户对象,窗口对象和钩子对象,当一个线程终止时,系统会自动地释放窗口对象,而不会释放任何钩子对象,其它对象在进程终止时才被释放;

2. 线程的退出代码由 STILL_ACTIVE 转变为退出码;

3. 线程的内核对象成为有信号;

4. 如果线程是进程中的最后一个活动线程,进程会终止;

5. 线程的内核对象引用计数减少 1。

一旦一个线程不再运行,没有任何其它线程可以使用线程的句柄,但可以使用 GetExitCodeThread(GetExitCodeProcess 检测一个进程的退出码)传递一个线程句柄检测线程是否已终止,函数原型如下:

BOOL GetExitCodeThread(
        HANDLE  hThread,
        PDWORD  pdwExitCode);

如果检测的线程没有终止,它返回 FALSE,返回的退出码为 STILL_ACTIVE。

深入了解线程

下图显示系统创建和初始化一个线程必须做的工作:

用户调用 CreateThread 函数,系统会创建一个线程内核对象,这个线程内核对象的使用计数为 2,分别表示线程自身引用和返回给创建函数的句柄的引用计数;暂停计数(Suspension count)设置为 1,线程初始化完成后,系统检测 CREATE_SUSPEND 标志是否传递给了创建函数,如果没有,则这个计数减少为 0,线程开始执行;线程的退出码会初始化为 STILL_ACTIVE;线程内核对象设置为无信号状态。

当线程内核对象创建时,系统也会为进程分配一块内存作为它的线程栈,并分别将你提供的线程入口函数的参数和函数本身的地址入栈。

每一个线程都有一个相应的上下文数据结构 CONTEXT,实际上是一组表示线程最新执行状态的 CPU 寄存器,系统创建线程时,指令寄存器(IP)指向一个 RtlUserThreadStart 函数,这个函数由 NTDLL.DLL 导出,CONTEXT 结构的堆栈指针指向线程栈中线程入口函数地址。当线程初始化完成后,系统会读取最后保存到线程上下文中实际的 CPU 寄存器的值,线程然后开始执行代码。

RtlUserThreadStart 函数原型如下:

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
        __try {
                ExitThread((pfnStartAddr)(pvParam));
        }
        __except(UnhandledExceptionFilter(GetExceptionInformation()) {
                ExitProcess(GetExceptionCode());
        }
}
因为新线程的指令指针指向这个函数,所以实际上 RtlUserThreadStart 这个函数才是真正的入口函数,这个函数带有两个参数,乍看起来,这个函数是由其它函数调用来执行,实际上,没有一个函数调用它,它能够正常运行是因为系统将指令指针指向了它,系统把它的两个参数事先入栈,某些 CPU 架构使用寄存器传递参数,系统也会对寄存器进行相应地设置保证参数能够正确地传递给它。

由于这个函数内部使用了 ExitThread 或 ExitProcess 函数,线程在内部已经终止,所以它永远也不会返回。

当一个进程的主线程初始化时,它的指令寄存器也会指向这个函数,这个函数内部调用 C/C++ 运行时库的启动函数,因为启动函数内部调用了 ExitProcess,所以启动函数永远也不会返回到 RtlUserThreadStart,但从系统执行的第一条指令来说,RtlUserThreadStart 函数才是真正地入口函数。

C/C++ 运行时库

早期的标准 C 运行时库没有为多线程的应用程序做太多的考虑,使用了大量的全局变量和静态变量,这样,在设计多线程程序时会有太多的负面影响。为了使多线程的 C/C++ 应用程序能更好地工作,必须使用与线程有关的数据结构,每一个线程只从自己的线程栈中查找使用数据,不对其他线程的运行产生影响。系统并不会自动为每一个新线程分配一个这样的数据块,创建数据块的责任由程序员自己完成,所以不要使用操作系统的 CreateThread 函数,C/C++ 运行时库为我们提供了一个合适的函数:

unsigned long _beginthreadex(
        viod    *security,
        unsigned        stack_size,
        unsigned        (*start_address)(viod *),
        void    *arglist,
        unsigned        initflg,
        unsigned        *thrdaddr);

这个函数内部会为我们创建一个与线程相关的数据结构。

_beginthreadex 函数内部使用一个 _tiddata 结构来存储对应于 C/C++ 运行时库中使用的全局变量和静态变量,通过调用 TlsSetValue 使用本地线程存储方法使这个结构与新线程关联。如果在运行过程中需要中止线程,必须调用 _endthreadex,它可以保证释放为 _tiddata 分配的内存。

如果使用 CreateThread 来创建一个线程,线程中如果使用了 C/C++ 运行时函数,这些函数需要访问 _tiddata 结构,这些函数会先使用 TlsGetValue 取得线程的数据块,如果 _tiddata 数据返回了 NULL,那么函数会创建并初始化一个 _tiddata 数据块,并将它与当前线程关联,这样,其它C/C++ 运行时函数需要访问 _tiddata 结构时,它就会能够获得这个数据块。可是如果线程发生了错误或使用 ExitThread 终止了线程,为 _tiddata 分配的内存不会得到释放,因为没有人会使用 _endthreadex 来终止 CreateThread 创建的线程。所以强烈建议使用 _beginthreadex 创建线程,使用 _endthreadex 终止线程。

C/C++ 运行时有两个旧的与线程有关的函数:

unsigned long _beginthread(
        void    (__cdecl *start_address)(*void),
        unsigned stack_size,
        void    *arglist);

void _endthread(void);

与 _beginthreadex 和 _endthreadex 一样,这两个函数分别创建和终止线程的函数,不要使用这两个函数,_beginthread 函数使用了三个参数创建一个线程,也就是说,它使用默认的安全性,不能将新线程设置为暂停运行,也不能获得新线程的 ID;而 _endthread 也在内部调用 ExitThread 函数来终止线程,但它总是设置线程的返回值为 0,更糟糕的是,_endthread 函数内部会调用 CloseHandle 关闭线程句柄,导致父线程在某些情况下使用线程句柄时会出错。所以尽量不要使用这两个函数。

获取自身的标识

线程可以使用 GetCurrentProcess 和 GetCurrentThread 来获取当前进程和当前线程的句柄,这个句柄值是一个伪句柄,可以使用这个句柄来完成当前进程或线程的操作。这两个函数不会在进程句柄表中创建新的句柄条目,也不会增加内核使用计数,它的值作为 CloseHandle 参数时,这个函数会直接返回 FALSE,GetLastError 返回 ERROR_INVALID_HANDLE。

还可以使用 GetCurrentProcessId 和 GetCurrentThreadId 来获取当前进程和当前线程的 ID,不过一般来说,这个 ID 对我们用处不大。

有时需要使用实际的句柄而不是伪句柄,例如将父线程句柄传递到子线程中,必须使用一种方法将伪句柄转换为真实的句柄,使用 DuplicateHandle 可以完成这个任务:

BOOL DuplicateHandle(
        HANDLE  hSourceProcess,
        HANDLE  hSoucce,
        HANDLE  hTargetProcess,
        PHANDLE phTarget,
        DWORD   dwDesiredAccess,
        BOOL    bInheritHandle,
        DWORD   dwOptions);

这个函数会增加内核对象的使用计数,所以使用完成后必须使用 CloseHandle 关闭句柄减少使用计数。

相关阅读 更多 +
排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载