【笔记整理 - Windows编程】

资料来源:《Windows程序设计》

2、WIN32程序的运行原理

基础知识

占有CPU时间片执行指令的是线程,线程是进程内代码的执行单元。

每个进程都有自己的私有地址空间,线程运行时只能访问属于它的进程的内存。

例如:不同进程内的线程都可以访问0x12345678地址,但实际上访问的物理地址是不同的。

32位weindows系统可寻址4GB(232)的地址空间,即一个指针能有 4,294,967,296 个不同的取值。

补充:win10的32位仍是4GB,但64位家庭版达到了128 GB,其它版本是2TB或6TB。

虚拟内存

通过交换技术将部分磁盘作为内存使用,使windows能为所有进程分配4GB的地址空间。

windows的虚拟内存机制基于32位的线性地址空间,带大多数系统上windows将4GB空间的前半部分(0x00000000——0x7ffffffff)留给进程作为私有存储,后一半(0x80000000——0xfffffffff)用来存储系统内部使用的数据。

为每个进程都分配4GB大小的虚拟内存,然后又都留出一半作为系统空间?什么意思?

进程的私有空间只能由该进程使用;系统空间存放OS的代码,包括内核、驱动设备、设备I/O缓冲区……,被所有进程共享,不能被用户线程访问。

操作系统分内核模式和用户模式。

虚拟内存中每一页的页属性都有访问模式标记,标识了哪一模式下的代码才有权限访问。

系统地址空间的页仅能从内核模式访问,用户程序执行系统调用时,会切换到内核模式。

内核对象

内核对象是系统提供的用户模式下代码与内核模式下代码进行交互的基本接口,是应用程序与系统内核交互的重要方式之一。

本质:一个内核对象是一块内核分配的内存,它只能被运行在内核模式下的代码访问。

内核对象中的数据在整个系统独一份,所以也被称为系统资源。

用户程序只能通过windows API函数间接地操作内核对象。


对象句柄

内核对象的数据结构只能从内核模式访问,所以应用程序(用户代码)无法直接在内存中定位这些数据结构,只能使用API函数。

调用函数创建内核对象时,函数会返回标识此内核对象的句柄。可以将其视为进程内任何线程都能使用的不透明的值。API函数通过这个句柄定位到内核对象。

句柄是进程相关的,一个进程的句柄无法让其它进程使用,除非该进程主动调用API函数将句柄分享出去。

使用计数

内核对象的一个属性。指明进程对特定内核对象的引用次数,当计数为0时,系统就会自动关闭资源。


进程的创建

进程和线程

进程是不活泼的,进程要完成任何事情,必须由一个运行在它的地址空间的线程来执行该进程地址空间的代码

所以说进程可以看做是一个容器。

进程内核对象

OS 使用此内核对象还管理该进程,存放该进程的统计信息。

私有的虚拟地址空间

包含了所有 可执行的/DLL模块 的代码和数据,它也是程序动态申请内存的地方。



应用程序的启动过程

应用程序的启动过程就是创建进程的过程。

**1、**调用CreateProcess函数创建一个内核对象用于管理该进程(PCB?);

**2、**为新创建的进程分配虚拟地址空间,加载应用程序运行所需的代码和数据;

**3、**系统为新进程创建一个主线程;

**4、**主线程调用C/C++运行期启动函数;

**5、**C/C++运行期启动函数调用main函数。

**C/C++运行期启动函数:**初始化C/C++运行库,保证用户代码执行之前所有全局、静态的C++对象能被正确地创建。

父进程:创建的进程;子进程:被创建的进程。

系统在创建新的进程时,会为新进程指定一个 STARTUPINFO 类型的变量,该变量记录了传递给新创建进程的信息。

在微软的文档中查到的是 STARTUPINFOASTARTUPINFOWSTARTUPINFOA与书中 STARTUPINFO 一致,STARTUPINFOW 与2者的区别在于将 LPSTR 改为了 LPWSTR

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa

可以使用GetStartupInfo函数获取 STARTUPINFO

定义一个STARTUPINFO 对象后,在使用前必须初始化cb成员为STARTUPINFO 结构的大小

STARTUPINFO si = { sizeof(si) };
::GetStartupInfo(si);

随着Windows版本的改变,API函数支持的结构体成员可能会变化,为了兼容以前的版本,要通过cb成员的值来确认结构体的成员数目。



CreateProcess函数 ~

STARTINFO

CreateProcess()

PROCESS_INFORMATION

创建一个新进程和该进程的主线程。新进程在父进程的安全上下文中运行指定的可执行文件。

例子:打开一个记事本

int main()
{
	STARTUPINFO si = { sizeof(si) };
	PROCESS_INFORMATION pi;

// 注释部分的代码会导致运行错误
	//wchar_t* szapp = const_cast<wchar_t*>(L"notepad");
	
	wchar_t szapp[] = L"notepad";
	::CreateProcess(NULL, szapp, NULL, NULL, false, NULL, NULL, NULL, &si, &pi);
}
// &si, &pi 少了任一个都会无法打开记事本

部分参数解析

lpCommandLine:为新的进程指定了一个完整命令行。CreateProcess函数会检查字符串中的第一个单词,并假设该单词就是想要运行的可执行文件的名字。缺省后缀是.exe

修改一下参数:

wchar_t szapp[] = L"notepad ReadMe.txt";

会要求记事本打开父进程当前目录下的ReadMe.txt文件。

VS实践:目录位置是main.cpp文件所在目录。

函数会按以下路径去搜索可执行文件:

1、调用进程的可执行文件所在目录;
2、调用进程的当前目录;
3、Windows的系统目录(system32);
4、Windows目录;
5、PATH环境变量中列出的目录。

lpApplicationName:第一个参数。也可以用这个参数来指定要执行的文件名。

注意:提供的是绝对路径或相对路径,所以

1、必须要有.exe后缀;

2、如果提供的是相对路径,就会假设该文件在父进程当前目录中。

所以常常将这个参数设为NULL。

dwCreationFlags:影响进程如何创建。

lpStartupInfo:指向STARTINFO的指针。

lpProcessInformation:指向PROCESS_INFORMATION的指针。


创建一个新的进程会使系统创建一个进程内核对象和一个线程内核对象,在创建它们的时候,系统将每个对象的使用计数初始化为1。CreateProcess函数返回之前,会打开这2个对象的句柄,将句柄的值传给PROCESS_INFORMATION结构,这会导致2个对象的使用计数增加到2。

所以父进程中必须又一个线程调用CloseHandle关闭CreateProcess函数返回的2个内核对象的句柄。

进程的ID号和线程ID号使用同一个号码分配器,所以进程ID和线程ID不可能会重复。

ID号会回收重利用,所以当获取了一个进程/线程ID号并保存后,在使用前得确认ID是否还是原来的进程/线程。



创建进程的例子

int main()
{
	char szapp[] = "cmd";
	STARTUPINFOA si = { sizeof(si) };
	PROCESS_INFORMATION pi;

	// 指定wShowWindow成员有效
	si.dwFlags = STARTF_USESHOWWINDOW;
	// 显示新进程的主窗口
	si.wShowWindow = TRUE;

// CREATE_NEW_CONSOLE:为新进程创建一个新的控制台窗口
// #define CREATE_NEW_CONSOLE 0x00000010
// 如果设置为NULL,父子进程共享同一个控制台窗口
	BOOL bRet = ::CreateProcessA(NULL, szapp, NULL, NULL, false, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

	if (bRet)
	{
		::CloseHandle(pi.hProcess);
		::CloseHandle(pi.hThread);

		printf("新进程的进程ID号:%d\n", pi.dwProcessId);
		printf("新进程的主线程ID号:%d\n", pi.dwThreadId);
	}
}

si.dwFlags

Windows先通过dwFlags参数查看那个成员有效,再去取那个成员的值。

可以通过|同时指定多个成员。

si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USERPOSITION;

进程控制

获取进程 ~

CreateToolhelp32Snapshot()

PROCESSENTRY32

Process32First()

Process32Next()

#include<tlhelp32.h> // 声明快照函数需要

CreateToolhelp32Snapshot 函数可以列出当前正在运行的一系列进程。

CreateToolhelp32Snapshot 函数给当前系统内执行的进程拍快照,就是获得一个进程列表,列表中记录着进程的ID、进程对应的应用名称、该进程的父进程ID等数据。

然后使用Process32FirstProcess32Next函数遍历快照中记录的列表。

例子:获得一个快照的句柄,然后使用2个函数,通过该句柄将一个个快照信息放入PROCESSENTRY32 结构体中,从该结构体获取快照信息。

int main()
{
	// 创建并初始化结构体
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof(pe32);

	// 给系统内所有进程拍一个快照,得到句柄
	HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hProcessSnap == INVALID_HANDLE_VALUE)
	{
		printf("Failed \n");
		return -1;
	}

	// 遍历进程快照,轮流显示信息
	BOOL bMore = ::Process32First(hProcessSnap, &pe32);
	while (bMore)
	{// 该成员函数是wchar_t类型,如果使用“%s”则只输出一个字符
		printf("process name: %ls\n", pe32.szExeFile);
		printf("process ID: %u\n\n", pe32.th32ProcessID);

		bMore = ::Process32Next(hProcessSnap, &pe32);
	}
	
	// 清除快找对象
	::CloseHandle(hProcessSnap);

	return 0;

}


终止当前进程 ~

ExitProcess ()

结束程序的执行,从内存中卸载。

终止进程的原因:

  • 主线程的入口函数返回;
  • 进程中的一个线程调用了ExitProcess函数;
  • 进程中的所有线程都结束了;
  • 其他进程中的一个线程调用了TerminateProcess函数。

当用户程序的入口函数返回时,启动函数会调用C/C++运行期退出函数exit,将用户的返回值传递给它。

exit函数会销毁所有全局的、静态的C++对象,然后调用系统函数ExitProcess让OS终止该进程。

可以在程序的任何地方调用ExitProcess,强制进程立即结束。

但C/C++应用程序应该尽量避免直接调用这个函数,因为会使得C/C++运行期库得不到通知,使得全局对象、静态对象没能及时析构。



终止其它进程 ~

TerminateProcess()

OpenProcess()

GetExitCodeProcess()

GetLastError()

要终止其它进程,得使用TerminateProcess函数。

对一个进程进行操作前,先要获得该进程的句柄。只能使用OpenProcess来获得已存在进程的访问权限。

例子:dwId可以通过获取进程例子中的ToolHelp函数得到,或直接看任务管理器。

BOOL TerminateProcessFromId(DWORD dwId)
{
	BOOL bRet = FALSE;
	// 句柄必须有“PROCESS_TERMINATE”权限
	HANDLE hProcess = ::OpenProcess(PROCESS_TERMINATE, FALSE, dwId);
	if (hProcess != NULL)
	{
		// 终止进程,第二个参数是退出值
		bRet = ::TerminateProcess(hProcess, 0);
	}
	CloseHandle(hProcess);
	return bRet;
 }

