Featured image of post Win32

Win32

Win32Microsoft Windows 操作系统的核心应用程序编程接口(API),用于开发在 Windows 平台上运行的应用程序

Win32 编码

Win32 字符编码体系

编码类型 宽字符 (Unicode) 多字节字符 (ANSI)
字符大小 2字节(wchar_t) 1字节(char)
编码标准 UTF-16(小端序) 本地代码页(如GBK, Shift-JIS)
API 后缀 W(如 MessageBoxW A(如 MessageBoxA
前缀标识 L(如 L"文本" 无(如 "文本"
适用场景 现代 Windows 应用(Win2000+) 兼容旧系统(Win9x)
推荐指数 ★★★★★(微软官方推荐) ★☆☆☆☆(已淘汰)

历史演进与技术替代

技术 出现时间 字符编码 现状
Win16 API 1985 ANSI 完全淘汰
Win32 API (A) 1993 ANSI 遗留系统维护
Win32 API (W) 2000 UTF-16 主流使用
WinRT/UWP 2012 UTF-16 (兼容UTF-8) 新一代应用

宽字符 Unicode

Unicode编码创建了一张包含世界上所有文字的编码表,只要世界上存在的文字符号,都会赋予一个唯一的编码。

Unicode编码的范围是:0x0-0x10FFFF,其可以容纳100多万个符号,但是Unicode本身也存在问题,因为Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何去存储。

Unicode存储的实现方式

  • UTF-8, UTF-16, UTF-32:它们就是将 Unicode 码点转换成计算机字节序列的具体规则。同一个 Unicode 码点,用这三种编码方式存储,得到的字节序列是不同的。
特性 UTF-8 UTF-16 UTF-32
编码单元 1 字节 (8位) 2 字节 (16位) 4 字节 (32位)
长度 变长 (1-4字节) 变长 (2或4字节) 定长 (4字节)
BOM标记 可选 (EF BB BF) 必需 (FE FF 或 FF FE) 可选 (00 00 FE FF)
对ASCII的兼容性 完全兼容 不兼容 不兼容
空间效率 对西文最高,对东亚文中等 对东亚文最高,对西文中等 最低,非常浪费空间
处理效率 长度不固定,计算字符数和索引较慢 多数常用字符定长,处理较快 定长,计算字符数和索引最快
字节序(Endianness)问题 (需BOM区分) (需BOM区分)
应用场景 互联网、Web、操作系统文件系统 Windows内部、Java、.NET 学术研究、内存中临时处理
  1. UTF-8 (8-bit Unicode Transformation Format)

这是目前互联网上使用最广泛的Unicode编码方式。

  • 核心思想: 用“前缀位”来表示一个字符需要用几个字节。
  • 规则:
    • 单字节0xxxxxxx。用于表示所有 ASCII 字符 (U+0000 到 U+007F)。这也是它完美兼容ASCII的原因。
    • 双字节110xxxxx 10xxxxxx。用于表示一些拉丁文、希腊文等。
    • 三字节1110xxxx 10xxxxxx 10xxxxxx。用于表示绝大多数常用汉字等中日韩字符。
    • 四字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。用于表示不常用的字符和Emoji表情等。
  1. UTF-16 (16-bit Unicode Transformation Format)

在很多系统内部,尤其是 Windows 和 Java,这是首选的编码方式。

  • 核心思想: 尽量用2个字节表示一个字符,实在不行再用4个。
  • 规则:
    • 对于码点在 U+0000U+FFFF 之间的字符(这个范围包含了几乎所有常用字符,包括大部分汉字),直接用2个字节存储。
    • 对于码点超出 U+FFFF 的字符(如Emoji),使用代理对 (Surrogate Pair) 的方式,用4个字节来表示。
  1. UTF-32 (32-bit Unicode Transformation Format)

最简单直接,但也最浪费空间的编码方式。

  • 核心思想: 不管什么字符,一律用4个字节(32位)来表示。
  • 规则: 直接将Unicode码点的值用4个字节存储。

C语言中的宽字符

1.宽字符使用

“中”字编码:

ASCII:d6 d0

UNICODE(UTF-16):4e 2d

数据类型 宽字符版本 多字节版本
字符类型 wchar_t char
字符串指针 LPCWSTR (const wchar_t*) LPCSTR (const char*)
可变字符串 LPWSTR (wchar_t*) LPSTR (char*)
通用字符类型 TCHAR(根据配置变化)
通用字符串宏 LPTSTR, LPCTSTR

字符和字符串

1
2
3
4
5
char x = '中';	//使用拓展ASCII编码
wchar_t x1 = L'中';	//L指宽字符Unicode编码

char x[] = "中国";    //A
wchar_t x1[] = L"中国";  //W

2.打印宽字符

包含头文件#include <locale.h>

主函数中添加

1
2
3
setlocale(LC_ALL, "");	//默认本地操作系统地域
printf("%s\n", x);	//使用控制台默认编码
wprintf(L"%s\n", x1);	//默认使用英文,可通过setlocale修改地域

3.字符串长度

头文件#include <string.h>

1
2
strlen(x);	//取得多字符编码字符串长度
wcslen(x1);	//取得宽字符编码字符串长度

4.字符串操作

1
2
3
4
5
6
char x[] = "china";
char x1[] = "123";
strcpy(x, x1);
wchar_t y[] = L"中国";
wchar_t y1[] = L"好";
wcscpy(y, y1);

还有wcscatwcscmpwcsstr

Win32 API

API(Application Programming Interface,应用程序编程接口)

什么是Win32 API

Win32 APIWindows 操作系统底层的应用程序编程接口,为开发者提供直接访问操作系统核心功能的途径,其主要存放在C:\Windows\System32(存储的DLL是64位)、C:\Windows\SysWOW64(存储的DLL是32位)下面的所有DLL文件(几千个)

特性 说明
接口类型 过程化 C 语言 API(非面向对象)
架构层级 用户模式与内核模式间的桥梁
实现方式 通过 DLL 文件暴露函数(如 kernel32.dll, user32.dll)
历史地位 Windows 95 至今的核心 API(Win64 是其在 64 位系统的扩展)
不可替代性 所有 Windows 应用框架(.NET/WinUI)最终都调用 Win32 API

Win32 API 核心组件架构

关键功能DLL

通过这些dll(三环)调用内核函数

  1. 系统服务(Kernel32.dll)
  • 进程/线程:CreateProcess(), CreateThread()
  • 内存管理:VirtualAlloc(), HeapAlloc()
  • 文件操作:CreateFile(), ReadFile()
  • 同步对象:CreateMutex(), WaitForSingleObject()
  1. 用户界面(User32.dll)
  • 窗口管理:CreateWindowEx(), ShowWindow()
  • 消息循环:GetMessage(), DispatchMessage()
  • 控件操作:CreateButton(), SetWindowText()
  • 资源管理:LoadIcon(), LoadString()
  1. 图形设备(GDI32.dll)
  • 绘图:LineTo(), Rectangle()
  • 文本:TextOut(), DrawText()
  • 位图:CreateBitmap(), BitBlt()
  • 设备上下文:GetDC(), ReleaseDC()
  1. 高级服务
  • 注册表:RegOpenKeyEx(), RegSetValueEx()(Advapi32.dll)
  • 网络:WSAStartup(), socket()(Ws2_32.dll)
  • COM 组件:CoInitialize(), CoCreateInstance()(Ole32.dll)
  • 安全:CryptAcquireContext(), CryptEncrypt()(Crypt32.dll)

Win32 API编码

Windows是使用C语言开发的,Win32 API同时支持宽字符和多字节字符

Windows下所有内核函数,涉及字符串均使用宽字节

Windows提供的类型如下:(包含头文件Window.h

1.字符类型

1
2
3
4
5
6
7
8
C	-- Win32
char -- CHAR	//F12可跟进查看类型
wchar_t -- WCHAR
 -- TCHAR	//若当前项目设置为ASCII编码,则相当于CHAR;若为Unicode编码,则相当于WCHAR,在Win32中推荐使用
	//用法
CHAR cha[] = "中国";
WCHAR chw[] = L"中国";
TCHAR cht[] = TEXT("中国");

2.字符串指针

1
2
3
4
5
6
7
PSTR(LPSTR)	//指向多字节字符串
PWSTR(LPWSTR)	//指向宽字符串
 -- PTSTR(LPTSTR)
	//用法
PSTR pszC = "China";
PWSTR pszWC = L"china";
PTSTR pszTC = TEXT("china");

Windows提供的API,若需要传递字符串参数,都会提供两个版本和一个宏,如:

1
2
3
MessageBoxA(0, "多字节", "标题", MB_OK);
MessageBoxW(0, L"宽字符", L"标题", MB_OK);
MessageBox(0, TEXT("内容"), TEXT("标题"), MB_OK);

从本质上来讲,Windows字符串都是宽字符的,所以使用MessageBoxW这种方式性能会更好一些,因为当你使用MessageBoxA的时候,在到内核的时候(系统底层)其会转化Unicode,弹窗调用如下:

1
2
3
WCHAR strTitle[] = L"Title";
WCHAR strContent[] = L"Hello World!";
MessageBoxW(0, strContent, strTitle, MB_OK);

Win32入口程序

VS创建项目Windows桌面应用程序

wWinMainWindows 图形用户界面(GUI)应用程序的入口点,它相当于 C/C++ 控制台程序中的 main 函数

当你在 Windows 系统上双击一个 .exe 文件的图标时,操作系统加载完程序后,第一个调用的就是这个函数(或者它的非宽字符版本 WinMain),函数签名

1
2
3
4
5
6
int APIENTRY wWinMain(
    _In_     HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_     LPWSTR    lpCmdLine,
    _In_     int       nCmdShow
)
  • APIENTRY: 这是一个宏,通常被定义为 __stdcall,这是所有标准 Win32 API 函数使用的约定,所以你的入口点也必须遵守
  • wWinMain:“Windows Main Function”
    • 前缀 w 代表 Wide-character(宽字符),这意味着这个版本的入口点接收的是**宽字符(UTF-16)**格式的命令行参数。与之对应的是 WinMain(没有w),它接收的是 ANSI(多字节)字符
  • HINSTANCE: Handle to an Instance(实例句柄)
    • 句柄 (Handle):在Windows中,句柄是一个唯一的数字,它代表了操作系统管理的某个资源(如窗口、文件、进程等)
    • 实例句柄 (Instance Handle)hInstance 是一个指向你程序被加载到内存中的那个模块 (module) 的句柄,通常,一个 .exe 文件就是一个模块。这个句柄非常重要,因为操作系统用它来唯一标识你的应用程序实例。
    • 用途:你会在很多后续的 Win32 API 调用中用到它,比如加载图标、光标、字符串资源,或者创建窗口时告诉系统这个窗口属于哪个程序实例。
  • HINSTANCE hPrevInstance: Handle to a Previous Instance(前一个实例的句柄)。
    • 历史遗留物:在古老的16位Windows时代,这个参数用来判断是否已经有同一个程序的实例在运行。如果有,hPrevInstance 会是一个有效值。
    • 在现代32位和64位Windows中,这个参数永远是 NULL (0)。因为现代操作系统为每个程序提供了独立的地址空间,一个程序无法轻易地知道另一个实例的存在。如果你需要实现“单实例”应用程序(即只允许程序运行一个),你需要使用其他更现代的方法,比如互斥体 (Mutex)
    • 结论:你可以完全忽略这个参数。
  • lpCmdLine: 这个参数包含了传递给程序的命令行参数
    • 例如,如果你在命令行中运行 my_program.exe /config "settings.xml" -debug,那么 lpCmdLine 指向的字符串就是 "/config \"settings.xml\" -debug"
    • 注意:它不包含程序本身的名称。如果你需要获取程序自己的路径,应该使用 GetModuleFileName 函数。
  • int nCmdShow: 这个参数是一个整数,指定了程序的主窗口应该以何种状态显示
    • 这个值是由启动你的程序的那个进程(比如 Windows 资源管理器)传递过来的。
    • 常见的值:
      • SW_SHOWNORMAL: 正常显示窗口(不最大化也不最小化)。
      • SW_SHOWMAXIMIZED: 最大化显示。
      • SW_SHOWMINIMIZED: 最小化到任务栏。
    • 如何使用:在你创建完主窗口后,你应该将这个 nCmdShow 值传递给 ShowWindow 函数,作为它的第二个参数。这是为了尊重启动者的意图。例如,如果用户在另一个程序的快捷方式属性里设置了“以最大化方式运行”,那么操作系统就会通过 nCmdShow 告诉你应该这么做。

wWinMain 的典型结构

一个经典的 wWinMain 函数内部通常会做以下几件事:

  1. 注册窗口类 (Register Window Class):使用 WNDCLASSEX 结构体定义窗口的各种属性(如窗口过程函数、图标、光标、背景颜色等),然后调用 RegisterClassEx 告诉操作系统“我要创建一种这样的窗口”。
  2. 创建窗口 (Create Window):调用 CreateWindowEx 函数,使用上一步注册的窗口类来创建一个具体的窗口实例。
  3. 显示和更新窗口 (Show and Update Window):调用 ShowWindow(并传入 nCmdShow)和 UpdateWindow,让窗口显示在屏幕上并绘制其内容。
  4. 进入消息循环 (Message Loop):
    • 这是一个 while 循环,不断调用 GetMessage 从程序的消息队列中获取消息(如鼠标点击、键盘按键、窗口重绘请求等)。
    • 调用 TranslateMessage 进行一些键盘消息的转换。
    • 调用 DispatchMessage 将消息分发给对应的窗口过程函数(WndProc)去处理。
  5. 循环结束,返回:当 GetMessage 收到 WM_QUIT 消息时,它会返回 FALSE,循环结束,wWinMain 函数返回,程序退出。
定位入口程序

这里以win32程序(Debug x86)为例,F8步进,运行到图片光标处,F7步入

F701

F702

此为真正入口函数,可看到push了4个参数

F703

F7步入,观察函数调用压栈后的堆栈,如图0x010FF6F0内容为函数返回地址,而后F6F4~F700内容分别为WinMain的四个参数:句柄、NULL、命令行参数地址、以何种状态显示

入口函数堆栈

ESP寻址

这里以Release版为例(Debug版的太丑了,看不到esp)

提升堆栈

返回地址[esp+80]、第一个参数[esp+84]?不错,我们可以双击堆栈中esp的值,这样就可以自动定位啦

esp定位工具

找到刚刚返回地址,验证成功

esp寻址

麻烦的是,随着程序的运行,esp的值很可能会改变,这时这个自动定位的用处就大大体现出,想要得到esp定位的真正值,还得运行到要定位的语句才行

Win32程序中调试信息

简单模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>

// 使用 OutputDebugString 输出调试信息
void PrintDebugMessage(LPCWSTR message) {
    OutputDebugString(message);  // 在调试器输出窗口显示
    // OutputDebugString(L"Debug message\n"); // 示例
}

// 带格式化输出
void PrintDebugFormat(LPCWSTR format, ...) {
    wchar_t buffer[256];
    va_list args;
    va_start(args, format);
    vswprintf_s(buffer, 256, format, args);
    va_end(args);
    OutputDebugString(buffer);
}

// 使用示例:
PrintDebugFormat(L"鼠标位置: X=%d, Y=%d\n", xPos, yPos);

安全模板

outputstr.h

1
2
3
4
5
#include <Windows.h>
#include <cstdarg>
#include <string>
void __cdecl outputStringF(_In_z_ _Printf_format_string_ const char* format, ...);
void __cdecl outputStringWF(_In_z_ _Printf_format_string_ const wchar_t* format, ...);

output.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
#include "outputstr.h"
void __cdecl outputStringF(_In_z_ _Printf_format_string_ const char* format, ...)
{
    // 使用栈内存避免分配开销(小消息更高效)
    char stackBuffer[512];
    char* dynamicBuffer = nullptr;
    char* bufferPtr = stackBuffer;
    const size_t maxSize = sizeof(stackBuffer);

    va_list args;
    va_start(args, format);

    // 尝试在栈缓冲区格式化
    int result = _vsnprintf_s(stackBuffer, maxSize, _TRUNCATE, format, args);

    // 若栈空间不足,动态分配内存
    if (result < 0) {
        va_end(args);
        va_start(args, format);

        // 获取实际需要的大小
        result = _vscprintf(format, args);
        if (result <= 0) {
            va_end(args);
            OutputDebugStringA("outputStringF: format error\n");
            return;
        }

        const size_t neededSize = static_cast<size_t>(result) + 2; // +换行符+空终止
        dynamicBuffer = static_cast<char*>(malloc(neededSize));
        if (!dynamicBuffer) {
            va_end(args);
            OutputDebugStringA("outputStringF: memory allocation failed\n");
            return;
        }

        _vsnprintf_s(dynamicBuffer, neededSize, _TRUNCATE, format, args);
        bufferPtr = dynamicBuffer;
    }
    va_end(args);

    // 确保添加换行符(安全版)
    const size_t len = strlen(bufferPtr);
    if (len < (maxSize - 1)) {
        bufferPtr[len] = '\n';
        bufferPtr[len + 1] = '\0';
    }
    else {
        // 若已满则替换最后一个字符
        bufferPtr[maxSize - 2] = '\n';
        bufferPtr[maxSize - 1] = '\0';
    }

    OutputDebugStringA(bufferPtr);

    if (dynamicBuffer) {
        free(dynamicBuffer);
    }
}
void __cdecl outputStringWF(_In_z_ _Printf_format_string_ const wchar_t* format, ...)
{
    wchar_t stackBuffer[512];
    wchar_t* dynamicBuffer = nullptr;
    wchar_t* bufferPtr = stackBuffer;
    const size_t maxSize = _countof(stackBuffer);

    va_list args;
    va_start(args, format);

    int result = _vsnwprintf_s(stackBuffer, maxSize, _TRUNCATE, format, args);

    if (result < 0) {
        va_end(args);
        va_start(args, format);

        result = _vscwprintf(format, args);
        if (result <= 0) {
            va_end(args);
            OutputDebugStringW(L"outputStringWF: format error\n");
            return;
        }

        const size_t neededSize = static_cast<size_t>(result) + 2;
        dynamicBuffer = static_cast<wchar_t*>(malloc(neededSize * sizeof(wchar_t)));
        if (!dynamicBuffer) {
            va_end(args);
            OutputDebugStringW(L"outputStringWF: memory allocation failed\n");
            return;
        }

        _vsnwprintf_s(dynamicBuffer, neededSize, _TRUNCATE, format, args);
        bufferPtr = dynamicBuffer;
    }
    va_end(args);

    const size_t len = wcslen(bufferPtr);
    if (len < (maxSize - 1)) {
        bufferPtr[len] = L'\n';
        bufferPtr[len + 1] = L'\0';
    }
    else {
        bufferPtr[maxSize - 2] = L'\n';
        bufferPtr[maxSize - 1] = L'\0';
    }

    OutputDebugStringW(bufferPtr);

    if (dynamicBuffer) {
        free(dynamicBuffer);
    }
}

使用方法

1
2
outputStringF("Debug message2, %x\n", &wndclass);
outputStringWF(L"Debug message, %x\n", &wndclass);

GetLastError的使用

1
2
MessageBox((HWND)1, 0, 0, 0);
DWORD erro = GetLastError();	//将改代码放在错误语句的后面,在其后设置断点,F5运行到断点,鼠标放在erro上查看错误值

VS提供错误查找工具

错误查找

输入刚刚的错误值查看错误信息

错误-1400

Win32图形界面

事件与消息

在 Windows 中,事件 (Event Object) 是一种内核对象 (Kernel Object),它与进程、线程、互斥体 (Mutex)、信号量 (Semaphore) 处于同一层面

事件特征:

  1. 用途:主要用于线程间的同步(Synchronization)。一个线程可以等待一个事件,而另一个线程可以触发这个事件,从而唤醒等待的线程。
  2. 状态:事件只有两种状态——已触发 (Signaled)未触发 (Non-signaled)
  3. 类型
    • 手动重置事件 (Manual-reset Event):一旦被触发,它会一直保持“已触发”状态,直到有代码明确地调用 ResetEvent 函数将其重置为“未触发”。它可以同时唤醒所有正在等待它的线程。
    • 自动重置事件 (Auto-reset Event):一旦被触发并成功唤醒一个等待的线程后,它会自动变回“未触发”状态。就像一个只能一个人通过的旋转门。
  4. 操作函数CreateEvent, SetEvent (触发), ResetEvent (重置), WaitForSingleObject, WaitForMultipleObjects (等待)。
  5. 可见性:可以是匿名的(只在当前进程内可见),也可以是命名的(可以跨进程共享和访问)

在 Windows 中,消息 (Message)GUI 子系统(USER32) 用来与应用程序窗口进行通信的数据结构,它驱动着整个 Windows 用户界面的交互

消息特征:

  1. 用途:用于通知应用程序发生了某个“有趣的事情”。这些事情大多与用户输入或窗口状态变化有关。
  2. 结构:一个消息通常是一个MSG结构体,包含以下关键信息:
    • hwnd: 消息的目标窗口句柄
    • message: 消息的类型(一个唯一的ID,如 WM_KEYDOWN, WM_LBUTTONDOWN, WM_PAINT)。
    • wParam: 附加信息1(具体含义取决于消息类型)。
    • lParam: 附加信息2(具体含义取决于消息类型)。
  3. 来源
    • 硬件输入:鼠标移动、点击,键盘按下等。操作系统捕获硬件中断,将其转化为消息。
    • 系统通知:窗口需要重绘 (WM_PAINT),窗口被销毁 (WM_DESTROY)。
    • 程序内部:程序可以自己给自己或其他窗口发送/投递消息(使用 SendMessagePostMessage)。
  4. 处理流程 (消息循环):
    • 操作系统将消息放入对应线程的消息队列 (Message Queue) 中。
    • 应用程序的消息循环 (while (GetMessage(...))) 从队列中取出消息。
    • DispatchMessage 将消息派发给目标窗口的窗口过程函数 (WndProc)
    • WndProc 根据消息类型执行相应的处理代码

两者对比

特性 事件 (Event) 消息 (Message)
层面 内核层 (Kernel) GUI/用户层 (USER32)
本质 同步对象 (Synchronization Object) 数据结构 (Data Structure)
目的 线程间同步,协调执行顺序 通知窗口发生了某事,驱动UI响应
状态/内容 只有两种状态:已触发/未触发 包含丰富信息:目标窗口、类型、参数
通信方 线程 vs 线程 (或 进程 vs 进程) 系统/用户 vs 窗口 (或 窗口 vs 窗口)
处理方式 线程通过 WaitFor... 等待,然后继续执行 窗口的 WndProc 被动接收并处理
典型例子 一个线程完成下载后,触发一个事件,通知主线程可以更新UI了 用户点击鼠标,系统向窗口发送 WM_LBUTTONDOWN 消息

MSG结构体

1
2
3
4
5
6
7
8
typedef struct tagMAG {
    HWND hwnd;	//窗口句柄
    UNIT message;	//消息类型
    WPARAM wParam;
    LPARAM lParam;
    DWORD time;	//事件什么时候发生
    POINT pt;	//坐标(鼠标位置)
} MSG, *PMSG;

消息类型

首先,Windows 消息都以 WM_(Window Message)为前缀,这使得它们非常容易辨认。比如 WM_CREATE, WM_PAINT, WM_KEYDOWN

  1. 窗口管理消息 (Window Management Messages)
  2. 用户输入消息 (User Input Messages)
  3. 键盘消息
  4. 鼠标消息
  5. 绘图消息 (Painting Messages)
  6. 控件通知消息 (Control Notification Messages)
  7. 用户自定义消息 (User-Defined Messages)
  8. 剪贴板消息 (Clipboard Messages)

Windows 系统消息队列与应用程序消息队列:事件触发 -> MSG ->系统消息队列 -> 应用消息队列 -> 消息循环 -> 处理消息

消息队列

Win32 GUI

Java GUI 编程和 Win32 GUI 编程在最终目标(创建图形用户界面)上是相似的,但在实现方式、抽象层次、开发体验、跨平台性和底层机制上存在巨大差异,可以说是两种截然不同的范式

  1. 抽象层次与直接性:
    • Win32 GUI: 极其底层。开发者直接调用 Windows 操作系统提供的原生 API(主要是 user32.dllgdi32.dll)。你需要:

      • 手动注册窗口类 (RegisterClassEx)。
      • 手动创建窗口 (CreateWindowEx)。
      • 编写庞大的消息循环 (GetMessage, TranslateMessage, DispatchMessage)。
      • 在复杂的窗口过程 (WndProc) 函数中处理大量的 Windows 消息 (WM_CREATE, WM_PAINT, WM_COMMAND, WM_SIZE, WM_DESTROY 等)。
      • 直接管理窗口句柄 (HWND)、设备上下文 (HDC)、画笔 (HPEN)、画刷 (HBRUSH) 等底层资源。
      • 显式处理资源释放和内存管理。
    • Java GUI (Swing/JavaFX): 高度抽象。开发者使用 Java 提供的 GUI 工具包
      • Swing: 基于 AWT,提供了一套纯 Java 实现的、可插拔外观的组件 (JFrame, JButton, JTextField, JTable 等)。开发者主要操作对象 (JButton btn = new JButton("Click Me")) 和事件监听器 (btn.addActionListener(...))。Swing 内部处理了与原生系统的交互。
      • JavaFX: 更现代、功能更强大的 GUI 工具包,同样基于对象 (Stage, Scene, Button, TextField, TableView 等)。它使用场景图 (Scene Graph) 模型、CSS 样式、FXML 声明式布局、丰富的图形/媒体/3D 支持,以及绑定 (Binding) 等高级特性。开发者主要关注业务逻辑和界面描述,底层渲染细节由 JavaFX 引擎处理(可以使用硬件加速)。
  2. 事件处理模型:
    • Win32 GUI: 基于消息传递。所有用户交互(点击、按键、移动鼠标、重绘请求)都转化为特定的 WM_* 消息,被投递到应用程序的消息队列中。你的窗口过程 (WndProc) 必须检查每条消息 (msg.message),使用巨大的 switch-caseif-else 结构来处理它们。处理逻辑与窗口创建代码紧密耦合。

    • Java GUI (Swing/JavaFX): 基于事件监听/回调。采用 观察者模式
      • 你为特定的 UI 组件 (Button, TextField) 注册事件监听器 (ActionListener, MouseListener, KeyListener, ChangeListener 等)。
      • 当用户与组件交互时(如点击按钮),组件会触发一个事件对象 (ActionEvent, MouseEvent)。
      • 你注册的监听器方法 (actionPerformed(ActionEvent e), mouseClicked(MouseEvent e)) 会被自动调用,你只需在这些方法中编写响应事件的逻辑。事件源(哪个组件)和事件类型(点击、移动等)由事件对象携带。
      • 这种方式更符合面向对象思想,代码组织更清晰,将事件处理逻辑与 UI 构建逻辑解耦。

Windows窗口实现

1.创建Windows应用程序,选择空项目

2.在新建窗口中选C++代码文件,创建一个新的cpp

3.在文件中添加:#include <Windows.h>

添加入口函数

1
2
3
4
5
6
7
//#define CALLBACK	__stdcall
int CALLBACK WinMain(_In_ HINSTANCE hInstance,	//当前实例句柄
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow) {
                     return 0;
}

4.设计窗口类

WNDCLASS 结构体的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct tagWNDCLASS {
    UINT      style;          // 窗口类的样式
    WNDPROC   lpfnWndProc;    // 指向窗口过程函数的指针
    int       cbClsExtra;     // 附加的类内存
    int       cbWndExtra;     // 附加的窗口内存
    HINSTANCE hInstance;      // 拥有该类的实例句柄
    HICON     hIcon;          // 类图标的句柄
    HCURSOR   hCursor;        // 类光标的句柄
    HBRUSH    hbrBackground;  // 类背景画刷的句柄
    LPCTSTR   lpszMenuName;   // 菜单资源的名称
    LPCTSTR   lpszClassName;  // 窗口类的名称
} WNDCLASS, *PWNDCLASS;

消息处理流程

代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#include <Windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);	//声明窗口过程函数
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow) {
    //定义并注册窗口类
    TCHAR CLASS_NAME[] = L"MyFisrtWindowC";
    WNDCLASS wndclass = { 0 };	//若不初始化所有成员则无法创建
    wndclass.hbrBackground = (HBRUSH)COLOR_MENU;    //背景色
    wndclass.lpfnWndProc = WindowProc;  //事件触发时由该函数处理(窗口过程函数)
    wndclass.lpszClassName = CLASS_NAME; //窗口类名
    wndclass.hInstance = hInstance; //当前窗口绑定的应用程序(当前实例)
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); // 箭头光标

    RegisterClass(&wndclass);  //注册窗口类

    //创建窗口
    HWND hwnd = CreateWindowEx(
        0,                      // 扩展样式
        CLASS_NAME,             // 注册的类名
        L"我的Win32窗口",        // 窗口标题
        WS_OVERLAPPEDWINDOW,    // 窗口样式
        // 位置和大小
        CW_USEDEFAULT, CW_USEDEFAULT,
        400, 300,
        NULL,       // 父窗口句柄
        NULL,       // 菜单
        hInstance,  // 实例句柄
        NULL        // 附加数据
    );
    if (hwnd == NULL) return 0;

    //显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    //消息循环
    MSG msg = { 0 };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}
//实现窗口过程函数(回调函数)
LRESULT CALLBACK WindowProc(
    HWND hwnd,        // 窗口句柄
    UINT uMsg,        // 消息类型
    WPARAM wParam,    // 附加信息
    LPARAM lParam)    // 附加信息
{
    switch (uMsg) {
    case WM_CREATE: {
        CREATESTRUCT* p = (CREATESTRUCT*)lParam;
        //PrintDebugFormat(L"WM_CREATE: %xh\nWM_CREATE: %s\n", uMsg, p->lpszClass);
        return 0;
    }
    case WM_MOVE: {
        DWORD x = (int)(short)LOWORD(lParam);   // horizontal position 
        DWORD y = (int)(short)HIWORD(lParam);   // vertical position
        //PrintDebugFormat(L"WM_MOVE: %xh, x: %d, y: %d\n", uMsg, x, y);
        return 0;
    }
    case WM_SIZE: {
        DWORD w = (int)(short)LOWORD(lParam);
        DWORD h = (int)(short)HIWORD(lParam);
        //PrintDebugFormat(L"WM_SIZE: %xh, size_v: %d, wide: %d, high: %d\n", uMsg, wParam, w, h);
        return 0;
    }
        // 处理关闭消息
    case WM_DESTROY: {
        PrintDebugFormat(L"WM_DESTROY: %xh\n", uMsg);
        PostQuitMessage(0);  // 退出消息循环
        return 0;
    }
    case WM_KEYUP: {
        PrintDebugFormat(L"WM_KEYUP: %xh\t\tw l: %d %d\n", uMsg, wParam, lParam);
        return 0;
    }
    case WM_KEYDOWN: {
        PrintDebugFormat(L"WM_KEYDOWN: %xh\tw l: %d %d\n", uMsg, wParam, lParam);
        return 0;
    }
    case WM_LBUTTONDOWN: {
        PrintDebugFormat(L"WM_LBUTTONDOWN: %xh\tw l: %d %d\n", uMsg, wParam, lParam);
        return 0;
    }
        // 处理绘制消息
    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);
        // 在此处绘制内容(例如文字)
        TextOut(hdc, 50, 50, L"Hello, Win32!", 13);
        EndPaint(hwnd, &ps);
        return 0;
    }
        // 默认处理其他消息
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

窗口实例

定位回调函数

参考定位入口程序

WNDCLASS结构体的第二个成员为回调函数地址,我们定位到RegisterClass函数,该函数传参为wndclass的地址,观察到RegisterClass函数前的push,此时的eax的值就是传入参数,我们跟随到堆栈,此时eax+4就是回调函数的地址

wndclass地址

右键回调函数地址,转到反汇编,F2设断点,F9运行到断点,多按几次,此时程序会一直运行到该断点;然而,我们在该断点处右键编辑条件为**[esp+8] == WM_LBUTTONDOWN,esp此时为回调函数的返回地址,esp+8自然为第二个参数uMsg**,而buttondown是点击鼠标左键,程序再次运行后,只有当点击鼠标左键这一事件触发后,程序便又断在该断点

断点设置条件

子窗口

创建子按钮窗口函数,这里的**实例句柄hInst**可用全局变量存储,在WinMain中给其赋值为当前实例hInstance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void CreateButton(HWND hwnd) {
    HWND hwndPushButton = CreateWindow(
        TEXT("button"), //系统定义的Button类
        TEXT("普通按钮"),	//按钮名称
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_DEFPUSHBUTTON,	//窗口样式
        10, 10,		//x, y
        80, 20,		//w, h
        hwnd,		//父窗口句柄,这里是前面创建的窗口
        (HMENU)1001,	//子窗口ID
        hInst,		//实例句柄(当前WinMain句柄)
        NULL
    );
    TCHAR szBuffer[0x20];
    GetClassName(hwndPushButton, szBuffer, 0x20);	//查看当前窗口句柄的类名,存在szBuffer中
    WNDCLASS wc;
    GetClassInfo(hInst, szBuffer, &wc);	//得到当前窗口类地址
    PrintDebugFormat(L"-->%s\n-->%x\n", wc.lpszClassName, wc.lpfnWndProc);
    HWND hwndCheckBox = CreateWindow(
        TEXT("button"),
        TEXT("复选框"),
        WS_CHILD | WS_VISIBLE | BS_CHECKBOX | BS_AUTOCHECKBOX,
        10, 40,
        80, 20,
        hwnd,
        (HMENU)1002,
        hInst,
        NULL
    );
    HWND hwndRadio = CreateWindow(
        TEXT("button"),
        TEXT("单选按钮"),
        WS_CHILD | WS_VISIBLE | BS_RADIOBUTTON,
        10, 70,
        80, 20,
        hwnd,
        (HMENU)1003,
        hInst,
        NULL
    );
}

子按钮窗口中,事件触发先会调用系统提供的WinProc,再转换成WM_COMMAND,接着调用父窗口WinProc,故在**父窗口WindowProc**中添加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
case WM_COMMAND: {
    switch (LOWORD(wParam)) {	//LOWORD(wParam) -> 子窗口ID
    case 1001:
        MessageBox(hwnd, L"Button1", L"Demo", MB_OK);
        return 0;
    case 1002:
        MessageBox(hwnd, L"Button2", L"Demo", MB_OK);
        return 0;
    case 1003:
        MessageBox(hwnd, L"Button3", L"Demo", MB_OK);
        return 0;
    }
}
按钮事件逻辑定位

生成Release x86程序,定位回调函数,F2下断点

定位窗口过程函数

此时,从栈顶往下的值依次是函数返回地址,窗口句柄hwnd消息类型uMsgwParamlParam,按W可查看所有句柄

查看句柄

我们看到子窗口“单选按钮”ID为0x3E8,设置断点条件为**[esp+8] == WM_COMMAND && [esp+0xC] == 0x3EB,前一句代表消息类型为子窗口类型**,后一句代表子窗口ID为**“单选按钮”ID**

断点设置条件02

程序运行后,只有点击“单选按钮”这个子窗口,程序才会再次停在该断点

资源文件

在 Win32 应用程序中,资源文件(通常为 .rc 文件)用于定义对话框布局、图标、菜单等资源

资源新建对话框Dialog

添加资源

左侧“工具箱”可以添加控件,右键可查看属性

资源属性

(名称)不可更改(只能通过代码更改),身份ID

对话框生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 对话框过程函数
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        // 对话框初始化
    case WM_INITDIALOG: {
        // 设置对话框图标
        HICON hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_WIN32DIALOG));
        SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);

        // 初始化控件
        SetDlgItemText(hDlg, IDC_STATIC, L"欢迎使用 Win32 对话框");

        // 设置按钮文本
        SetDlgItemText(hDlg, IDOK, L"确定(&O)");
        SetDlgItemText(hDlg, IDCANCEL, L"取消(&C)");

        // 设置编辑框提示文本
        SetDlgItemText(hDlg, IDC_EDIT_NAME, L"请输入姓名");

        // 初始化进度条
        HWND hProgress = GetDlgItem(hDlg, IDC_PROGRESS);
        //SendMessage(hProgress, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
        //SendMessage(hProgress, PBM_SETPOS, 50, 0);

        return TRUE;
    }

                      // 命令处理
    case WM_COMMAND: {
        WORD id = LOWORD(wParam);
        WORD event = HIWORD(wParam);

        switch (id) {
            // 确定按钮
        case IDOK: {
            // 获取编辑框内容
            wchar_t name[100];
            GetDlgItemText(hDlg, IDC_EDIT_NAME, name, ARRAYSIZE(name));

            // 显示消息框
            wchar_t message[256];
            swprintf(message, ARRAYSIZE(message),
                L"您好,%s!\n感谢使用本对话框",
                wcslen(name) > 0 ? name : L"用户");

            MessageBox(hDlg, message, L"问候", MB_ICONINFORMATION);

            // 关闭对话框
            EndDialog(hDlg, IDOK);
            return TRUE;
        }

                 // 取消按钮
        case IDCANCEL:
            EndDialog(hDlg, IDCANCEL);
            return TRUE;

                      // 退出菜单项
        case IDM_EXIT:
            EndDialog(hDlg, IDCANCEL);
            return TRUE;
        }
        break;
    }

                   // 绘制对话框背景
    case WM_CTLCOLORDLG: {
        static HBRUSH hBrush = NULL;
        if (hBrush == NULL) {
            hBrush = CreateSolidBrush(RGB(240, 245, 255)); // 浅蓝色背景
        }
        return (INT_PTR)hBrush;
    }

                       // 关闭对话框
    case WM_CLOSE:
        EndDialog(hDlg, IDCANCEL);
        return TRUE;
    }

    return FALSE;
}

