目录
一、线程
1.1 线程的基本概念
1.2 何时创建线程
二、线程控制
三、遍历线程
四、线程内核对象
4.1 线程上下文
4.2 暂停次数
4.3 信号
五、线程调度
5.1 什么是线程优先级
5.2 进程优先级与相对线程优先级
5.3 编程改变优先级
5.4 动态优先级的概念
一、线程
1.1 线程的基本概念
线程是进程中的一个独立控制流,它是程序运行时的调度单位,也是执行代码的基本单元。从 CPU 的角度来看,所有线程都是平等的,不会因为属于某个进程而受到特殊对待。CPU 根据线程的优先级来决定执行哪个线程,并分配相应的时间片。当一个线程的时间片用完时,CPU 会切换到下一个线程继续执行。
通常情况下,线程执行的代码属于某个进程,同一个进程中的所有线程共享进程的资源,包括虚拟内存空间、内核对象句柄表等。
此外,还有一种内核线程,它的代码完全在内核空间中运行,不在我们当前的讨论范围内。每个线程都是一个独立的执行单元。
1.2 何时创建线程
通常在以下情况下需要创建线程:
- 在输入的同时需要输出(例如在显示界面的同时处理数据);
- 逻辑A的执行依赖于逻辑B的实时反馈(比如进度条的更新);
- 利用计算机的空闲时间处理一些繁重但不紧急的任务;
- 涉及大量磁盘操作的程序;
- 与网络操作相关的程序;
- 需要处理多个长时间等待的业务逻辑;
- 需要进行密集计算,并充分利用多核CPU性能的程序;
- 需要让其他进程执行自己的代码时(例如远程注入代码);
简单来说,当某些操作可能会阻塞当前线程,而当前线程又非常重要不能被阻塞时,就需要创建新的线程。
二、线程控制
相关的线程控制函数如下:
- `CreateProcess`:创建进程。
- `OpenProcess`:打开进程。
- `ExitProcess`:退出本进程。
- `TerminateProcess`:结束其他进程。
- `SuspendThread`:暂停线程(挂起)。
- `ResumeThread`:恢复线程。
创建线程示例代码:
#include "stdafx.h"
#include <windows.h>
int g_nNum = 0;DWORD WINAPI ThreadProc(LPVOID lParam) {printf("我是线程!\n");for (int i=0;i<1000;i++){g_nNum++;}printf("%d\n", g_nNum);return 0;
}int main()
{CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);system("pause");printf("%d\n", g_nNum);return 0;
}
三、遍历线程
线程的遍历同样可以使用创建快照的方式,以下是遍历某一个进程线程的示例代码:
VOID ListProcessThreads(DWORD dwPID) {HANDLE hThreadSnap = INVALID_HANDLE_VALUE;THREADENTRY32 te32;//创建快照,当前操作系统的线程快照hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);if (hThreadSnap == INVALID_HANDLE_VALUE)return;//设置输入参数,结构的大小te32.dwSize = sizeof(THREADENTRY32);//开始获取信息if (!Thread32First(hThreadSnap, &te32)) {CloseHandle(hThreadSnap); //关闭句柄return;}do {if (te32.th32OwnerProcessID == dwPID) {//显示相关信息printf("\n THREAD ID=0x%08X", te32.th32ThreadID);printf("\t base priority =%d", te32.tpBasePri);printf("\t delta priority =%d", te32.tpDeltaPri);}} while (Thread32Next(hThreadSnap, &te32));CloseHandle(hThreadSnap);
}
这段代码的功能是列出指定进程id(通过 dwPID
参数标识)的所有线程信息。它使用了 Windows API 中的 Toolhelp32
系列函数来遍历系统中的线程,并输出每个线程的 ID 和优先级信息。
四、线程内核对象
线程的创建伴随着在内核中创建一个线程内核对象的过程,在线程内核对象的数据结构中包含了一些重要信息。
4.1 线程上下文
对于CPU来说,系统里有大量线程等着被执行。CPU会依据线程的优先级,来决定先执行哪个线程,并且在执行前给它分配一定的时间片。当正在执行的线程把时间片用完了,CPU就会切换去执行其他线程,就这样循环往复,让操作系统里的线程都有机会依次执行。而在这个切换线程的过程中,就会涉及到线程环境的保存和加载操作。
具体来讲,当一个线程暂时不使用CPU了,它得把自己当前的执行环境保存起来,这样下次再轮到它执行的时候,就能接着之前的状态继续。相反,当一个线程即将要被CPU执行时,得先把它对应的执行环境加载到CPU里。
在线程的内核对象中,存在一个结构体专门用来存储当前线程的执行环境。在用户层面,我们可以借助特定的API来获取和设置线程环境 。
获取线程环境的函数:
BOOL WINAPI GetThreadContext(_In_ HANDLE hThread,_Inout_ LPCONTEXT lpContext
);
设置线程环境的函数:
BOOL WINAPI SetThreadContext(_In_ HANDLE hThread,_In_ CONST CONTEXT* lpContext
);
`CONTEXT`结构体定义如下:
typedef struct _CONTEXT {DWORD ContextFlags;DWORD Dr0;DWORD Drl;DWORD Dr2;DWORD Dr3;DWORD Dr6;DWORD Dr7;FLOATING_SAVE_AREA FloatSave;DWORD SegGs;DWORD SegFs;DWORD SegEs;DWORD SegDs;DWORD Edi;DWORD Esi;DWORD Ebx;DWORD Edx;DWORDDWORDDWORDDWORDDWORDDWORDDWORDDWORDBYTE }CONTEXT;Ecx;Eax;Ebp;Eip;SegCs; EFlags;Esp;SegSs;ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
可以看出,线程环境基本就是CPU在执行此线程时的寄存器信息。
4.2 暂停次数
一个线程能够被暂停,且能被暂停多次。一个线程被暂停的次数在线程的内核对象中有记录,每暂停一次线程,暂停次数自增,每恢复一次线程,暂停次数自减,只有暂停次数为0的时候,线程才会运行起来。
4.3 信号
在创建和等待线程时会涉及信号的概念。例如:
//创建一个线程
HANDLE hThread = INVALID_HANDLE_VALUE;
CreateThread(NULL, NULL, Proc, NULL, NULL, NULL);//等待线程结束
WaitForSingleObject(hThread, -1);
//关闭句柄
CloseHandle(hThread);
信号其实就是线程自身的一种状态。有个`WaitForSingleObject`函数,它专门用来检查这个信号状态。要是信号状态是`FALSE` ,也就是没有信号,处于非激发态,那`WaitForSingleObject`函数就会一直等着。而一旦信号状态变成`TRUE`,也就是有信号,处于激发态了,这个函数就会马上返回。就拿线程来说,当线程运行结束的时候,它的信号状态就会从没有信号的状态转变成有信号的状态。这时候,`WaitForSingleObject`函数就能等到线程结束,然后返回,程序也就可以接着去执行后续的代码了。
不光线程是可等待的,还有很多内核对象也是可以等待的,如下表:
五、线程调度
5.1 什么是线程优先级
(1)每个线程都有一个优先级数值,范围从0(最低)到31(最高)。
(2)当一个线程把分配给它的时间片用完了,系统就得决定接下来给哪个线程分配时间片。这时,系统会从优先级最高的线程开始,依次往低优先级查看,优先让高优先级的线程得到执行机会。
(3)要是有个低优先级的线程正在运行,可恰好有高优先级的线程已经准备好要执行了,那系统就会马上中断低优先级线程,转而给高优先级线程分配时间片,让高优先级线程运行起来。比如说,只要系统里存在优先级为31的线程,那系统就不会去执行那些优先级在0到30之间的线程。
(4)系统刚启动的时候,会有一个特殊的线程,它的优先级是0 ,这个线程专门负责把系统内存里所有闲置的页面都清零,而且整个系统里只有这一个优先级为0的线程 。
5.2 进程优先级与相对线程优先级
在Windows系统里,我们没办法直接去调整线程的优先级。Windows采用了一种间接的办法,它依据进程优先级以及相对线程优先级,让操作系统自己来算出线程优先级具体是多少。
进程优先级有这么几种类型,分别是`real-time`(实时)、`high`(高)、`above normal`(高于标准)、`normal`(标准)、`below normal`(低于标准)、`idle`(最低) 。
进程优先级就像是给这个进程里所有线程的优先级定了个大方向。之后,再结合相对线程优先级,就能确定线程真正的优先级数值。
这里要特别注意,线程优先级是相对进程优先级而言的。要是你更改了进程优先级,虽然线程的相对优先级不会变,但是它实际的优先级数值却是会跟着改变的 。
5.3 编程改变优先级
1. 当我们使用`CreateProcess`函数创建进程时,能在它的`fdwCreate`参数里设置优先级。
2. 要是想改变进程的优先级,还可以用`SetPriorityClass`这个函数来实现。
3. 要是想知道某个进程的优先级是多少,就得调用`GetPriorityClass`函数。
4. 注意,`CreateThread`函数没有设置线程相对优先级的参数,不能通过它来设置线程相对优先级。
5. 要设置线程的优先级,得使用`SetThreadPriority`函数。
6. 要是想获取线程的相对优先级,就得调用`GetThreadPriority`函数。
5.4 动态优先级的概念
1. 由进程优先级和线程优先级算出来的优先级,我们叫它基本优先级。有时候,系统会自动提高某个线程的优先级,这么做主要有两个原因:
(1)为了能快点响应一些紧急操作。
(2)给那些一直没机会执行、处于“饥饿”状态的线程分配时间片。
2. 这里有几点要注意:
(1)优先级被提高的线程,在得到时间片去执行后,它的优先级数值会自动减小,一直减到基本优先级就不再减了。
(2)如果一个“饥饿”线程有2到3秒都没执行,它的优先级会临时变为15,等它执行完两个时间片后,优先级又会变回原来的数值。
(3)系统自动提升优先级的范围是1到15,优先级在16到31之间的线程,系统不会去提升它们的优先级。
(4)我们能阻止系统提升优先级:
- 使用`SetProcessPriorityBoost()`函数,能禁止提升当前进程里线程的优先级。
- 使用`SetThreadPriorityBoost()`函数,能禁止提升某个线程的优先级。
- 要是想知道进程或线程有没有使用优先级提升功能,可以用`GetProcessPriorityBoost`和`GetThreadPriorityBoost`这两个函数来判断 。