TerminateProcess函数如果调用失败,可以通过GetLastError 获得函数的出错代码(uExitCode),以此判断错误信息。

可以通过GetExitCodeProcess 函数获取终止的进程的退出值,如果该函数仍在运行,GetExitCodeProcess 的返回值是STILL_ACTIVE

一个进程终止后,就会发生如下事件:

  • 所有这个进程创建或打开的对象句柄关闭;
  • 此进程内的所有线程都中止执行;
  • 进程内核对象变成收心状态,所有等待在此对象上的线程开始运行;
  • 系统将进程对象中的退出代码的值由STILL_ACTIVE改为程序指定的退出码。


保护进程

都需要用到HOOK知识,第九章才会讲到

指保护进程不被其它进程非法关闭。例如,网络游戏通常都会禁止WPE运行,它们在运行时定期检测系统进程,发现WPE进程存在就立即试图关闭。

保护进程不被关闭,有2种方法:

1、防止进程被其他进程检测到;

2、防止进程被终止。

检测进程通常使用ToolHelp函数或是Process Status函数

而终止进程通常都使用TerminateProcess函数

这2种方法都要用到HOOK方法,在这里略过。

实例:游戏内存修改器

原理 ~

ReadProcessMemory()

WriteProcessMemory()

GetVersionEx()

OSVERSIONINFO()

修改游戏进程所在的内存数据。

因为进程间的地址空间是相互隔离的,所以必须通过API函数的协助才能访问其它进程的内存。

ReadProcessMemory:读指定进程的内存;

WriteProcessMemory:写指定进程的内存。

应该在目标进程的整个用户地址空间进行搜索,在进程的整个4GB地址中,不同OS为应用程序预留的地址范围不同,所以在搜索前先要判断OS类型,以决定搜索范围。

windows提供GetVersionEx函数返回当前OS版本信息。

思路

通过值定位到内存地址。

目标进程中可能存在多个要搜索的值,在第一次搜索的时候,把搜索到的地址记录下来,然后让用户改变要搜索的值,再记录结果地址,直到最后只剩下一个地址为止。

代码:2个辅助函数和3个全局变量。

BOOL FindFirst(DWORD dwValue);	// 第一次查找
BOOL FindNext(DWORD dwValue);	// 后续查找

DWORD g_arList[1024];	// 地址列表,记录搜索结果
int g_ListCnt;			// 有效地址个数
HANDLE g_hProcess;		// 目标进程句柄

测试程序

int g_nNum;	// 全局测试变量
int main()
{
	int i = 198;	// 局部测试变量
	g_nNum = 1003;

	while (1)
	{
		printf("i=%d,addr=%08lX; g_nNum=%d,addr=%08lX\n", ++i, &i, --g_nNum, &g_nNum);
		getchar();
	}

	return 0;
}

搜索内存

windows使用了分页机制来管理内存,每页的大小是4kb。所以可以按页来搜索目标内存,提高搜索效率。

实现了第一次搜索(代码)
#include<iostream>
#include<windows.h>
#include<tlhelp32.h>
#include<versionhelpers.h>

using namespace std;

BOOL FindFirst(DWORD dwValue);	// 第一次查找
BOOL FindNext(DWORD dwValue);	// 后续查找

DWORD g_arList[1024];	// 地址列表,记录搜索结果
int g_ListCnt;			// 有效地址个数
HANDLE g_hProcess;		// 目标进程句柄


// 以页为单位,寻找与指定值相等的地址
// dwBaseAddr:一页的起始地址,dwValue:指定值
BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
	// typedef unsigned char  BYTE;
	BYTE arBytes[4096];
	
	// 通过全局变量“g_hProcess”获得目标进程句柄
	//  从“dwBaseAddr”开始,读取4096B(一页大小)的数据到“arBytes”位置
	if (!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
		return FALSE;

	// typedef unsigned long  DWORD;
	DWORD* pdw;
	for (int i = 0; i < (int)4 * 1024 - 3; ++i)
	{
		pdw = (DWORD*)&arBytes[i];
		if (pdw[0] == dwValue)
		{// 如果数量溢出,返回FALSE;否则记录匹配值所在地址
			if (g_ListCnt >= 1024)
				return FALSE;
			g_arList[g_ListCnt++] = dwBaseAddr + i;
		}
	}
	return TRUE;
}



BOOL FindFirst(DWORD dwValue)
{
	const DWORD dwOneGB = 1024 * 1024 * 1024;
	const DWORD dwOnePage = 4 * 1024;

	if (g_hProcess == NULL)
		return FALSE;

/* 根据OS版本,确定起始地址
* 原来按书中所说,需要判断OS版本,但实际编程的环境是win10,可以直接从0开始,不然也找不到目标值
*DWORD dwBase;
*if (IsWindows8OrGreater())
*	dwBase = 4 * 1024 * 1024;
*else
*	dwBase = 640 * 1024;
*/
	// 一页一页地查找数据
	for (DWORD dwBase = 0; dwBase < 2 * dwOneGB; dwBase += dwOnePage)
		CompareAPage(dwBase, dwValue);

	return TRUE;
}



void ShowList()
{
	for (int i = 0; i < g_ListCnt; ++i)
		printf("%08lX\n", g_arList[i]);
}



int main()
{
	wchar_t szFileName[] = L"D:\\VS\\Project_file\\Project2\\Debug\\Project2.exe";
	STARTUPINFO si = { sizeof(si) };
	PROCESS_INFORMATION pi;
	::CreateProcess(NULL, szFileName, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

	::CloseHandle(pi.hThread);
	g_hProcess = pi.hProcess;

	int iVal;
	printf("Input val = ");
	cin >> iVal;

	FindFirst(iVal);
	ShowList();

	::CloseHandle(g_hProcess);

	return 0;
}

VS实践:搜索数值时需要花点时间,但的确是有结果的。有一些书中未解答的问题,但自己已想通/未相同的问题:

BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
...
	BYTE arBytes[4096];
...
	for (int i = 0; i < (int)4 * 1024 - 3; ++i)
	...
		pdw = (DWORD*)&arBytes[i];
	...
...
}

BYTE 1B 大小,DWORD 4B 大小。for循环执行的动作是:以每个Byte位置为起点,检查4B大小的空间,如果所存储的值与目标值相等,就将其记录。

**已想通:**检查大小是4B,所以for循环的结束条件要-3。

**已想通:**内存中的数据不是都以4Byte为单位放置的,for循环++i是为了能正确定位到数据的起始位置。

所以,一个基本类型数据是不会被拆分为2部分,分别位于2页中?

**但又有新的疑问:**函数是假设FindFirstdwBase数据的值能精确定位到windows为用户程序预留空间的起始位置,然后以4Byte为单位调用CompareAPage函数。

所谓的“以页为单位”只是在逻辑上实现,实际上dwBase += dwOnePage是否正好匹配到一个页,取决于dwBase数据的值。

所以,要找的值的起始地址有可能在(int)4 * 1024 - 3被省略的3Byte中???

2GB大小共有512页,这意味着有1536个地址被忽略了。

之后的搜索(代码)

在前一次搜索的代码中搜索新的值

BOOL FindNext(DWORD dwValue)
{
	int nOrgCnt = g_nListCnt;	// 记录上一次查找的有效地址数
	g_nListCnt = 0;				// 初始化新的 g_nListCnt 值

	BOOL bRet = FALSE;	// 假设查找失败
	DWORD dwReadValue;
	for (int i = 0; i < nOrgCnt; ++i)
		// 从 g_hProcess 进程中的 g_arList[i] 位置,读取一个 DWORD 大小的数据,放到 &dwReadValue 位置
		if (::ReadProcessMemory(g_hProcess, (LPVOID)g_arList[i], &dwReadValue, sizeof(DWORD), NULL))
			if (dwReadValue == dwValue)
			{
				g_arList[g_nListCnt++] = g_arList[i];
				bRet = TRUE;
			}
	
	return bRet;
}

main中:

...
	while (g_nListCnt > 1)
	{
		printf("Input val = ");
		cin >> iVal;
		if (!FindNext(iVal))
			cout << "fail" << endl;
		ShowList();
	}
...
写内存(代码)
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue)
{
	return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}

main中:

...
	printf("New value = ");
	cin >> iVal;
	if (WriteMemory(g_arList[0], iVal))
		cout << "Write data success" << endl;
	else
		cout << "Write data failed" << endl;
...

提炼接口

#pragma once
#include<iostream>
#include<windows.h>
#include<tlhelp32.h>

class CMemFinder
{
protected:
	DWORD m_arList[1024];	// 地址列表,记录搜索结果
	int m_nListCnt;			// 有效地址个数
	HANDLE m_hProcess;		// 目标进程句柄
	BOOL m_bFirst;

	BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue);

public:
	CMemFinder(DWORD dwPrpcessId);
	CMemFinder(LPWSTR szFileName);
	virtual ~CMemFinder();

public:
// 属性
	BOOL IsFirst() const { return m_bFirst; }
	BOOL IsValid() const { return m_hProcess != NULL; }
	int GetListCount() const { return m_nListCnt; }
	DWORD operator[](int nIndex) { return m_arList[nIndex]; }
	void ShowList()
	{
		for (int i = 0; i < m_nListCnt; ++i)
			printf("%08lX\n", m_arList[i]);
	}

// 操作
	virtual BOOL FindFirst(DWORD dwValue);
	virtual BOOL FindNext(DWORD dwValue);
	virtual BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);

};

3、WIN32程序的执行单元

程序被装入内存后就成为了进程,进程在内存中是以线程为单位执行的。

多线程

每个进程都至少有一个线程,该线程从main开始执行,直到return语句返回,主线程结束,进程也就从内存中返回。

同一进程的不同线程可以共享进程的资源,如全局变量,句柄等。各个线程也可以有自己的私有栈堆。


线程的创建 ~

CreateThread()

WaitForSingleObject()

SECURITY_ATTRIBUTES

一般情况下,使用主线程接受用户输入,显示结果;创建新的线程(辅助线程)来处理长时间的操作。

每个线程都要有一个进入点函数,线程从这个进入点开始执行。主线程的进入点是main,辅助函数必须指定一个进入点函数,这个函数被称为线程函数,定义如下

DWORD WINAPI ThreadProc(_In_ LPVOID lpParameter);

WINAPI 是一个宏明,有如下声明:

#define WINAPI __stdcall

__stdcall:是新标准(现在已经不新了吧)C/C++函数的调用方法。Windows规定,凡是由它负责调用的函数都必须定义为__stdcall类型。ThreadProc是一个回调函数,由Windows系统负责调用,所以能定义为__stdcall类型。

这是windows的函数调用约定写法:https://docs.microsoft.com/zh-cn/cpp/cpp/calling-conventions?view=msvc-160

linux有自己的函数调用约定写法。

_In_:表明这个变量或参数是输入值,必须给这个变量填写好以后提交给某个函数去执行;