WM_INITDIALOG 是 Windows 对话框编程中一个非常重要的消息,用于对话框的初始化工作

触发时机

  • 在对话框被创建后,显示之前发送
  • 当调用以下函数创建对话框时触发:
1
DialogBox(), DialogBoxParam(), CreateDialog(), CreateDialogParam()

消息含义

  • 通知对话框过程:对话框及其所有控件已创建完成
  • 为开发者提供初始化对话框的机会

在此消息中应该做什么

  1. 初始化控件

    1
    2
    
    SetDlgItemText(hwndDlg, IDC_EDIT1, L"默认文本");
    CheckDlgButton(hwndDlg, IDC_CHECK1, BST_CHECKED);
    
  2. 设置图标

    1
    2
    
    HICON hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_APP_ICON));
    SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
    
  3. 设置默认焦点

    1
    
    SetFocus(GetDlgItem(hwndDlg, IDC_EDIT1));
    
  4. 子类化控件(高级用法):

    1
    2
    
    SetWindowLongPtr(GetDlgItem(hwndDlg, IDC_LIST1), GWLP_WNDPROC, 
                    (LONG_PTR)MyListProc);
    
  5. 加载初始数据

    1
    
    LoadInitialData(hwndDlg);
    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
BOOL CALLBACK DialogProc(
    HWND hwndDlg,	//handle to dialog box
    UINT uMsg,		//message
    WPARAM wParam,	//first message parameter
    LPARAM lParam	//second message parameter
) {
    switch (uMsg) {
    case WM_INITDIALOG:
        return TRUE;
    case WM_COMMAND: {
        switch (LOWORD(wParam)) {
        case IDOK: {
            //获取文本框的句柄
            HWND hEditUser = GetDlgItem(hwndDlg, IDC_EDIT1);
            HWND hEditPwd = GetDlgItem(hwndDlg, IDC_EDIT2);
            //通过句柄得到内容
            TCHAR szUserBuffer[0x50];
            TCHAR szPwdBuffer[0x50];
            GetWindowText(hEditUser, szUserBuffer, 0x50);
            GetWindowText(hEditPwd, szPwdBuffer, 0x50);
            return TRUE;
        }
        case IDCANCEL:
            EndDialog(hwndDlg, IDCANCEL);
            return TRUE;
        }
        break;
    }
    case WM_CLOSE:
        EndDialog(hwndDlg, IDCANCEL);   //关闭对话框
        return TRUE;
    }
    return FALSE;
}

