Windows线程简介

1.简介

线程由以下两部分构成。

  • 一个线程的内核对象,操作系统用它来管理线程。系统还用内核对象来存放线程统计信息的地方。
  • 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。

进程是惰性的。从来不执行任何东西,它只是一个线程的容器。线程必然是某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。这意味着线程要在其进程的地址空间内执行代码和处理数据。假如一个进程上下文中有两个以上的线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,可以处理相同的数据。这些线程还共享内核对象句柄,因为句柄表示针对每一个进程的,而不是针对线程。

2.创建线程

调用CreateThread时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。可以把线程内核对象想象为一个由线程统计信息构成的小型数据结构。这与进程和进程内核对象之间的关系是相同的。

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);
  • lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,该结构确定返回的句柄是否可以被子进程继承。
  • dwStackSize:堆栈的初始大小,以字节为单位。如果此参数为零,则新线程使用默认大小。
  • lpStartAddress:指向线程要执行的应用程序定义函数的指针。
  • lpParameter:指向要传递给线程的变量的指针。
  • dwCreationFlags:控制线程创建的标志。
  • lpThreadId:指向接收线程标识符的变量的指针。如果该参数为NULL,则不返回线程标识符。

示例:在线程中做加法。

    DWORD WINAPI threadFun(PVOID pvParm)
    {
    
    
        int i = 0;
        int x = *((int*)pvParm);
        i += x;
        printf("i = %d\n", i);
 
        return 0;
    }
 
    DWORD threadID;
    int x = 2;
    HANDLE hThread = CreateThread(NULL, 0, threadFun, (PVOID)&x, 0, &threadID);

3.终止运行线程

有以下4中方式终止:

  • 线程函数返回(强烈推荐使用)
  • 线程调用ExitThread函数杀死自己(不推荐使用)
  • 另一个进程中的线程调用TerminateThread函数(不推荐使用)
  • 包含线程的进程终止运行(不推荐使用)

3.1线程函数返回

让线程函数返回,可以确保以下正确的应用程序清理工作都得以执行。

  • 线程函数中创建的所有C++对象都通过其析构函数被正确销毁
  • 操作系统正确释放线程栈使用的内存
  • 操作系统把线程的退出代码设为线程函数的返回值。
  • 系统递减少线程的内核对象的使用计数

3.2ExitThread函数

为了强迫线程终止运行,可以调用ExitThread。

该函数终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源。但是C/C++资源不会被销毁。和ExitProcess函数一样。

3.3TerminateThread函数

不同于ExitThread总是杀死主调线程,TerminateThread能杀死任何线程。

3.4包含线程的进程终止运行

ExitProcess和TerminateProcess可用于终止线程的运行。这些函数会使终止运行的进程中的所有线程全部终止。同时,由于整个进程都会关闭,所以它使用的所有资源肯定会被清理。包括线程的堆栈。但是C++对象析构函数不会被调用等。

4._beginthreadex

如果你正在编写C/C++代码,决不应该调用CreateThread。相反应该使用_beginthreadex,退出也应该使用_endthreadex。

原因:首先要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况。

比如标准C运行库的全局变量errno,很多运行库中的函数在出错时会将错误代号赋值给这个全局变量。

if (system("notepad.exe readme.txt") == -1)
{
    
    
	switch(errno)
	{
    
    
		...//错误处理代码
	}
}

假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。

为了解决这个问题,Windows操作系统提供了这样的一种解决方案——创建了一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联。然后,在调用C/C++运行库函数时,那些函数必须知道去查找主调线程的数据块,从而避免影响到其他线程。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。

_beginthreadex会在内部调用CreateThread。

5.线程的挂起和恢复

在线程内核对象中有一个值表示线程的挂起计数。调用CreateThread时,系统将创建线程内核对象,并把挂起计数初始化为1.这样,就不会给这个线程调度CPU了。

CreateThread函数调用时,查看是否有CREATE_SUSPENDED标志传入。如果有,函数会返回并让新的线程处于挂起状态。如果没有,函数会将线程的挂起计数递减为0。当线程的挂起计数为0时,线程就称为可调度的了,除非它还在等待某个事件的发生(例如键盘输入)。

调用ResumeThread恢复。

ResumeThread成功,返回线程的前一个挂起计数;否则,它将返回0xFFFFFFFF。

一个线程可以被多次挂起,如果一个线程被挂起三次,则在它有资格让系统为它分配CPU之前必须恢复三次。除了在创建线程时使用CREATE_SUSPENDED标志,还可以使用SuspendThread来挂起线程。

6.线程睡眠

线程还可以告诉系统,在一段时间内自己不需要调度了,可以调用Sleep实现。

void Sleep(
  [in] DWORD dwMilliseconds
);

这个函数将线程自己挂起dwMilliseconds长的时间,以毫秒为单位。

  • 值为0会导致线程将剩余的时间片让出给任何其他准备运行的线程。如果没有其他线程可以运行,函数将立即返回,线程继续执行。
  • INFINITE值,表示告诉系统,永远不要调度这个线程。

1.简介

线程由以下两部分构成。

  • 一个线程的内核对象,操作系统用它来管理线程。系统还用内核对象来存放线程统计信息的地方。
  • 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。

进程是惰性的。从来不执行任何东西,它只是一个线程的容器。线程必然是某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。这意味着线程要在其进程的地址空间内执行代码和处理数据。假如一个进程上下文中有两个以上的线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,可以处理相同的数据。这些线程还共享内核对象句柄,因为句柄表示针对每一个进程的,而不是针对线程。

2.创建线程

调用CreateThread时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。可以把线程内核对象想象为一个由线程统计信息构成的小型数据结构。这与进程和进程内核对象之间的关系是相同的。

猜你喜欢

转载自blog.csdn.net/yohnyang/article/details/131264597
今日推荐