_out_:表明这个是输出值,可以传个地址给形参,函数内部会往这个地址写数据。


CreateThread函数

使用CreateThread函数创建新线程,函数执行成功后,将返回新建线程的线程句柄。

第3个参数lpStartAddress制定了线程函数的地址,新建的线程就从此地址开始执行,直到return语句返回,线程运行结束。

例子:主线程创建一个辅助线程,打印辅助线程的ID;辅助线程输出几行字符串,模拟真正的工作。

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
	int i = 0;
	while (i < 10)
		cout << "I am from a thread, count = " << i++ << endl;
	return 0;
}

int main()
{
	HANDLE hThread;
	DWORD dwThreadId;

// ThreadProc:设定ThreadProc函数作为线程入口;
// 0:立即执行;
// &dwThreadId:将生成的新线程ID返回给dwThreadId。
	hThread = ::CreateThread(NULL, NULL, ThreadProc, NULL, 0, &dwThreadId);

	cout << "Now another thread has been created. ID = " << dwThreadId << endl;

// 一直等待,直到hThread句柄标识的线程返回
	::WaitForSingleObject(hThread, INFINITE);
	::CloseHandle(hThread);

	return 0;
}

结果:

Now another thread has been created. ID = I am from a thread, count = 0
I am from a thread, count = 1
I am from a thread, count = 2
I am from a thread, count = 3
I am from a thread, count = 4
I am from a thread, count = 5
I am from a thread, count = 6
I am from a thread, count = 7
I am from a thread, count = 8
I am from a thread, count = 12676
9

WaitForSingleObject(hThread, INFINITE);:等待指定的对象(hHandle)变成受信状态,第二个参数是以毫秒为单位的等待时间,INFINITE表示无限等待。

当等待的对象变为受信状态,或指定的时间到期时,WaitForSingleObject函数返回。

部分参数

dwCreationFlags:创建标志,如果是0,则线程被创建后立即执行;如果指定的是CREATE_SUSPENDED,表示线程被创建后处于挂起状态,指导使用ResumeThread函数显示启动线程。

lpThreadAttributes:是一个指向SECURITY_ATTRIBUTES 结构的指针,如果是NULL,则表示使用默认的安全属性。

如果需要此线程句柄能被子进程继承,需要设定SECURITY_ATTRIBUTESbInheritHandle成员为TRUE:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;

HANDLE h = ::CreateThread(&sa,...);


线程内核对象(TCB?) ~

OpenThread()

ResumeThread()

SuspendThread()

GetExitCodeThread()

线程内核对象就是包含了线程状态信息的数据结构。每一次对CreateThread的调用,系统都会在内部为新的线程分配一个内核对象。

系统提供的线程管理函数也是用过访问线程内核对象来实现管理的。


一些线程内核对象结构的参数

1、CONTEXT

每个线程都有它自己的一组CPU寄存区,称为线程的上下文。这组寄存器的值保存在CONTEXT结构体中,记录了该线程上次运行时CPU寄存器的状态。


2、Usage Count

记录了线程内核对象的使用计数。当值为0的时候,系统就认为没有任何进程在使用此内核对象,线程内核对象就要从内存中撤离。

在创建一个新线程的时候,CreateThread函数会返回线程内核对象的句柄,相当于打开一次刚创建的内核对象,使得新线程的Usage Count的初值为2。

每当有一个进程调用OpenThread函数打开线程内核对象后,Usage Count的值+1。所以在使用完返回的句柄后,一定要调用CloseHandle关闭句柄,使Usage Count的值-1。

线程函数返回后,线程生命周期停止,Usage Count-1。

一些函数会返回内核对象的伪句柄,不会影响Usage Count的值,对伪句柄调用CloseHandle会返回false。



3、Suspend Count

指明线程的暂停计数。初值为1,因为刚创建的线程初始化需要时间,能避免线程被立即调度到CPU中。

初始化完成后,CreateProcessCreateThread函数就会检查是否传递了CREATE_SUSPENDED标志:

  • 如果传递了,函数返回,新线程处于暂停状态;

  • 如果未传递,暂停计数递减为0,线程处于可调动状态。

创建线程时指定CREATE_SUSPENDED标志,可以在线程运行前改变现成的运行环境。环境设置完成后,可以使用ResumeThread函数唤醒线程。

ResumeThread函数会递减线程的Suspend Count。如果调用成功,函数会返回线程的前一个暂停计数。

SuspendThread函数可以挂起线程,递增线程的Suspend Count。已经被暂停的线程还能多次暂停(其实就是Suspend Count++)。

任何线程都能通过刚函数挂起另一个线程。

约每20ms,windows查看一次当前存在的所有线程内核对象,从可调度的线程中选择一个,将它的CONTEXT装入CPU寄存器。这一过程就是上下文切换


4、Exit Code

指定了线程的退出代码,也就是线程函数的返回值。

在线程运行期,Exit Code的值为STILL_ACTIVE;线程结束后,Exit Code的值为线程函数的返回值。

可以通过GetExitCodeThread函数得到线程的退出代码。

...
DWORD dwExitCode;
if(::GetExitCodeThread(hThread, &dwExitCode))
	if(dwExitCode == STILL_ACTIVE)
	{
		...
	}
	else
	{
		...
	}
...

5、Signaled

指示线程对象是否为“受信”状态。

在线程运行期间,Signaled的值为false,“未受信”;

线程结束后,Signaled的值为true,针对此对象的等待函数就会返回。例如WaitForSingleObject函数。


线程的终止 ~

ExitThread()

TerminateThread()

线程终止时,会发生下列事件:

  1. 线程函数中创建的所有C++对象,通过析构函数销毁;
  2. 该线程使用的栈堆被释放;
  3. 修改Exit Code为线程的返回值;
  4. 递减Usage Count。

终止线程的方法有4种:

1、线程函数自然退出

函数运行到return语句。

2、使用ExitThread函数终止线程

ExitThread会终止当前线程的运行,使系统释放所有此线程使用的资源。但C/C++资源不会正确地清除。例如:下方代码中,CMyClass的析构函数不会执行。

...
class CMyClass
{
public:
	CMyClass() { cout << "Con" << endl; }
	~CMyClass() { cout << "Des" << endl; }
};

int main()
{
	CMyClass object;
	::ExitThread(0);
	return 0;
}

3、使用TerminateThread终止另一个线程

强烈建议避免使用的函数,因为无法知道目标线程的运行状况,目标线程很可能无法释放以占有的资源,如打开的文件和申请的内存等。

使用TerminateThread系统甚至不会释放线程使用的栈堆。

4、使用ExitProcess结束进程

这种方法相当于对进程中的所有线程调用TerminateThread。也是应该避免使用的。

始终应该让线程正常退出,即由它的线程函数返回。

如果需要提前终止某个线程,应该使用通知的方式,让该线程收到通知后自行退出。

线程的优先级 ~

SetThreadPriority()

WaitForMultipleObjects()

最高31,最低0。使用优先级调度,调度程序选择优先级最高的可调度线程分配CPU运行。

Windows支持6个优先级类idle、below normal、normal、above normal、high、real_time。

默认使用normal,也是大多数应用采用的优先级。

进程有优先级,还可以为进程中的线程赋予相对优先级。

通常不对进程的优先级进行设置,所以可认为线程的相对优先级就是它的真实优先级。

设置线程优先级的函数:SetThreadPriority

例子:

DWORD WINAPI ThreadIdle(LPVOID lpParam)
{
	int i = 0;
	while (i++ < 10)
		cout << "Idle Thread is running" << endl;
	return 0;
}
DWORD WINAPI ThreadNormal(LPVOID lpParam)
{
	int i = 0;
	while (i++ < 10)
		cout << "Normal Thread is running" << endl;
	return 0;
}

int main()
{
	DWORD dwThreadID;
	HANDLE h[2];

// 常用的编程方法:为线程设置好一些条件后才唤醒
	h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL, CREATE_SUSPENDED, &dwThreadID);
	::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE);
	::ResumeThread(h[0]);

	h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL, 0, &dwThreadID);

	::WaitForMultipleObjects(2, h, TRUE, INFINITE);

	::CloseHandle(h[0]);
	::CloseHandle(h[1]);

	return 0;
}
WaitForMultipleObjects 的一些参数解释

前2个参数表示句柄数组的长度句柄数组的指针

bWaitAll:为true时,只有当等待的所有内核对象都变成受信状态后才返回;如果为false,则只要有一个线程内核对象变为受信状态,函数就返回。

bWaitAll为false时函数从索引0开始扫描句柄数组。

函数的返回值指明了是哪一个内核对象受信。

返回值的取值:

WAIT_OBJECT_0 ~ (WAIT_OBJECT_0 + nCount– 1)

WAIT_ABANDONED_0 ~ (WAIT_ABANDONED_0 + nCount– 1)和锁有关

WAIT_TIMEOUT

WAIT_FAILED

C/C++运行期库 ~

_beginthreadex()

_endthreadex()

在实际开发过程中,一般是使用C/C++运行期函数_beginthreadex来创建线程。

C/C++运行期库提供另一个版本的CreateThread是为了多线程同步的需要。在标准运行库中有许多全局变量,例如用于表示线程状态的errnostrerror等。而在多线程程序设计中,每个线程都要有唯一的状态。

所以使用_beginthreadex能让每个线程各自拥有一个全局变量的副本。

_beginthreadex的参数与CreateThread功能对应,类型和参数名并不完全相同。所以在使用时需要进行类型转换。

_beginthreadex让运行期设置了相关变量后再调用CreateThread函数。

同样有ExitThread对应的_endthreadex

_endthreadex会释放_beginthreadex为保持线程同步而申请的内存空间,再调用ExitThread终止线程。

同理,尽量让线程自然退出。

线程同步

共享资源包括全局变量、公共数据成员、句柄等。

线程必须互斥访问共享资源。

临界区对象 ~

InitializeCriticalSection()

EnterCriticalSection()

LeaveCriticalSection()

DeleteCriticalSection()

一个说明线程同步必要性的例子:

记这段代码是为了注意一下_beginthreadex的用法,以及作为共享资源的全局变量的概念。

#include<iostream>
#include<windows.h>
#include<process.h>

using namespace std;

// 3个共享资源
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;

UINT __stdcall ThreadFunc(LPVOID)
{
	while (g_bContinue)
	{
		++g_nCount1;
		++g_nCount2;
	}
	return 0;
}

int main()
{
	UINT uId;
	HANDLE h[2];

	h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
	h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

	// 等待1秒后通知2个线程函数结束,关闭句柄
	Sleep(1000);
	g_bContinue = FALSE;
	::WaitForMultipleObjects(2, h, TRUE, INFINITE);
	::CloseHandle(h[0]);
	::CloseHandle(h[1]);

	cout << "g_nCount1: " << g_nCount1 << endl;
	cout << "g_nCount2: " << g_nCount2 << endl;

	return 0;
}

最后得到的结果,2个全局变量的值不相等。