创建Dialog(主函数调用)

1
DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);

创建 Win32 项目

  1. 打开 Visual Studio,点击“创建新项目”。
  2. 选择 “Windows 桌面向导”(C++),点击“下一步”。
  3. 填写项目名称,点击“创建”。
  4. 在“配置新项目”界面,选择“Windows 应用程序”,点击“创建”

添加Dialog资源

  1. 在“解决方案资源管理器”中,右键点击“资源文件(.rc)”,选择“打开”。
  2. 在资源视图中,右键点击“Dialog”,选择“插入对话框”

编辑对话框(添加控件)

  1. 双击新建的对话框(如 IDD_DIALOG1),进入对话框编辑器
  2. 在左侧工具箱(Toolbox)中,可以拖拽以下控件到对话框上

响应控件事件(按钮点击等)

  1. 在对话框类的消息映射中添加响应函数。
  2. 以 Win32 API 为例,通常在对话框过程(DialogProc)中处理 WM_COMMAND 消息:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
        if (LOWORD(wParam) == IDC_BUTTON1) // 按钮ID
        {
            MessageBox(hDlg, L"按钮被点击!", L"提示", MB_OK);
            return (INT_PTR)TRUE;
        }
        break;
    case WM_CLOSE:
        EndDialog(hDlg, 0);
        return (INT_PTR)TRUE;
    }
    return (INT_PTR)FALSE;
}

