线程基础及要点

来源:互联网 发布:生意参谋有哪些数据 编辑:程序博客网 时间:2024/05/20 12:48

 

线程的组成如下图所示:

 

可见,线程由两部分组成:

1、内核对象

操作系统用它来管理线程。还用内核对象来存放线程统计信息。

2、线程栈

用于维护线程执行时所需要的所有函数参数和局部变量。

 

每个线程都必须有一个入口点函数,这是线程的起点,形式如下:
DWORD WINAPI ThreadFunc(PVOID pvParam)

{

       DWORD dwResult=0;

       .....

       return (dwResult);

}

1、线程函数的名称,可以任意命名,不一定要是ThreadFunc。如果应用程序中有多个线程,则需为他们指定不同名称的线程函数。

2、线程函数的参数,可以传递任意类型的数据的指针。只需在传递时强制转换为PVOID,在线程中调用时,再强制转换回来即可。

3、线程函数必须返回一个值,如上例中的dwResult,这将成为线程的退出代码。

4、线程函数中,应尽可能使用局部变量和函数参数(在线程栈上创建)。如果使用全局变量或静态变量,则其他线程也有可能破坏该变量中的值。

 

线程创建

如需创建线程,则可以在一个正在运行的线程中调用CreateThread.其原型为:

HANDLE WINAPI CreateThread(
  __in          LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全性
  __in          SIZE_T dwStackSize,//线程栈大小上限
  __in          LPTHREAD_START_ROUTINE lpStartAddress,//线程函数入口地址
  __in          LPVOID lpParameter,//传入线程函数的参数
  __in          DWORD dwCreationFlags,//可设定线程创建完毕后,立即运行或挂起
  __out         LPDWORD lpThreadId//如需返回线程ID,则可在此传入一个DWORD指针。否则,传入NULL
);

调用CreateThread时,系统会创建一个线程内核对象。并从进程地址空间中分配内存给线程栈。

因为新线程与负责创建的那个线程在同一个上下文中运行,则新线程可以访问进程内核对象的所有句柄、进程中的所有内存、同一进程中其他线程的栈。这样一来,线程间通信就非常容易。

 

终止线程

有四种方式

1、线程函数返回

被推荐的方式。因为这种自然的返回,可以让应用程序的清理工作都得以执行。

2、线程自己调用ExitThread

可强制线程终止运行,并导致系统清理该线程的所有资源。但是线程中的c/c++资源(如c++类对象)不会被销毁。所有更好的做法是让线程自然返回。

3、线程调用TerminateThread终止其他线程

不被推荐的方式。TerminateThread可以杀死任何线程。因为被杀死的线程不会收到被“杀死”的通知。所以线程无法正确清理,而且不能阻止自己被终止运行。

使用TerminateThread后,被杀死的线程不会销毁这个线程的堆栈。

此外,DLL通常会在线程终止运行时收到通知并处理(如果此DLL有DllMain函数的话),如果使用TerminateThread结束线程,则DLL不会收到通知。其结果就是不能执行正常的清理工作。

4、包含线程的进程终止

如调用ExitProcess或TerminateProcess,则进程中所有的线程都会退出。当然,这种效果相当于对所有线程调用TerminateThread。

 

线程终于运行时,会发生下面的事

1、线程退出代码,由STILL_ACTIVE变成传给ExitThread或TerminateThread的代码(线程函数的返回值)

2、线程内核对象的状态,变为触发状态。图中的“已通知(Signaled)=TRUE”

3、如果线程是进程中的最后一个活动线程,则系统认为进程也终止了

4、线程内核对象的使用计数递减1

 

线程内幕

结合本文开始的图示。

对CreateThread函数的调用,导致了系统创建一个线程内核对象。

1、最初使用计数(Usage count)为2

注意:如果要销毁线程内核对象,就必须线程终止,计数减1;并且用closeHandle关闭线程句柄,计数减1变为0。

2、暂停计数(Suspend count)被设置为1

3、退出代码(Exit code)设置为STILL_ACTIVE(0x103)

4、设置为未触发状态(signaled=FALSE)

 

内核对象创建完毕后,则系统就为线程栈分配内存。首先被压入栈的是线程函数的参数 pvParam的值,之后便压入线程函数的地址(CreateThread参数中的lpStartAddress)。

 

每一个线程内核对象,都有一个上下文结构(context),上下文结构保存了线程上一次执行时CPU寄存器状态(以便windows下次调度此线程时,将上下文载入CPU寄存器继续执行)。其中最重要的两个寄存器便是IP(指令指针寄存器)和SP(栈指针寄存器)。

内核初始化时,SP设置为lpStartAddress在线程栈中的地址。IP被设置为RtlUserThreadStart函数的地址,此函数是从NTDLL.dll中导出的。参见本文开始的图例。

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
      __try {
            ExitThread((pfnStartAddr)(pvParam));
      }

      __except(UnhandledExceptionFilter(GetExceptionInformation())) {
            ExitProcess(GetExceptionCode());
      }
      // 注意: 我们永远不会到达这里.
    }

因为新线程的指令指针寄存器被设置为RtlUserThreadStart的地址,所以,这里才是线程的真正起点。当RtlUserThreadStart调用线程函数时,它会将线程函数地址压入堆栈,当处理完成后,则可调用 ExitThread 或 ExitProcess来终止线程或整个进程。由此可见,线程永远不可能退出RtlUserThreadStart。

原创粉丝点击