使用临界区对象

由OS的知识可知,进入临界区之前有一个进入区,在那里申请锁;离开临界区后有一个离开区,在那里释放锁。Windows为实现这一功能都提供了API函数。

临界区对象是定义在数据段中的一个CRITICAL_SECTION结构(在微软文档中没有这个结构的定义,只要会使用就行了)。

总之,在任何线程使用临界区对象之前先要用InitializeCriticalSection初始化;

然后在进入区、退出区分别都要调用EnterCriticalSectionLeaveCriticalSection

不再使用临界区对象后调用DeleteCriticalSection

使用临界区对象对之前的代码进行修改:

int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
CRITICAL_SECTION g_cs;// 临界对象作为全局变量


UINT __stdcall ThreadFunc(LPVOID)
{
	while (g_bContinue)
	{
// 在任一个线程进入临界区后且释放锁之前,其它调用这个函数的线程都会被阻塞
		::EnterCriticalSection(&g_cs);
		++g_nCount1;
		++g_nCount2;
		::LeaveCriticalSection(&g_cs);
	}
	return 0;
}


int main()
{
	UINT uId;
	HANDLE h[2];

	// 初始化临界区对象
	::InitializeCriticalSection(&g_cs);

	h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
	h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

	Sleep(1000);

	g_bContinue = FALSE;
	::WaitForMultipleObjects(2, h, TRUE, INFINITE);
	::CloseHandle(h[0]);
	::CloseHandle(h[1]);

	// 删除临界区对象
	::DeleteCriticalSection(&g_cs);

	cout << "g_nCount1: " << g_nCount1 << endl;
	cout << "g_nCount2: " << g_nCount2 << endl;

	return 0;
}

结果中2个全局变量的值是相同的。

临界区对象能保护共享数据,但不能用于进程之间的资源锁定,因为它不是内核对象。

要在进程间维持线程同步,要使用事件内核对象

互锁函数 ~

InterlockedIncrement()

InterlockedDecrement()

…互锁函数有很多,这里只介绍2个

互锁函数为同步访问多线程共享变量提供一个简单的机制。如果变量处于共享内存,不同进程的线程也能使用此机制。

互锁函数不是信号量,是一堆用于进行数值处理的函数,其中一部分可用来替代基本运算符。

使用方法:直接用互锁函数替换基本运算。

// 在不使用临界区对象的版本上修改
UINT __stdcall ThreadFunc(LPVOID)
{
	while (g_bContinue)
	{
		::InterlockedIncrement((long*)&g_nCount1);
		::InterlockedIncrement((long*)&g_nCount2);
	}
	return 0;
}

事件内核对象 Event ~

CreateEvent()

OpenEvent()

SetEvent()

ResetEvent()

主线程创建工作线程后,需要使用通信机制来控制工作线程,同时,工作线程有时也要将一些情况通知给主线程

事件内核对象就是一个较好的通信方法。

事件对象(event)有未受信和受信状态,可以使用WaitForSingleObject函数等待其变成受信状态。

事件对象能在2个状态之间转变。

事件对象是个内核对象,所以可以跨进程使用

CreateEvent函数

先要使用CreateEvent函数创建事件对象。

HANDLE CreateEventW(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL                  bManualReset,
  BOOL                  bInitialState,
  LPCWSTR               lpName
);

部分参数:

bManualReset:指定内核对象为人工重置自动重置

​ 人工重置:一个人工重置事件对象受信后,所有等待在这个事件上的线程都变为可调度状态;

​ 自动重置:一个自动重置事件对象受信后,仅一个等待在该事件上的线程变为可调度状态,然后自动重置此事件为未受信状态

bInitialState:事件的初始状态是受信或未受信。

lpName:指定事件对象的名称,可在其他进程的线程中通过CreateEventOpenEvent获取此内核对象的句柄。

创建或打开一个事件内核对象后,会返回事件的句柄。当不再使用时,要调用CloseHandle释放。

事件对象建立后,可通过SetEventResetEvent函数来设置状态。

SetEvent:设置为受信;

ResetEvent:设置为未受信。

例子:

#include<iostream>
#include<windows.h>
#include<process.h>

using namespace std;

// 一个句柄,用于接收创建事件的返回值。
HANDLE g_hEvent;

UINT __stdcall ChildFunc(LPVOID)
{
	// !等待事件受信
	::WaitForSingleObject(g_hEvent, INFINITE);
	cout << "Child thread is working......" << endl;
	// 模拟工作时间
	::Sleep(5000);
	return 0;
}


int main()
{
	HANDLE hChileThread;
	UINT uId;

	// 创建一个自动重置的、未受信的事件内核对象
	g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);

	hChileThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);

	cout << "Input a char to tell the Child Thread to Work" << endl;
	cin.get();
	// !通知子线程函数开始工作
	::SetEvent(g_hEvent);

	::WaitForSingleObject(hChileThread, INFINITE);
	cout << "work has been finished" << endl;
	::CloseHandle(g_hEvent);
	::CloseHandle(hChileThread);

	return 0;
}

线程局部存储 ~

TlsAlloc()

TlsSetValue()

TlsGetValue()

TlsFree()

是一个用于存储线程局部数据的系统。为每个线程提供全局变量或静态变量的副本,方便编程。

各个线程通过由TLS分配的全局索引来访问与自己关联的数据。

进程的数组用来指示线程数组中哪一个成员在使用中。线程数组则保存自己的数据。

Windows为系统中每一个进程维护一个位数组,再为该进程中的每一个线程申请一个同样长的数组空间。

位数组的成员是一个标志位,被设置为FREEINUSE,至少有TLS_MINIMUM_AVAILABLE个标志位可用。

系统为每个进程都维护一个长度为TLS_MINIMUM_AVAILABLE的位数组,TlsAlloc函数的返回值就是数组的一个下标(索引)

位数组的唯一作用是记忆哪一个下标正在使用中。

调用TlsAlloc时,系统挨个检查数组中成员的值,直到找到一个为FREE的成员,将其修改为INUSE,然后返回其下标。

如果查找失败,返回-1。

当线程被创建时,Windows就在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的数组,数组成员都被初始化为0。

线程通过调用TlsSetValueTlsGetValue来设置、获取线程数组中的值。用于这2个函数的索引就是TlsAlloc的返回值。

这也使得每个线程只能访问自己的线程数组。

**例子:**TLS的存取都要用到类型转换

#include<iostream>
#include<windows.h>
#include<process.h>

using namespace std;


DWORD g_tlsUsedTime; // 保存TLS索引值
void InitStartTime();
DWORD GetUsedTime();

UINT __stdcall ThreadFunc(LPVOID)
{
	int i;

	InitStartTime();

	i = 10000 * 10000;
	while (i--) {}
	
	cout << "Thread ID : " << ::GetCurrentThreadId() << ", Used Time : " << GetUsedTime() << endl;
	
	return 0;
}


int main()
{
	UINT uId;
	HANDLE h[10];

	// 申请一个索引
	g_tlsUsedTime = ::TlsAlloc();

	// 创建10个线程
	for (int i = 0; i < 10; ++i)
		h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

	// 等待并关闭所有线程
	for (int i = 0; i < 10; ++i)
	{
		::WaitForSingleObject(h[i], INFINITE);
		::CloseHandle(h[i]);
	}

	::TlsFree(g_tlsUsedTime);

	return 0;
}

void InitStartTime()
{
	// 获取当前时间,通过索引保存到自己的数组中
	// DWORD和LPVOID都是4字节大小
	DWORD dwStart = ::GetTickCount();
	::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
}

DWORD GetUsedTime()
{
	// 通过索引获取数组数值,并返回
	DWORD dwElapsed = ::GetTickCount();
	dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
	return dwElapsed;
}
使用TLS的必要性

**问题:**为什么不直接使用线程的局部对象,而是要对全局变量构造多个副本?

如果不使用TLS达成上方代码同样的功能,有2种方法:

1、使用一个全局变量数组,保存InitStartTime的结果,数组长度与线程数量相等;

2、在ThreadFunc函数中创建一个局部变量,然后修改函数:

UINT __stdcall ThreadFunc(LPVOID)
{
	...
	DWORD initTime = InitStartTime();
	...
	cout << "Thread ID : " << ::GetCurrentThreadId() << ", Used Time : " << GetUsedTime(initTime) << endl;
	...
}
...
DWORD InitStartTime()
{
	return ::GetTickCount();
}

DWORD GetUsedTime(DWORD initTime)
{
	DWORD dwElapsed = ::GetTickCount();
	return dwElapsed - initTime;
}

尝试过输出initTime的地址,每个线程的局部变量地址都不一样,好像不会像全局变量那样有同步问题。

就本例而言,TLS没体现出优势。

一个较可信的说法:

全局对象:全局变量的作用域和生命周期是全局的,如果设计了一个全局对象,就意味着希望在多线程的环境中,仍然能对其共享和访问。引入了线程的互斥后并没有改变共享的本质。

局部变量:局部对象的作用域和生命周期局限于所在的函数中,如果希望在线程执行时,任意函数和对象都能访问该变量,该如何处理?如果改用全局对象,这又会使得该变量的作用域上升到进程级别。

在全局范围内都能访问一个函数的局部对象,这就催生了TLS。

另一种回答:

“TLS is helpful for things like user session context information which is thread specific, but might be used in various unrelated methods.

“In such situations, TLS is more convenient than passing the information up and down the call stack.”

理解:TLS用于存储线程上下文信息,这些信息与线程紧密相关,但有可能被各种不相关的函数使用。

在这种情况下,使用TLS比在调用栈中来回传递参数更方便。

设计自己的线程局部存储 ~

offsetof()

GlobalAlloc()

GlobalLock()

GlobalUnlock()

GlobalFree()

GlobalHandle()

实际的应用中,TLS往往用于保存各线程相关联的指针,指针指向的数据是在进程的堆中申请的。

希望TLS具有的特性:

**1、**自动管理它所保存的指针所指内存单元的分配和释放。

​ 一方面能方便用户使用;另一方面,在线程不使用TLS的情况下,系统可以决定不分配内存,节省空间。

**2、**允许用户申请使用任意多个TLS索引。

为了实现以上特性,就得设计新的TLS。

新的TLS由4个类构成:

CSimpleList:实现简单的链表功能,把各线程私有数据联系到一起。

CNoTrackObject:重载new和delete操作符,负责为线程私有数据分配内存空间。

CThreadSlotData:系统的核心,负责分配索引和存取线程私有数据。

CThreadLocal:最终提供给用户的类模板。为用户提供API。

CSimpleList 类

总结:是为了达成“TLS存任意大小数据”而设计的,从下方的实现来看,所谓的“任意大小数据”是指链表结点,链表结点的data域是个指向实际TLS的指针,与原来的TLS存储的数据没有区别。

换句话说,就是将原版的TLS外面包了一层struct,形成链表结点。

然后又用CSimpleList 类来管理这些结点的释放