五、显示对话框

在主程序中调用:

1
DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);

六、添加图片资源

  1. 在资源视图中,右键“资源”->“导入”,选择图片(如 BMP)。
  2. 在 Picture Control 的属性中,设置“Type”为 Bitmap,“Image”为刚导入的图片ID。
通过子类定位回调函数

即通过子窗口内置窗口过程函数定位父窗口回调函数WinProc

以上述Release x86程序为例

x32dbg打开,程序运行到弹出窗口后,点击“句柄”,右键->刷新,找到Button类,右键->消息断点设置WM_LBUTTONUP,等价于[esp+8] == WM_LBUTTONUP(消息类型为鼠标左键松开),此时点击窗口中的按钮,程序便会停在Button类的窗口过程函数了(在dll空间)

消息断点

我们转到内存布局,选中“.text”段,右键->内存访问断点(程序访问内存时便会停下)

内存访问断点

F9,此时的断点不一定是我们需要找的函数,因为可能在调用父窗口WinProc函数之前进行内存访问

内存断点

继续F9,当[esp+8] = 0x111时,即uMsg = WM_COMMANDWM_COMMAND消息类型0x111,因为子类调用父类的回调函数时,消息类型变为WM_COMMAND,所以满足上述条件即为父类的窗口过程函数),定位成功

窗口过程函数

图标

加载图标

1
2
HICON hIcon;
hIcon = LoadIcon(hwndDlg, MAKEINTRESOURCE(IDI_ICON1));	//句柄,图标编号

设置图标

1
2
3
4
case WM_INITDIALOG:
HICON hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1));
SendMessage(hwndDlg, WM_SETICON, IDI_ICON1, (DWORD)hIcon);
return TRUE;

Win32线程

在 Windows 操作系统中,线程是程序执行的基本单元。理解 Win32 线程对于开发高效、响应迅速的 Windows 应用程序至关重要

Win32 线程是 Windows 操作系统中最小的执行单元,具有以下特性:

  • 轻量级进程:线程比进程更轻量,创建和切换开销小
  • 共享资源:同一进程内的线程共享内存空间和资源
  • 独立执行:每个线程有自己的程序计数器、寄存器集和堆栈
  • 并行执行:多核 CPU 上可真正并行运行

线程生命周期

  1. 创建:使用 CreateThread() 创建线程
  2. 运行:线程执行其函数
  3. 挂起:可暂停执行(SuspendThread()
  4. 恢复:继续执行(ResumeThread()
  5. 终止:线程函数返回或调用 ExitThread()
  6. 清理:关闭线程句柄(CloseHandle()

创建线程

1
2
3
4
5
6
7
8
HANDLE CreateThread(	//返回值:线程句柄
  LPSECURITY_ATTRIBUTES   lpThreadAttributes, // 安全属性(通常为 NULL)
  SIZE_T                  dwStackSize,        // 堆栈大小(0=默认)
  LPTHREAD_START_ROUTINE  lpStartAddress,     // 线程函数指针
  LPVOID                  lpParameter,        // 传递给线程的参数
  DWORD                   dwCreationFlags,    // 创建标志(0=立即运行)
  LPDWORD                 lpThreadId          // 接收线程ID(可 NULL)
);

线程函数签名

1
DWORD WINAPI ThreadFunction(LPVOID lpParam);

等待线程结束

1
2
3
4
DWORD WaitForSingleObject(
  HANDLE hHandle,        // 线程句柄
  DWORD  dwMilliseconds   // 超时时间(INFINITE=无限等待)
);

线程实现过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <windows.h>
using namespace std;
HANDLE hThread;
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
    // 使用参数(可选)
    int threadId = (int)(INT_PTR)lpParam;
    for (int i = 0; i < 1000; i++) {
        Sleep(1000);
        printf("Thread %d: +++++++++++++\n", threadId);
    }
    return 0;
}
void MyThread() {
    DWORD threadId;
    hThread = CreateThread(		//线程句柄
        NULL,           // 安全属性
        0,              // 堆栈大小(默认)
        ThreadFunction, // 线程函数指针
        (LPVOID)(INT_PTR)1, // 传递给线程的参数(这里也可以用全局变量传)
        0,              // 创建标志(立即运行)
        &threadId       // 接收线程ID
    );
    if (hThread == NULL) {
        cerr << "CreateThread failed! Error: " << GetLastError() << endl;
    }
}
int main() {
    MyThread();
    // 主线程工作
    for (int i = 0; i < 1000; i++) {
        Sleep(1000);
        printf("Main thread: -----------------\n");
    }
    // 等待所有线程完成(实际项目中需要实现)
    if (hThread) {
        WaitForSingleObject(hThread, INFINITE);
        ::CloseHandle(hThread);		//若用"::"代表是全局函数,而不是类成员
        hThread = NULL;
    }
    // 这里简单等待足够长时间让线程完成
    Sleep(1000 * 1000);
    return 0;
}

与事件结合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "framework.h"
#include "Win32线程窗口.h"
#include <stdio.h>
HINSTANCE hInst;
HWND hEdit;
HANDLE hThread;
DWORD WINAPI Timer(LPVOID lpParam) {
    TCHAR szBuffer[20] = { 0 };
    GetWindowText(hEdit, szBuffer, 20); //获取文本框内容
    DWORD dwTime;
    swscanf_s(szBuffer, L"%d", &dwTime);    //字符串转整数函数
    while (dwTime > 0) {
        memset(szBuffer, 0, 20);
        Sleep(100);
        swprintf_s(szBuffer, L"%d", --dwTime);  //整数转字符串函数
        SetWindowText(hEdit, szBuffer);
    }
    return 0;
}
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_INITDIALOG: {   //初始化Dialog
        hEdit = GetDlgItem(hDlg, IDC_EDIT1);    //得到资源句柄
        SetWindowText(hEdit, L"1000");  //设置文本框内容
        break;
    }
    case WM_COMMAND:
        if (LOWORD(wParam) == IDC_BUTTON1) {    // 按钮ID
            //MessageBox(hDlg, L"按钮被点击!", L"提示", MB_OK);
            hThread = CreateThread(NULL, 0, Timer, (LPVOID)(INT_PTR)1, 0, 0);
            return (INT_PTR)TRUE;
        }
        break;
    case WM_CLOSE:
        EndDialog(hDlg, 0);
        return (INT_PTR)TRUE;
    }
    return (INT_PTR)FALSE;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow) {
    DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
}