windows的TLS只允许用户保存一个32位(4B)的指针,改进的TLS要允许用户保存任意类型的数据。

这个任意类型的数据所占内存在进程的堆中分配,当用户释放全局索引时,系统要将每个线程内此数据占用的内存释放掉,这就要求系统把为各个线程分配的内存都记录下来。

上一段内容的意思:

原版TLS保存的是4Byte指针,且指针所指内存是由用户手动释放的,实际上与TLS没有关系。TLS保存的就是一个4Byte大小的数据,调用TlsFree不需要执行任何额外操作,当要保存新的TLS时直接覆盖就行了。

而改进的TLS为了保存任意类型数据,即TLS可能会保存占用较多内存的数据结构。所以在释放索引时,必须得将使用了该索引的线程申请的内存释放掉。

较好的方法是将各个TLS的首地址用一个链表连在一起,释放索引时遍历链表释放内存空间。

链表结点(以CThreadData作为例子):

struct CThreadData
{
	CThreadData* pNext;	// 链表的Next
	LPVOID pData;		// 指向线程的私有数据的指针
}
思路

需要编写一个类专门管理这个数据结构。

需要知道的唯一信息是pNext成员的偏移量,指定偏移量后,就知道了pNext的地址,就能对其进行存取了。再记录下链表第一个结点的地址,类基本就实现了。

void* m_pHead;
size_t m_nNextOffset;
// 为了函数的通用性,使用void*
void** GetNextPtr(void* p) const 
	return (void**)((BYTE*)p + m_nNextOffset);
// 对返回值解引用,得到的仍是一个指针(pNext)。

GetNextPtr通过偏移量m_nNextOffset取得pNext的地址,所以返回值是指针的指针。例如,传递了一个CThreadData类型数据的指针,在GetNextPtr在这个地址加上偏移量就得到了成员pNext的地址。( 即&(ctd.pNext) = &ctd + m_nNextOffset

为什么会需要偏移量?一个类对象的第一个成员变量的地址不就是等于类对象的地址吗???

因为传递进来的指针p不一定指向一个CThreadData对象,指针p指向的类中,作为pNext的成员变量也不一定是第一个变量

还是不理解GetNextPtr函数,如果要返回“指向p->pNext的指针”,直接返回&(p->pNext)不行吗?

int main()
{
	MyClass* mclass = new MyClass{ nullptr,0 };

	CSimpleList list(offsetof(MyClass, mNext));

	cout << list.GetNextPtr(mclass) << endl;
	cout << &(mclass->mNext) << endl;
}

输出地址相同。

设计

将这个实现了简单链表功能的类命名为CSimpleList。接下来还要为该类设计用户接口。

_AFXTLS_.h

#pragma once
#include<windows.h>

class CSimpleList
{
private:
	void* m_pHead;	// 链表头结点
	size_t m_nNextOffset;// 链表结点的next指针的偏移量

public:
	CSimpleList(int nNextOffset = 0)
		:m_pHead(NULL), m_nNextOffset(nNextOffset)
	{}

	void AddHead(void* p);
	BOOL Remove(void* p);

	void SetNextOffset(int nNextOffset) { m_nNextOffset = nNextOffset; }
	BOOL IsEmpty() const { return m_pHead == NULL; }
	void RemoveAll() { m_pHead = NULL; }
	void* GetHead() const { return m_pHead; }
	// 传入了指针preElement,获得了指针preElement->next
	void* GetNext(void* preElement) const { return *GetNextPtr(preElement); }
	// 传入了指针p,获得了指向指针p->next的指针
	void** GetNextPtr(void* p) const { return (void**)((BYTE*)p + m_nNextOffset); }
};

_AFXTLS_.cpp

#include "_AFXTLS_.h"

void CSimpleList::AddHead(void* p)
{// 头插法
	*GetNextPtr(p) = m_pHead;
	m_pHead = p;
}

BOOL CSimpleList::Remove(void* p)
{
	if (p == NULL)
		return NULL;

	BOOL bResult = FALSE;
	// 移除头结点,m_pHead直接等于nullptr
	if (p == m_pHead)
	{
		m_pHead = *GetNextPtr(p);
		bResult = TRUE;
	}
	else
	{
		void* pTest = m_pHead;
		// 定位到p结点的前一个结点
		while (pTest != NULL && *GetNextPtr(pTest) != p)
			pTest = *GetNextPtr(pTest);
		// 如果链表中的确存在p结点,将其删除
		if (pTest != NULL)
		{
			*GetNextPtr(pTest) = *GetNextPtr(p);
			bResult = TRUE;
		}
	}

	return bResult;
}

疑问:RemoveAll函数的编写好像有点危险,这个函数仅仅是将头指针置空。如果用户没有在使用完CSimpleList 后通过循环语句清除链表,直接调用RemoveAll,则会导致内存泄漏。

补充:Remove也仅仅是将要删除的结点从链表移除而已,没有释放内存。

参考下方例子,可能是用一个指针p1保存了p结点后,调用list.Remove(p),然后再delete p1

似乎是这样的,所谓的移除和清空操作的内存释放,都是完全由用户自己编写代码完成的,最后RemoveAllRemove这2个函数都仅仅是针对链表操作的,结点所占内存情况完全不关心。

用例
#include<iostream>
#include "_AFXTLS_.h"

using namespace std;

struct MyThreadData
{
	int mData;
	MyThreadData* pNext;
};

int main()
{
	MyThreadData* pData;
	// 告知CSimpleList MyThreadData结构的pNext成员位置
	CSimpleList list(offsetof(MyThreadData, pNext));

	// 为链表添加成员
	for (int i = 0; i < 10; ++i)
	{
		pData = new MyThreadData;
		pData->mData = i;
		list.AddHead(pData);
	}

	// 使用链表中的数据

	// 清空链表
	pData = (MyThreadData*)list.GetHead();
	while (pData)
	{
		MyThreadData* pNextData = pData->pNext;
		cout << pData->mData << endl;
		delete pData;
		pData = pNextData;
	}

	return 0;
}

offsetof

 #define offsetof(s,m) ((::size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
改进:CTypeSimpleList

目前CSimpleList为了泛用性,与数据交互的函数都使用了void*,在使用时都得进行显示转换。

可以将其修改为模板,将CSimpleList所有成员函数中的void*替换为模板类T,省略了显式转换。CTypeSimpleList

CNoTrackObject 类

总结:只是重载了new和delete而已。

C++默认的new运算符除了分配内存,还存有额外信息,用于告知delete运算符该如何释放内存。

再调试环境中,为了方便跟踪内存泄漏的情况,new的额外信息更多。

线程私有数据使用的内存是由系统在内部自动为用户分配的。当使用私有数据的线程结束时,这个系统也会为用户自动释放掉这块内存。如果能确保不会发生内存泄漏,就不需要跟踪内存的使用了。

为了保证所有线程都是用重写的new和delete,可以编写一个重载new和delete运算符的类,让所有线程私有数据使用的结构都从此类继承即可。将这个类命名为CNoTrackObject

设计

_AFXTLS_.h

class CNoTrackObject
{
public:
	void* operator new(size_t nSize);
	void operator delete(void*);
	virtual ~CNoTrackObject() {}
};

_AFXTLS_.cpp

void* CNoTrackObject::operator new(size_t nSize)
{
	void* p = ::GlobalAlloc(GPTR, nSize);
	return p;
}

void CNoTrackObject::operator delete(void* p)
{
	if (!p)
		::GlobalFree(p);
}

如果是用GMEM_MOVEABLE方式申请移动内存,在使用之前,必须先调用GlobalLock函数把它锁定到物理内存。不使用时再调用GlobalUnlock函数解锁。

用例
struct CThreadData : public CNoTrackObject
{
	int mData;
	MyThreadData* pNext;
};
CThreadSlotData 类

总结:

到了这一步,自己的TLS系统中一共有3个数组

1、windows的TLS,存储的是线程各自的CThreadData对象。(之后都简称wTLS)

2、CThreadDatapData成员指向的数组,数组成员是LPVOID,指向真正的数据。

3、槽数组。数组成员CSlotData表示该槽是否被使用、该槽所记录的句柄类型。

2个用于管理数组的数据结构:

1、CTypeSimpleList:以链表的形式管理CThreadDataCThreadData作为链表结点。只有在执行槽的释放操作时才会通过链表获取CThreadData,平时存取元素都是通过wTLS来获取线程的CThreadData对象。

2、CThreadSlotData :类的对象是全局变量。用于管理槽索引的分配、释放。链表wTLS的索引值都是该类的成员变量。对链表槽数组的操作都要用临界区对象来提供互斥访问保证。

这时再把CThreadData结构中pData指向的空间分成多个槽(slot),每个槽放一个线程私有数据指针,就可以允许每个线程存放任意个线程私有指针了。

原来pData指向线程的私有数据;

分成多个槽后,pData指向一个LPVOID指针数组,数组的元素才是指向线程的私有数据。

设计1

引入了CThreadSlotData后,CThreadData结构中要保存的信息就是指针数组的首地址数组的个数

CThreadData修改如下:

struct CThreadData : public CNoTrackObject
{
	CThreadData* pNext;
	int nCount;		// 数组元素个数
	LPVOID* pData;	// 数组首地址
};

注意!pData是一个数组的首地址,数组成员类型是LPVOID,所以pData类型为LPVOID*


之后要解决的问题:

当用户请求访问线程的私有数据时,应该说明要访问的是哪一个槽号对应的线程私有数据。

系统先获得该线程的CThreadData结构(链表结点)首地址,再以用户提供的槽号作为pData的下标取得指向私有数据的指针。此时要解决问题:

1、如何为用户分配槽号?

2、如何保存各线程中CThreadData结构首地址?

问题2:直接用Windows的TLS申请一个全局索引,保存CThreadData结构首地址。

**问题1:**模仿Windows实现TLS的方法,申请一个全局数组,数组下标表示槽号,成员的值表示该槽是否被分配。该全局数组每个进程唯一。

数组成员还可以用来表示其他信息,如是什么模块占用了该槽等。

所以数组的数据类型应该如下所示:

struct CSlotData
{
	DWORD dwFlags;
	HINSTANCE hInst;
}

需要设计一个类来负责全局槽号的分配和各线程中槽里数据的存取,命名为CThreadSlotData

设计2

类中要有一个用作TLS索引的成员变量。

DWORD m_tlsIndex;

使用CTypeSimpleList串联各线程的私有数据。

CTypeSimpleList<CThreadData*> m_list;

负责管理全局标志数组的成员:

int m_nAlloc;			// m_pSlotData所指向数组的大小
int m_nMax;				// 已分配的槽的数量
CSlotData* m_pSlotData;	// 全局数组的首地址

书中指出,总是保留第一个槽Slot0不使用,且m_nMax的值实际上是:“当前被使用的槽中,槽号最高的数”+1,类似栈顶指针这样的。

都没有解释为什么。

_AFXTLS_.h

struct CSlotData;
struct CThreadData;

class CThreadSlotData
{
private:
	int m_nAlloc;	// m_pSlotData所指向数组的大小
	int m_nRover;	// 当前可用槽的索引值。为了快速找到一个空闲槽而设定的值
	int m_nMax;		// CThreadData中pData所指向的数组大小
	
/* 以下3个成员的使用,除了作为TLS索引的m_tlsIndex,都要通过临界对象访问 */
	DWORD m_tlsIndex;// 作为Windows的TLS的索引,供线程存储各自的CThreadData结构
	CSlotData* m_pSlotData;	// 指向进程唯一的槽数组的首地址指针
	CTypeSimpleList<CThreadData*> m_list; // 用于管理CThreadData的链表

/* CThreadSlotData定义的对象是全局对象,所以需要用到临界区对象m_cs来同步多个线程对CThreadSlotData对象的并发访问。*/
	CRITICAL_SECTION m_cs;	

public:
	CThreadSlotData();
	~CThreadSlotData();

	int AllocSlot();
	void FreeSlot(int nSlot);
	void* GetThreadValue(int nSlot);
	void SetValue(int nSlot, void* pValue);
	void DeleteValues(HINSTANCE hInst, BOOL bAll = FALSE);

	void DeleteValues(CThreadData* pData, HINSTANCE hInst);
	void* operator new(size_t, void* p) { return p; }
};

_AFXTLS_.cpp

长的一批

struct CSlotData
{
	DWORD dwFlags;
	HINSTANCE hInst;
};
struct CThreadData :public CNoTrackObject
{
	CThreadData* pNext;
	int nCount;
	LPVOID* pData;
};

#define SLOT_USED 0X01

// 构造函数
CThreadSlotData::CThreadSlotData()
	:m_tlsIndex(::TlsAlloc()), m_nAlloc(0), m_nMax(0), m_nRover(1), m_pSlotData(NULL)
{
	// 设置偏移量
	m_list.SetNextOffset(offsetof(CThreadData, pNext));
	// 初始化临界区
	::InitializeCriticalSection(&m_cs);
}

int CThreadSlotData::AllocSlot()
{
	::EnterCriticalSection(&m_cs);	// 进入区
	int nAlloc = m_nAlloc;	// 获取槽的总数
	int nSlot = m_nRover;	// 获取当前的槽的索引值

	// 如果槽的索引值大于槽的总数 || 当前索引值定位到的槽不可用
	if (nSlot >= nAlloc || m_pSlotData[nSlot].dwFlags & SLOT_USED)
	{
		// 先从头(略过0)遍历槽数组,查找前面是否有用过的槽被释放了,将nSlot定位到第一个找到的可用槽位置
		for (nSlot = 1; nSlot < nAlloc && m_pSlotData[nSlot].dwFlags & SLOT_USED; ++nSlot);

		// 循环结束了,没找到可用槽,意味着所有槽都已被使用(或是第一次使用,还有没建立槽数组)
		if (nSlot >= nAlloc)
		{
			// 以32为一个单位扩容
			int nNewAlloc = nAlloc + 32;
			HGLOBAL hSlotData;	// HANDLE的别名

			// m_pSlotData == NULL表示槽数组还没建立,是第一次使用CThreadSlotData对象分配槽
			if (m_pSlotData == NULL)
				// 申请32个CSlotData对象大小的可移动内存
				hSlotData = ::GlobalAlloc(GMEM_MOVEABLE, nNewAlloc * sizeof(CSlotData));
			else	// 如果是所有槽都被使用,原地扩容
			{
				// 获取已分配的内存首地址
				hSlotData = ::GlobalHandle(m_pSlotData);
				// 解锁内存
				::GlobalUnlock(hSlotData);
				// 重新分配内存,新分配的内存大小为扩容后的大小
				hSlotData = ::GlobalReAlloc(hSlotData, nNewAlloc * sizeof(CSlotData), GMEM_MOVEABLE);
			}
			// 无论是刚创建槽数组,还是重新分配了内存,都将内存锁定到物理内存
			CSlotData* pSlotData = (CSlotData*)::GlobalLock(hSlotData);
			// 将新申请的空间初始化为0。
			// pSlotData + m_nAlloc: 新申请部分的内存的首地址
			memset(pSlotData + m_nAlloc, 0, (nNewAlloc - nAlloc) * sizeof(CSlotData));
			// 更新槽数组大小和槽数组首地址
			m_nAlloc = nNewAlloc;
			m_pSlotData = pSlotData;
		}
	}
	// 更新线程的局部存储的数组大小m_nMax,递增更新
	if (nSlot >= m_nMax)
		m_nMax = nSlot + 1;
	
	m_pSlotData[nSlot].dwFlags |= SLOT_USED; // 将nSlot定位的槽元素置为“已使用”
	m_nRover = nSlot + 1;	// 更新索引值,总是假设下一个槽未被使用
	::LeaveCriticalSection(&m_cs);

	return nSlot;	// 返回的槽号可以被其他成员函数使用了
}

/*
* 真正用户使用的存储数据的空间是GetThreadValue函数返回的指针指向的内存,
* CThreadSlotData不负责创建这块空间,但它负责释放这块空间所使用的内存,就在释放索引的时候。
* 
* 释放一个槽,就释放了所有线程中此槽对应的用户数据
*/
void CThreadSlotData::FreeSlot(int nSlot)
{
	::EnterCriticalSection(&m_cs);

	// 直接通过链表获取CThreadData指针,不使用wTLS
	// CTypeSimpleList类重载了operator TYPE()函数,下方代码直接获取链表头结点指针
	CThreadData* pThreadMember = m_list;
	// 遍历链表,将所有线程的TLS中槽指定的位置删除
	while (pThreadMember != NULL)
	{	// 确认nSlot的值有效后,执行删除操作
		if (nSlot < pThreadMember->nCount)
		{
			delete (CNoTrackObject*)pThreadMember->pData[nSlot];
			pThreadMember->pData[nSlot] = NULL;
		}
		pThreadMember = pThreadMember->pNext;
	}

	// 将此槽号标识为未被使用
	m_pSlotData[nSlot].dwFlags &= ~SLOT_USED;
	::LeaveCriticalSection(&m_cs);
}

void* CThreadSlotData::GetThreadValue(int nSlot)
{
	// 从wTLS获取CThreadData指针
	CThreadData* pThreadMember = (CThreadData*)::TlsGetValue(m_tlsIndex);
	// 如果TLS还未创建 || 槽索引值无效,返回NULL
	if (pThreadMember == NULL || nSlot >= pThreadMember->nCount)
		return NULL;
	return pThreadMember->pData[nSlot];
}

void CThreadSlotData::SetValue(int nSlot, void* pValue)
{
	// 通过wTLS获得CThreadData指针
	CThreadData* pThreadMember = (CThreadData*)::TlsGetValue(m_tlsIndex);

	// (如果是第一次使用 || 槽索引值大于私有空间数组的容量) && 传入了有效值
	if ((pThreadMember == NULL || nSlot >= pThreadMember->nCount) && pValue != NULL)
	{
		// 如果是第一次使用,初始化一个CThreadData对象并插入链表
		if (pThreadMember == NULL)
		{
			pThreadMember = new CThreadData;
			pThreadMember->nCount = 0;
			pThreadMember->pData = NULL;

			// 将刚创建的结点登记到链表中
			::EnterCriticalSection(&m_cs);
			m_list.AddHead(pThreadMember);
			::LeaveCriticalSection(&m_cs);
		}

/* !!!这里第一次使用是用LMEM_FIXED模式分配内存;如果是扩容,则是用LMEM_MOVEABLE模式分配,且在置0前没有固定内存!!! */
		// 如果槽索引值大于私有空间数组的容量
		// 情况一:第一次使用,TLS数组还未创建。
		if (pThreadMember->pData == NULL)
			// 私有空间数组大小m_nMax;(void**)——pData为LPVOID*类型,指针的指针
			pThreadMember->pData = (void**)::GlobalAlloc(LMEM_FIXED, m_nMax * sizeof(LPVOID));
		else	// 情况二:槽索引值大于私有空间数组的容量,扩容。
			pThreadMember->pData = (void**)::GlobalReAlloc(pThreadMember->pData,
				m_nMax * sizeof(LPVOID), LMEM_MOVEABLE);

		// 将新申请部分的内存置0
		memset(pThreadMember->pData + pThreadMember->nCount, 0,
			(m_nMax - pThreadMember->nCount) * sizeof(LPVOID));
		// 更新nCount成员变量,保存设置好了的CThreadData对象
		pThreadMember->nCount = m_nMax;
		::TlsSetValue(m_tlsIndex, pThreadMember);
	}
	// 给数组元素赋值
	pThreadMember->pData[nSlot] = pValue;
}

// 全都是调用另一个版本的DeleteValues来完成任务
void CThreadSlotData::DeleteValues(HINSTANCE hInst, BOOL bAll)
{
	::EnterCriticalSection(&m_cs);
	if (!bAll)
	{
		// 仅删除当前线程的TLS,从wTLS获取CThreadData指针
		CThreadData* pThreadMember = (CThreadData*)::TlsGetValue(m_tlsIndex);
		if (pThreadMember != NULL)
			DeleteValues(pThreadMember, hInst);
	}
	else
	{
		// 删除所有线程的TLS,遍历链表获取CThreadData指针
		CThreadData* pThreadMember = m_list.GetHead();
		while (pThreadMember != NULL)
		{
			CThreadData* pNextData = pThreadMember->pNext;
			DeleteValues(pThreadMember, hInst);
			pThreadMember = pNextData;
		}
	}
	::LeaveCriticalSection(&m_cs);
}

void CThreadSlotData::DeleteValues(CThreadData* pThreadMember, HINSTANCE hInst)
{
	BOOL bDelete = TRUE;
	// 遍历槽数组,检查是否有模块匹配。如果匹配,根据匹配的索引值删除线程TLS数组对应的数据
	// hInst的值为NULL时,表示匹配所有模块
	for (int i = 1; i < pThreadMember->nCount; ++i)
	{
		if (hInst == NULL || m_pSlotData[i].hInst == hInst)
		{
			// 如果占用槽的模块句柄与参数hInst匹配,则将其删除,腾出线程局部存储空间。
			delete (CNoTrackObject*)pThreadMember->pData[i];
			pThreadMember->pData[i] = NULL;
		}
		else
		{
			// 只要遇到有槽被占用且与参数hInst不匹配,标志位bDelete就置为FALSE
			// 意思就是这个线程TLS还存有数据,不能清空!
			if (pThreadMember->pData[i] != NULL)
				bDelete = FALSE;
		}
	}
	if (bDelete)
	{	// 如果bDelete = TRUE,意味着这个CThreadData的数组没有保存任何有效数据,
		// 执行清空操作,将其从链表中移除
		::EnterCriticalSection(&m_cs);
		m_list.Remove(pThreadMember);
		::LeaveCriticalSection(&m_cs);
		// 释放这个CThreadData对象占用的内存
		::LocalFree(pThreadMember->pData);
		delete pThreadMember;

		// 清除这个线程的TLS索引
		::TlsSetValue(m_tlsIndex, NULL);
	}
}

/*
* 虚构函数要释放掉所有使用的内存,并释放TLS索引m_tlsIndex,移除临界区对象m_cs
*/
CThreadSlotData::~CThreadSlotData()
{
// 直接 DeleteValues(NULL, TRUE); 不就行了
	// 从链表获取CThreadData指针,遍历链表执行删除操作 
	CThreadData* pThreadMember = m_list;
	while (pThreadMember != NULL)
	{
		// 使用NULL参数调用DeleteValues函数,将所有线程TLS的所有数据删除
		// 删除线程局部存储CThreadData对象的工作由DeleteValues函数完成了
		CThreadData* pDataNext = pThreadMember->pNext;
		DeleteValues(pThreadMember, NULL);
		pThreadMember = pThreadMember->pNext;
	}

	// 删除wTLS索引值
	if (m_tlsIndex != (DWORD)-1)
		::TlsFree(m_tlsIndex);

	// 释放槽数组
	if (m_pSlotData != NULL)
	{
		// 前2行为什么不直接使用“::GlobalUnlock(m_pSlotData);”?
		HGLOBAL hSlotData = ::GlobalHandle(m_pSlotData);
		::GlobalUnlock(hSlotData);
		::GlobalFree(m_pSlotData);
	}
	
	// 删除临界区对象
	::DeleteCriticalSection(&m_cs);
}
CThreadLocal 类模板

CThreadSlotData类没有实现为用户使用的数据分配存储空间的功能。这功能由CThreadLocal 类实现。

CThreadLocal 类是最终提供给用户的类模板。

要实现的功能:

1、在进程堆中,为每个使用线程私有变量的线程申请内存空间。

2、将上面申请的内存空间的首地址与各线程对象关联起来。

设计1

保存内存地址是一项独立的工作,另外封装一个类来完成。

_AFXTLS_.h

class CThreadLocalObject
{
private:
	DWORD m_nSlot; // 使用CThreadSlotData类分配的槽号

public:
	// 用于获得保存在TLS中的指针
	// 参数是一个函数指针,指针指向的无参函数返回CNoTrackObject* 
	CNoTrackObject* GetData(CNoTrackObject* (*pfnCreateObject)());	// 传入一个构造函数,如果TLS中没有数据,就用构造函数进行创建,并返回刚构造的对象。肯定能返回有效值
	CNoTrackObject* GetDataNA();	// 和上面的函数目的一样,但如果TLS中没有数据,就直接返回NULL

	~CThreadLocalObject();
};

1个成员变量,用于记录槽索引号;

2个成员函数,通过槽索引,获取指向实际存储的Data数据的指针。

_AFXTLS_.cpp

CNoTrackObject* CThreadLocalObject::GetData(CNoTrackObject* (*pfnCreateObject)())
{
// 如果还未获得索引值,就申请一个未使用的槽索引值,并用pfnCreateObject所指的构造函数生成一个对象,存入TLS中
	if (m_nSlot == 0) // 还未获得槽索引值
	{
		// 生成全局的CThreadSlotData对象,如果还未生成
		if (_afxThreadData == NULL)
			_afxThreadData = new(__afxThreadData)CThreadSlotData;
		// 获取一个槽索引值
		m_nSlot = _afxThreadData->AllocSlot();
	}

	// 用槽索引获取线程局部存储
	// TLS的数据类型为CNoTrackObject类的派生类对象
	CNoTrackObject* pValue = (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);

	// 如果TLS还未存有数据,调用pfnCreateObject所指的构造函数生成一个对象
	// 如果前一个if语句执行了,这一个if也一定会执行
	if (pValue == NULL)
	{
		pValue = (*pfnCreateObject)();

		// 将新创建的对象的指针存入TLS
		_afxThreadData->SetValue(m_nSlot, pValue);
	}
	// 返回类对象
	return pValue;
}

CNoTrackObject* CThreadLocalObject::GetDataNA()
{
	// 还未获得槽索引值 || 还未生成全局CThreadSlotData对象,返回空
	if (m_nSlot == 0 || _afxThreadData == 0)
		return NULL;
	// 获取线程局部存储
	return (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
}

CThreadLocalObject::~CThreadLocalObject()
{
	if (m_nSlot != 0 && _afxThreadData != NULL)
		_afxThreadData->FreeSlot(m_nSlot);
	m_nSlot = 0;
}

CThreadLocalObject类对象也是作为全局对象使用,全局对象的所有成员自动初始化为0,不需要显式的构造函数。

设计2

CThreadLocal ,提供为线程私有变量申请内存空间的函数,还要能进行类型转化。

_AFXTLS_.h

template<class TYPE>
class CThreadLocal :public CThreadLocalObject
{
public:
	static CNoTrackObject* CreateObject() { return new TYPE; }

	// 直接返回父类同名函数的调用结果
	TYPE* GetData()
	{
		// 函数的返回类型为CNoTrackObject*,期待TYPE为CNoTrackObject类的派生类
		TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
		// 返回的是一个类TYPE对象的指针
		return pData;
	}
	TYPE* GetDataNA()
	{
		TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA();
		return pData;
	}
	operator TYPE* () { return GetData(); }
	TYPE* operator->() { return GetData(); }
};
用例
#include<iostream>
#include<process.h>
#include "_AFXTLS_.h"

using namespace std;

struct CTest :public CNoTrackObject
{
	int nData;
};

void ShowData();

CThreadLocal<CTest> g_testData;

UINT __stdcall ThreadFunc(LPVOID lpParam)
{
/*
* CThreadLocal类重载了“*”和“->”运算符,返回的结果都是 模板* 参数类型的指针,即TYPE*。本例中是CTest*
*/
	g_testData->nData = (int)lpParam;
	ShowData();
	return 0;
}

int main()
{
	HANDLE h[10];
	UINT uID;

	for (int i = 0; i < 10; ++i)
		h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, (void*)i, 0, &uID);
	::WaitForMultipleObjects(10, h, TRUE, INFINITE);
	for (int i = 0; i < 10; ++i)
		::CloseHandle(h[i]);

	return 0;
}

void ShowData()
{
	int nData = g_testData->nData;
	cout << "Thread ID : " << ::GetCurrentThreadId() << "  nData : " << nData << endl;
}
总结
windows库的TLS

以一个索引号为线索,通过系统调用存取一个 4Byte 大小的值。

这个 4Byte 大小的值可以是数据本身,也可以是指针、指针的指针……以此存储更大的数据。

不同线程之间唯一的联系就是那个相同的索引号

自己的理解:如果TLS存的是指针,则需要有一个全局数组,数组元素就是指针。然后在创建线程的函数中传入数组下标,这样就能通过一个索引号为各个线程获取不同的指针了。

那么指针所指的数据结构呢?也要分配一个数组,数组的下标一一对应?

如果保存了指针,则指针所指向的内存需要用户分配和释放。

自己的TLS

设计线程类 CWinThread

将控制线程工作的API函数和线程自身的属性封装成类,就能得到一个线程类。

就目前的知识而言,一个线程类应该具有以下内容:

  • 构造函数的工作应该包括线程类中所有成员变量的初始化和线程创建。
  • 保存和设置线程特有的属性。一个线程基本的属性有:内核对象句柄、线程ID号、优先级等。
  • 对线程的操作。包括挂起和唤醒操作等。

_AFXSTATE类

一个进程中可能存在多个活动的线程,各个线程的状态是不同的。这就要求将这些差别组成一个数据结构维护起来,AFX_MODULE_THREAD_STATE类用来维护各线程的状态。

因为应用程序中需要维护的状态很多,所以将类库中声明各种状态信息的代码放在单独的头文件中。

_AFXSTATE.h

class CWinThread;

class AFX_MODULE_THREAD_STATE : public CNoTrackObject
{
public:
	CWinThread* m_pCurrentWinThread;
};

extern thread_local AFX_MODULE_THREAD_STATE _afxModuleThreadState;
AFX_MODULE_THREAD_STATE* AfxGetModuleThreadState();

_AFXSTATE.cpp

AFX_MODULE_THREAD_STATE* AfxGetModuleThreadState()
{
	return &_afxModuleThreadState;
}
thread_local AFX_MODULE_THREAD_STATE _afxModuleThreadState;

定义了_afxModuleThreadState线程局部变量历各线程的状态,AfxGetModuleThreadState函数用于获取当前线程中AFX_MODULE_THREAD_STATE类的指针。

实际上AFX_MODULE_THREAD_STATE类的成员会更多,在以后的程序中会逐渐添加新成员。

_AFXWIN类

不直接将用户要求的线程函数地址直接传递给API,而是传递给_AfxThreadEntry函数作为线程函数,此函数完成初始化工作后再调用用户传递的线程函数执行用户代码。

_AFXWIN_.h

typedef UINT(__cdecl* AFX_THREADPROC)(LPVOID);

class CWinThread
{
public:
	// 指示线程结束后,是否销毁此对象
	BOOL m_bAutoDelete;

	// 线程对象属性
	HANDLE m_hThread;
	DWORD m_hThreadID;

	// 保存创建函数的参数
	LPVOID m_pThreadParams;
	AFX_THREADPROC m_pfnThreadProc;

public:
	CWinThread();
	CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam);
	BOOL CreateThread(DWORD dwCreateFlags = 0, UINT nStackSize = 0,
		LPSECURITY_ATTRIBUTES lpSecurity = NULL);

	virtual ~CWinThread();
	virtual void Delete();
	void CommonConstruct();

	// 保存和设置线程对象的属性
public:
	operator HANDLE() const { return this == NULL ? NULL : m_hThread; }

	int GetThreadPriority() { return ::GetThreadPriority(m_hThread); }
	BOOL SetThreadPriority(int nPriority) { return ::SetThreadPriority(m_hThread, nPriority); }

	DWORD SuspendThread() { return ::SuspendThread(m_hThread); }
	DWORD ResumeThread() { return ::ResumeThread(m_hThread); }
};