线程控制函数

Windows中的函数

功能 函数名 语法
线程创建函数 CreateThread() HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
线程终止函数 ExitThread() VOID ExitThread(DWORD dwExitCode);//在线程内部使用,参数为退出码,清除堆栈
强制终止线程 TerminateThread() BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);//不推荐
线程挂起函数 SuspendThread() DWORD SuspendThread(HANDLE hThread);
线程恢复函数 ResumeThread() DWORD ResumeThread(HANDLE hThread);
关闭线程句柄 CloseHandle() BOOL CloseHandle(HANDLE hObject);//不会终止线程
获取线程退出码 GetExitCodeThread() BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
获取线程伪句柄 GetCurrentThread() HANDLE GetCurrentThread(VOID);
获取线程ID GetCurrentThreadId() DWORD GetCurrentThreadId(VOID);

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "framework.h"
#include "Win32线程窗口.h"
#include <stdio.h>
HINSTANCE hInst;
HWND hEdit;
HANDLE hThread;
DWORD WINAPI ThreadProc1(LPVOID lpParam) {
    TCHAR szBuffer[20] = { 0 };
    DWORD dwIndex = 0;
    DWORD dwCount;
    while (dwIndex < 100) {
        //if (dwIndex == 10) ExitThread(2);
        GetWindowText(hEdit, szBuffer, 20);
        swscanf_s(szBuffer, L"%d", &dwCount);
        memset(szBuffer, 0, 20);
        Sleep(100);
        swprintf_s(szBuffer, L"%d", ++dwCount);
        SetWindowText(hEdit, szBuffer);
        dwIndex++;
    }
    return 0;
}
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_INITDIALOG: {   //初始化Dialog
        hEdit = GetDlgItem(hDlg, IDC_EDIT1);    //得到资源句柄
        SetWindowText(hEdit, L"0");  //设置文本框内容
        break;
    }
    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDC_BUTTON1: {
            hThread = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
            return true;
        }
        case IDC_BUTTON2: {
            SuspendThread(hThread);     //挂起线程:不分配cpu
            return true;
        }
        case IDC_BUTTON3: {
            ResumeThread(hThread);      //恢复线程:重新分配cpu
            return true;
        }
        case IDC_BUTTON4: {
            TerminateThread(hThread, 2);
            WaitForSingleObject(hThread, INFINITE);
            return true;
        }
        }
        break;
    case WM_CLOSE:
        EndDialog(hDlg, 0);
        return (INT_PTR)TRUE;
    }
    return (INT_PTR)FALSE;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow) {
    DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
}

CONTEXT

在 Windows 系统编程中,CONTEXT 是一个关键的数据结构,它用于表示处理器的状态。这个结构体包含了线程执行时 CPU 寄存器的完整快照,允许开发者在调试、异常处理或线程控制中检查和修改处理器的状态。

CONTEXT 结构体定义在 <winnt.h> 头文件中,其主要作用是:

  1. 保存和恢复线程状态:捕获线程执行时的完整寄存器状态
  2. 异常处理:当发生异常时,系统提供 CONTEXT 结构显示异常发生时的状态
  3. 调试器实现:调试器使用它来检查和控制被调试线程
  4. 线程操作:挂起/恢复线程时保存状态

结构定义(x86 架构示例)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef struct _CONTEXT {
    DWORD ContextFlags;  // 标志位,指定哪些寄存器有效

    // 调试寄存器
    DWORD Dr0;
    DWORD Dr1;
    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;
    DWORD Ecx;
    DWORD Eax;

    // 控制寄存器
    DWORD Ebp;
    DWORD Eip;      // 指令指针
    DWORD SegCs;
    DWORD EFlags;   // 标志寄存器
    DWORD Esp;      // 栈指针
    DWORD SegSs;
    
    // 扩展寄存器(如XMM等)
    BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

ContextFlags 标志位

这些标志指定要获取/设置哪些寄存器组:

标志 含义
CONTEXT_CONTROL 控制寄存器 (Eip, Esp, Ebp, Eflags, SegCs, SegSs)
CONTEXT_INTEGER 整数寄存器 (Edi, Esi, Ebx, Edx, Ecx, Eax)
CONTEXT_SEGMENTS 段寄存器 (SegDs, SegEs, SegFs, SegGs)
CONTEXT_FLOATING_POINT 浮点寄存器
CONTEXT_DEBUG_REGISTERS 调试寄存器 (Dr0-Dr7)
CONTEXT_EXTENDED_REGISTERS 扩展寄存器 (XMM, MMX等)
CONTEXT_FULL 等价于 CONTROL + INTEGER + SEGMENTS
CONTEXT_ALL 所有可用寄存器
获取线程CONTEXT结构
  1. 获取线程上下文
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HANDLE hThread = ...; // 线程句柄
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL; // 指定要获取的寄存器组
// 必须先挂起线程
SuspendThread(hThread);
if(GetThreadContext(hThread, &ctx)) {
    printf("当前EIP: 0x%08X\n", ctx.Eip);
    printf("当前ESP: 0x%08X\n", ctx.Esp);
}
ResumeThread(hThread);
  1. 设置线程上下文
1
2
3
4
5
6
7
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_CONTROL; // 只修改控制寄存器
ctx.Eip = 0x00401000; // 设置新的指令指针
ctx.Esp = 0x0012FF88; // 设置新的栈指针
SuspendThread(hThread);
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
  1. 异常处理

在异常处理回调中:

1
2
3
4
5
6
7
8
LONG WINAPI ExceptionHandler(
    EXCEPTION_POINTERS* pExceptionInfo) {
    CONTEXT* ctx = pExceptionInfo->ContextRecord;
    printf("异常地址: 0x%08X\n", ctx->Eip);
    // 可以修改上下文,例如跳过导致异常的指令
    ctx->Eip += 2; // 假设指令长度为2字节
    return EXCEPTION_CONTINUE_EXECUTION;
}

实例

修改线程执行流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void RedirectThread(HANDLE hThread, LPVOID newAddress) {
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_CONTROL;
    
    SuspendThread(hThread);
    if(GetThreadContext(hThread, &ctx)) {
        ctx.Eip = (DWORD)newAddress;
        SetThreadContext(hThread, &ctx);
    }
    ResumeThread(hThread);
}

获取调用堆栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void PrintStackTrace(HANDLE hThread) {
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_CONTROL;
    SuspendThread(hThread);
    GetThreadContext(hThread, &ctx);
    DWORD ebp = ctx.Ebp;
    DWORD eip = ctx.Eip;
    printf("Call stack:\n");
    for(int i = 0; i < 10 && ebp != 0; i++) {
        printf("  [%d] 0x%08X\n", i, eip);
        // 读取返回地址 (eip 在 ebp+4)
        ReadProcessMemory(GetCurrentProcess(), (LPCVOID)(ebp + 4), &eip, sizeof(eip), NULL);
        // 读取上一栈帧的 ebp
        ReadProcessMemory(GetCurrentProcess(), (LPCVOID)ebp, &ebp, sizeof(ebp), NULL);
    }
    ResumeThread(hThread);
}

线程互斥与同步

在并发编程中(如Java),**线程互斥(Mutual Exclusion)线程同步(Synchronization)**是确保多线程程序正确性的核心机制。它们解决了多线程环境下的共享资源访问问题,防止出现数据竞争和不确定行为

线程互斥

  1. 概念与目的
  • 定义:确保同一时间只有一个线程可以访问共享资源(临界区)

  • 目的:防止数据竞争(Data Race) - 多个线程同时修改同一数据导致的不一致

  • 锁 (Lock):这就是互斥机制的核心。在 Windows 中,最常用的锁是 互斥体 (Mutex)临界区 (Critical Section)

  • 操作流程

    • 进入临界区前,先尝试获取锁(锁门)
    • 如果锁是空闲的,那么这个线程就成功获取锁,然后进入临界区(进卫生间并锁上门)
    • 如果锁已经被其他线程持有,那么这个线程就会被阻塞(在门口排队等待),直到那个线程释放锁
    • 离开临界区后,必须释放锁(开门离开),以便其他等待的线程可以获取它

互斥是实现线程同步的基础,它是一种强制性的串行访问,保证了操作的原子性

  1. 互斥实现机制
互斥锁Mutex
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
long long shared_counter = 0;
void critical_section() {
    mtx.lock();   // 获取锁
    // ... 访问共享资源 ...
    shared_counter++;
    mtx.unlock(); // 释放锁
}
// RAII风格的更安全写法
void increment_safe() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 构造时自动加锁,析构时自动解锁
        shared_counter++;
    } // lock 变量在此处超出作用域,自动解锁
}
int main() {
    std::thread t1(increment_safe);
    std::thread t2(increment_safe);
    t1.join();
    t2.join();
    std::cout << "Final counter: " << shared_counter << std::endl; // 结果总是 200000
    return 0;
}

特点

  • 阻塞式:获取锁失败的线程会休眠
  • 所有权:只有获取锁的线程能释放它
  • 非递归:同一线程重复加锁会导致死锁(除非使用递归锁)

缺点:线程的挂起和唤醒涉及上下文切换,有较大的系统开销

自旋锁(Spinlock)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <thread>
#include <atomic>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
long long shared_counter_spin = 0;
void critical_section() {
    for (int i = 0; i < 100000; ++i) {
    // 忙等待循环,直到获取锁
    	while (lock.test_and_set(std::memory_order_acquire)) {} // 自旋等待
    // ... 访问共享资源 ...
    	shared_counter_spin++;
    	lock.clear(std::memory_order_release); // 释放锁
    }
}
int main() {
    std::thread t1(increment_spin);
    std::thread t2(increment_spin);

    t1.join();
    t2.join();

    std::cout << "Final counter (spin lock): " << shared_counter_spin << std::endl;
    return 0;
}

特点

  • 忙等待:线程不会休眠,持续检查锁状态(忙等待Busy-Waiting
  • 适用场景:临界区代码执行时间极短(< 上下文切换时间)

缺点:如果锁被占用的时间较长,自旋锁会极度浪费CPU资源

读写锁Read-Write Lock
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <shared_mutex>
std::shared_mutex rw_mutex;
// 读操作(可并发)
void read_data() {
    std::shared_lock<std::shared_mutex> lock(rw_mutex);
    // ... 只读访问 ...
}
// 写操作(独占)
void write_data() {
    std::unique_lock<std::shared_mutex> lock(rw_mutex);
    // ... 修改数据 ...
}

Win32 API SRWLock (Slim Reader/Writer Lock) 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>
SRWLOCK srw_lock; // 读写锁对象
int shared_value = 0;
void reader_thread(int id) {
    for (int i = 0; i < 5; ++i) {
        AcquireSRWLockShared(&srw_lock); // 获取读锁(共享锁)
        std::cout << "Reader " << id << " reads value: " << shared_value << std::endl;
        ReleaseSRWLockShared(&srw_lock); // 释放读锁
        Sleep(100); // 模拟其他工作
    }
}
void writer_thread(int id) {
    for (int i = 0; i < 5; ++i) {
        AcquireSRWLockExclusive(&srw_lock); // 获取写锁(独占锁)
        shared_value++;
        std::cout << "Writer " << id << " incremented value to: " << shared_value << std::endl;
        ReleaseSRWLockExclusive(&srw_lock); // 释放写锁
        Sleep(100);
    }
}
int main() {
    InitializeSRWLock(&srw_lock);
    std::vector<std::thread> threads;
    threads.emplace_back(writer_thread, 1);
    threads.emplace_back(reader_thread, 1);
    threads.emplace_back(reader_thread, 2);
    threads.emplace_back(reader_thread, 3);
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

特点

  • 允许多个读线程并发访问
  • 写操作需要独占访问
  • 适用场景:读多写少的场景

缺点:实现比普通互斥锁复杂,可能会导致“写者饥饿”

临界区Critical Section
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CRITICAL_SECTION cs;	// 全局临界区对象
// 初始化
InitializeCriticalSection(&cs);
// 进入临界区(阻塞)
EnterCriticalSection(&cs);
// 尝试进入(非阻塞)
if(TryEnterCriticalSection(&g_cs)) {
    // 成功进入
} else {
    // 其他线程持有锁
}
// 离开临界区
LeaveCriticalSection(&cs);
// 删除
DeleteCriticalSection(&cs);

Windows API 例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION g_cs; // 临界区对象
int g_counter = 0;     // 共享资源
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    for(int i = 0; i < 10000; i++) {
        EnterCriticalSection(&g_cs);   // 进入临界区
        // 安全访问共享资源
        g_counter++;
        LeaveCriticalSection(&g_cs);   // 离开临界区
    }
    return 0;
}
int main() {
    InitializeCriticalSection(&g_cs);  // 初始化临界区
    HANDLE hThreads[2];
    hThreads[0] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
    hThreads[1] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
    WaitForMultipleObjects(2, hThreads, TRUE, INFINITE); // 等待线程结束
    DeleteCriticalSection(&g_cs);      // 销毁临界区
    printf("Final counter value: %d\n", g_counter); // 应为20000
    return 0;
}

线程同步

  1. 概念与目的
  • 定义:协调线程间的执行顺序
  • 目的:确保线程按照特定顺序执行(如:生产者-消费者模式)
  • 事件 (Event):最常用的同步工具。一个线程可以等待一个事件被触发,另一个线程可以去触发这个事件
    • 自动重置事件:像一个旋转门,触发一次只能通过一个等待的线程,然后自动关闭。非常适合“一对一”的通知。
    • 手动重置事件:像一个水闸,一旦打开,所有等待的线程都可以通过,直到被手动关闭。适合“一对多”的广播通知。
  • 信号量 (Semaphore):一个带计数的锁。允许多个线程(不超过设定的最大数量)同时访问一个资源。就像一个有多把钥匙的房间,或者一个有固定车位的停车场。
  • 互斥体 (Mutex):它本身既是互斥锁,也是一个内核同步对象。可以用于跨进程的线程同步,但效率比临界区低。
  • 条件变量 (Condition Variable):通常与临界区或互斥锁配合使用,可以实现更复杂的等待/通知逻辑,避免CPU空转。
  1. 同步实现机制

条件变量(Condition Variable)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
// 生产者
void producer() {
    std::unique_lock<std::mutex> lock(mtx);
    // ... 生产数据 ...
    data_ready = true;
    cv.notify_one(); // 通知消费者
}
// 消费者
void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return data_ready; }); // 等待条件满足
    // ... 消费数据 ...
    data_ready = false;
}