UINT __stdcall _AfxThreadEntry(void* pParam);
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,
	int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,
	DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
CWinThread* AfxGetThread();
void AfxEndThread(UINT nExitCode, BOOL bDelete = TRUE);


class AFX_MODULE_THREAD_STATE : public CNoTrackObject
{
public:
	CWinThread* m_pCurrentWinThread;
};

AFX_MODULE_THREAD_STATE* AfxGetModuleThreadState();

_AFXWIN_.cpp

struct _AFX_THREAD_STARTUP
{
	CWinThread* pThread;
	HANDLE hEvent1;
	HANDLE hEvent2;
	BOOL bError;
};


UINT __stdcall _AfxThreadEntry(void* pParam)
{
	_AFX_THREAD_STARTUP* pStartup = (_AFX_THREAD_STARTUP*)pParam;
	CWinThread* pThread = pStartup->pThread;
	try
	{
		AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
		pState->m_pCurrentWinThread = pThread;
	}
	catch (...)
	{
		pStartup->bError = TRUE;
		::SetEvent(pStartup->hEvent1);
		AfxEndThread((UINT)-1, FALSE);
	}

	HANDLE hEvent2 = pStartup->hEvent2;

	::WaitForSingleObject(hEvent2, INFINITE);
	::CloseHandle(hEvent2);

	DWORD nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);

	AfxEndThread(nResult);
	return 0;
}

CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority, UINT nStackSize, DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs)
{
	CWinThread* pThread = new CWinThread(pfnThreadProc, pParam);

	if (!pThread->CreateThread(dwCreateFlags | CREATE_SUSPENDED, nStackSize, lpSecurityAttrs))
	{
		pThread->Delete();
		return NULL;
	}

	pThread->SetThreadPriority(nPriority);

	if (!(dwCreateFlags & CREATE_SUSPENDED))
		pThread->ResumeThread();

	return pThread;
}

CWinThread* AfxGetThread()
{
	AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
	return pState->m_pCurrentWinThread;
}

void AfxEndThread(UINT nExitCode, BOOL bDelete)
{
	AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
	CWinThread* pThread = pState->m_pCurrentWinThread;
	if (pThread != NULL)
	{
		if (bDelete)
			pThread->Delete();
		pState->m_pCurrentWinThread = NULL;
	}

	if (_afxThreadData != NULL)
		_afxThreadData->DeleteValues(NULL, FALSE);

	_endthreadex(nExitCode);
}


AFX_MODULE_THREAD_STATE* AfxGetModuleThreadState()
{
	return _afxModuleThreadState->GetData();
}


void CWinThread::CommonConstruct()
{
	m_hThread = NULL;
	m_hThreadID = 0;
	m_bAutoDelete = TRUE;
}

CWinThread::CWinThread()
	:m_pThreadParams(NULL), m_pfnThreadProc(NULL)
{
	CommonConstruct();
}

CWinThread::CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam)
	: m_pThreadParams(pParam), m_pfnThreadProc(pfnThreadProc)
{
	CommonConstruct();
}

BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize, LPSECURITY_ATTRIBUTES lpSecurity)
{
	_AFX_THREAD_STARTUP startup;
	memset(&startup, 0, sizeof(startup));
	startup.pThread = this;
	startup.hEvent1 = ::CreateEvent(NULL, TRUE, FALSE, NULL);
	startup.hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL);

	m_hThread = (HANDLE)_beginthreadex(lpSecurity, nStackSize, &_AfxThreadEntry, &startup, dwCreateFlags | CREATE_SUSPENDED, (UINT*)&m_hThreadID);
	if (m_hThread == NULL)
		return FALSE;

	ResumeThread();
	::WaitForSingleObject(startup.hEvent1, INFINITE);
	::CloseHandle(startup.hEvent1);

	if (dwCreateFlags & CREATE_SUSPENDED)
		::SuspendThread(m_hThread);

	if (startup.bError)
	{
		::WaitForSingleObject(m_hThread, INFINITE);
		::CloseHandle(m_hThread);
		m_hThread = NULL;
		::CloseHandle(startup.hEvent2);
		return FALSE;
	}

	::SetEvent(startup.hEvent2);
	return TRUE;
}

CWinThread::~CWinThread()
{
	if (m_hThread != NULL)
		::CloseHandle(m_hThread);

	AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
	if (pState->m_pCurrentWinThread == this)
		pState->m_pCurrentWinThread = NULL;
}

void CWinThread::Delete()
{
	if (m_bAutoDelete)
		delete this;
}

有了CWinThread类后,创建额外线程很方便,不用关心线程类对象的创建和销毁,不用关闭线程内核对象句柄,这些都由CWinThread完成了。

--------------代码--------------

在API函数前加::符号,标识这是一个全局函数。

::MessageBos();

Windows API 的命名方式

使用的是匈牙利命名法,例如

lp ——long pointer;

b —— BOOL;

sz —— string zero。

只记录结构和文档地址

对于书中描述的结构和函数,在windows文档中好像都分有A/W两种版本,A版本与书中相同,而在VS的实践中,默认(不加A/W后缀)是使用W版本。

后面跟有“x”的表示已被微软弃用,有更好的替代。

-------数据结构-------

2、

STARTINFO

A/W版本

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa

typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

PROCESS_INFORMATION

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_information

typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;
  HANDLE hThread;
  DWORD  dwProcessId;
  DWORD  dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

PROCESSENTRY32

https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/ns-tlhelp32-processentry32

W版本,最后一个成员CHAR变为WCHAR

VS实践:其实只剩W版本了,头文件中有如下代码:

#define PROCESSENTRY32 PROCESSENTRY32W

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      szExeFile[MAX_PATH];
} PROCESSENTRY32;

OSVERSIONINFO x

https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfow

A/W版本:CHARWCHAR

typedef struct _OSVERSIONINFOW {
  DWORD dwOSVersionInfoSize;
  DWORD dwMajorVersion;
  DWORD dwMinorVersion;
  DWORD dwBuildNumber;
  DWORD dwPlatformId;
  WCHAR szCSDVersion[128];
} OSVERSIONINFOW, *POSVERSIONINFOW, *LPOSVERSIONINFOW, RTL_OSVERSIONINFOW, *PRTL_OSVERSIONINFOW;

3、

SECURITY_ATTRIBUTES

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

-------函数-------

2、

CreateProcess

A/W版本

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

CreateToolhelp32Snapshot

https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot

HANDLE CreateToolhelp32Snapshot(
  DWORD dwFlags,
  DWORD th32ProcessID
);

Process32First

https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32first

都有W版本

BOOL Process32First(
  HANDLE           hSnapshot,
  LPPROCESSENTRY32 lppe
);

Process32Next

https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next

BOOL Process32Next(
  HANDLE           hSnapshot,
  LPPROCESSENTRY32 lppe
);

ExitProcess

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess

void ExitProcess(
  UINT uExitCode
);

TerminateProcess

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess

BOOL TerminateProcess(
  HANDLE hProcess,
  UINT   uExitCode
);

OpenProcess

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);

dwDesiredAccess的取值:决定了能通过该句柄对那个进程做什么事。

https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights

GetExitCodeProcess

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess

BOOL GetExitCodeProcess(
  HANDLE  hProcess,
  LPDWORD lpExitCode
);

GetLastError

https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror

_Post_equals_last_error_ DWORD GetLastError();

ReadProcessMemory

https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory

BOOL ReadProcessMemory(
  HANDLE  hProcess,
  LPCVOID lpBaseAddress,
  LPVOID  lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesRead
);

WriteProcessMemory

BOOL WriteProcessMemory(
  HANDLE hProcess,
  LPVOID lpBaseAddress,
  LPVOID lpBuffer,
  DWORD nSize,
  LPDWORD lpNumberOfBytesWritten
);

GetVersionEx x

https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw

A/W版本

NOT_BUILD_WINDOWS_DEPRECATE BOOL GetVersionExW(
  LPOSVERSIONINFOW lpVersionInformation
);

补充:这一函数被微软认为是不安全的,微软推荐使用Version Helper functions

https://docs.microsoft.com/en-us/windows/win32/sysinfo/version-helper-apis

3、

CreateThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

WaitForSingleObject

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject

DWORD WaitForSingleObject(
  HANDLE hHandle,
  DWORD  dwMilliseconds
);

OpenThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthread

HANDLE OpenThread(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwThreadId
);

dwDesiredAccess的取值:

https://docs.microsoft.com/en-us/windows/win32/procthread/thread-security-and-access-rights

ResumeThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-resumethread

DWORD ResumeThread(
  HANDLE hThread
);

SuspendThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-suspendthread

DWORD SuspendThread(
  HANDLE hThread
);

GetExitCodeThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodethread

BOOL GetExitCodeThread(
  HANDLE  hThread,
  LPDWORD lpExitCode
);

ExitThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitthread

void ExitThread(
  DWORD dwExitCode
);

TerminateThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminatethread

BOOL TerminateThread(
  HANDLE hThread,
  DWORD  dwExitCode
);

SetThreadPriority

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority

BOOL SetThreadPriority(
  HANDLE hThread,
  int    nPriority
);

WaitForMultipleObjects

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects

DWORD WaitForMultipleObjects(
  DWORD        nCount,
  const HANDLE *lpHandles,
  BOOL         bWaitAll,
  DWORD        dwMilliseconds
);

_beginthreadex

https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-160

uintptr_t _beginthreadex( // NATIVE CODE
   void *security,
   unsigned stack_size,
   unsigned ( __stdcall *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr
);

_endthreadex

https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/endthread-endthreadex?view=msvc-160

void _endthreadex(
   unsigned retval
);

InitializeCriticalSection

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-initializecriticalsection

void InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

EnterCriticalSection

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-entercriticalsection

void EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

LeaveCriticalSection

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-leavecriticalsection

void LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

DeleteCriticalSection

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-deletecriticalsection

void DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

InterlockedIncrement

https://docs.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-interlockedincrement

LONG InterlockedIncrement(
  LONG volatile *Addend
);

InterlockedDecrement

https://docs.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-interlockeddecrement

LONG InterlockedDecrement(
  LONG volatile *Addend
);

CreateEvent

A/W版本

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventw

HANDLE CreateEventW(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL                  bManualReset,
  BOOL                  bInitialState,
  LPCWSTR               lpName
);

OpenEvent

A/W版本

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-openeventw

HANDLE OpenEventW(
  DWORD   dwDesiredAccess,
  BOOL    bInheritHandle,
  LPCWSTR lpName
);

SetEvent

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setevent

BOOL SetEvent(
  HANDLE hEvent
);

ResetEvent

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-resetevent

BOOL ResetEvent(
  HANDLE hEvent
);

TlsAlloc

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsalloc

DWORD TlsAlloc();

TlsSetValue

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlssetvalue

BOOL TlsSetValue(
  DWORD  dwTlsIndex,
  LPVOID lpTlsValue
);

TlsGetValue

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsgetvalue

LPVOID TlsGetValue(
  DWORD dwTlsIndex
);

TlsFree

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsfree

BOOL TlsFree(
  DWORD dwTlsIndex
);

GlobalAlloc

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc

DECLSPEC_ALLOCATOR HGLOBAL GlobalAlloc(
  UINT   uFlags,
  SIZE_T dwBytes
);

GlobalLock

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock

LPVOID GlobalLock(
  HGLOBAL hMem
);

GlobalUnlock

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock

BOOL GlobalUnlock(
  HGLOBAL hMem
);

GlobalFree

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree

HGLOBAL GlobalFree(
  _Frees_ptr_opt_ HGLOBAL hMem
);

GlobalHandle

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalhandle

HGLOBAL GlobalHandle(
  LPCVOID pMem
);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值