工作原理:条件变量总是与一个互斥锁配合使用。它允许一个线程在某个条件不满足时,原子地释放锁并进入睡眠,直到另一个线程满足了该条件,并发送通知来唤醒它

工作流程

  1. 消费者等待条件满足
  2. 生产者完成工作后通知消费者
  3. 消费者被唤醒并检查条件

特点

  • 必须与互斥锁配合使用
  • 解决"等待-通知"问题
  • 防止忙等待

适用场景:需要等待某个复杂条件成立的生产者-消费者模型,或任何需要精细等待/通知的场景。可以避免使用 Sleep 的忙等待

信号量Semaphore
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <semaphore>
std::counting_semaphore<10> sem(0); // 最大10,初始0
// 生产者
void producer() {
    // ... 生产项目 ...
    sem.release(); // V操作:增加信号量
}
// 消费者
void consumer() {
    sem.acquire(); // P操作:减少信号量(阻塞如果为0)
    // ... 消费项目 ...
}

Win32 API 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>
HANDLE hSemaphore; // 信号量句柄
void worker_thread(int id) {
    std::cout << "Thread " << id << " is waiting to enter the critical section." << std::endl;
    // 等待信号量,如果计数>0则减一并进入,否则阻塞
    WaitForSingleObject(hSemaphore, INFINITE);
    std::cout << "Thread " << id << " has entered the critical section." << std::endl;
    // 模拟正在使用有限的资源
    Sleep(2000); 
    std::cout << "Thread " << id << " is leaving the critical section." << std::endl;
    // 释放信号量,计数加一
    ReleaseSemaphore(hSemaphore, 1, NULL);
}
int main() {
    const int MAX_CONCURRENT_THREADS = 3;
    const int TOTAL_THREADS = 10;
    // 创建信号量,初始计数为3,最大计数为3
    hSemaphore = CreateSemaphore(NULL, MAX_CONCURRENT_THREADS, MAX_CONCURRENT_THREADS, NULL);
    if (hSemaphore == NULL) {
        return 1;
    }
    std::vector<std::thread> threads;
    for (int i = 0; i < TOTAL_THREADS; ++i) {
        threads.emplace_back(worker_thread, i + 1);
    }
    for (auto& t : threads) {
        t.join();
    }
    CloseHandle(hSemaphore);
    return 0;
}

工作原理:信号量维护一个非负整数计数,它有两个基本操作:

  • P操作(acquire/proberen):减少信号量,如果<0则阻塞
  • V操作(release/verhogen):增加信号量,唤醒等待线程

类型

  • 二进制信号量:值域{0,1},类似互斥锁
  • 计数信号量:控制并发访问数量

适用场景控制对有限资源的并发访问数量。例如,限制同时连接数据库的线程数,或限制线程池中同时工作的线程数

注意:与互斥锁不同,信号量的 waitpost 操作可以由不同的线程执行

互斥体Mutex
1
WaitForSingleObject(hMutex, INFINITE);

timeout 参数设为 0 是关键技巧。这意味着 WaitForSingleObject 不会等待,它会立即检查锁的状态:

  • 如果锁可用,它就立即获取锁,并返回 WAIT_OBJECT_0
  • 如果锁不可用,它不会挂起线程去等待,而是立即返回 WAIT_TIMEOUT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <windows.h>
#include <iostream>
int main() {
    // 定义一个全局唯一的互斥体名称
    const wchar_t* mutexName = L"Global\\MyAppSingleInstanceMutex";
    // "Global\\" 前缀确保在所有会话(包括远程桌面)中都唯一
    HANDLE hMutex = CreateMutex(
        NULL,
        TRUE, // 尝试成为初始所有者
        mutexName
    );
    if (hMutex == NULL) {
        // 创建失败,可能是权限问题
        MessageBox(NULL, L"无法创建互斥体!", L"错误", MB_OK);
        return 1;
    }
    // 检查互斥体是否已经存在
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
        // 互斥体已存在,说明有另一个实例正在运行
        MessageBox(NULL, L"应用程序已经在运行了!", L"提示", MB_OK);
        // 必须关闭我们获取到的这个句柄
        CloseHandle(hMutex);
        return 0; // 退出当前实例
    }
    // 如果代码能执行到这里,说明这是第一个实例
    MessageBox(NULL, L"应用程序启动成功!", L"成功", MB_OK);
    // ... 在这里运行你的主程序逻辑,比如消息循环 ...
    // 程序退出前,释放并关闭互斥体
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);
    return 0;
}

工作原理

  • 等待 (Wait):当一个线程尝试获取一个已经被占用的互斥体时(例如调用 WaitForSingleObject),操作系统会将这个线程置于等待状态并将其从调度队列中移除。这个线程会“睡眠”,不消耗CPU。
  • 唤醒 (Signal/Notify):当持有互斥体的线程完成操作并释放它时(例如调用 ReleaseMutex),操作系统会检查是否有其他线程正在等待这个互斥体。如果有,操作系统会从等待队列中选择一个线程,将其状态从“等待”变为“就绪”,让它有机会获得互斥体并继续执行

特点:它本身既是互斥锁,也是一个内核同步对象。可以用于跨进程的线程同步,但效率比临界区低。

事件Event
1
2
3
4
5
6
7
8
9
// Windows API 示例
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 设置事件
SetEvent(hEvent);
// 等待事件
WaitForSingleObject(hEvent, INFINITE);
// 设置事件
SetEvent(hEvent);
CloseHandle(hEvent);

特点

  • 状态机制:有信号/无信号
  • 线程等待事件被触发
  • 手动重置 vs 自动重置事件

与互斥体的对比

  • 事件的“等待-唤醒”是显式的。你需要在一个线程中明确调用 SetEvent 来唤醒另一个正在WaitForSingleObject 的线程。
  • 互斥体的“等待-唤醒”是隐式的。它的唤醒操作与ReleaseMutex 绑定在一起,你无法单独触发唤醒而不释放锁
生产者-消费者
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <windows.h>
#include <iostream>
// 共享的“信箱”
char g_mailbox[256];
bool g_dataReady = false;
// 同步对象:一个事件
HANDLE g_hEvent;
// 生产者线程:负责准备数据
DWORD WINAPI ProducerThread(LPVOID lpParam) {
    std::cout << "Producer: Preparing data..." << std::endl;
    Sleep(2000); // 模拟耗时的数据准备工作
    strcpy_s(g_mailbox, "Hello from the producer!");
    g_dataReady = true;
    std::cout << "Producer: Data is ready. Signaling the event." << std::endl;
    // 数据准备好了,触发事件,通知消费者
    SetEvent(g_hEvent);
    return 0;
}
// 消费者线程:负责使用数据
DWORD WINAPI ConsumerThread(LPVOID lpParam) {
    std::cout << "Consumer: Waiting for data..." << std::endl;
    // 等待事件被触发,最多等待5秒
    WaitForSingleObject(g_hEvent, 5000);
    // 一旦被唤醒,就知道数据已经准备好了
    if (g_dataReady) {
        std::cout << "Consumer: Received data: \"" << g_mailbox << "\"" << std::endl;
    } else {
        std::cout << "Consumer: Timed out. No data received." << std::endl;
    }
    return 0;
}
int main() {
    // 创建一个自动重置、初始未触发的事件
    g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    HANDLE hProducer = CreateThread(NULL, 0, ProducerThread, NULL, 0, NULL);
    HANDLE hConsumer = CreateThread(NULL, 0, ConsumerThread, NULL, 0, NULL);
    WaitForSingleObject(hProducer, INFINITE);
    WaitForSingleObject(hConsumer, INFINITE);
    CloseHandle(hProducer);
    CloseHandle(hConsumer);
    CloseHandle(g_hEvent);
    return 0;
}

互斥与同步对比

特性 线程互斥 线程同步
主要目的 保护共享资源 协调线程执行顺序
核心问题 数据竞争 执行顺序依赖
典型场景 共享数据修改 生产者-消费者
实现机制 互斥锁、自旋锁 条件变量、信号量
资源访问 独占访问 协作访问
性能影响 上下文切换开销 线程阻塞开销

参考

Win32 - 滴水逆向课程笔记

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
人生若只如初见
使用 Hugo 构建
主题 StackJimmy 设计