Win32 是 Microsoft 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 |
学术研究、内存中临时处理 |
- 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表情等。
- UTF-16 (16-bit Unicode Transformation Format)
在很多系统内部,尤其是 Windows 和 Java,这是首选的编码方式。
- 核心思想: 尽量用2个字节表示一个字符,实在不行再用4个。
- 规则:
- 对于码点在
U+0000 到 U+FFFF 之间的字符(这个范围包含了几乎所有常用字符,包括大部分汉字),直接用2个字节存储。
- 对于码点超出
U+FFFF 的字符(如Emoji),使用代理对 (Surrogate Pair) 的方式,用4个字节来表示。
- 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);
|
还有wcscat、wcscmp、wcsstr等
Win32 API
API(Application Programming Interface,应用程序编程接口)
什么是Win32 API
Win32 API 是 Windows 操作系统底层的应用程序编程接口,为开发者提供直接访问操作系统核心功能的途径,其主要存放在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 |

关键功能DLL
通过这些dll(三环)调用内核函数
- 系统服务(Kernel32.dll)
- 进程/线程:
CreateProcess(), CreateThread()
- 内存管理:
VirtualAlloc(), HeapAlloc()
- 文件操作:
CreateFile(), ReadFile()
- 同步对象:
CreateMutex(), WaitForSingleObject()
- 用户界面(User32.dll)
- 窗口管理:
CreateWindowEx(), ShowWindow()
- 消息循环:
GetMessage(), DispatchMessage()
- 控件操作:
CreateButton(), SetWindowText()
- 资源管理:
LoadIcon(), LoadString()
- 图形设备(GDI32.dll)
- 绘图:
LineTo(), Rectangle()
- 文本:
TextOut(), DrawText()
- 位图:
CreateBitmap(), BitBlt()
- 设备上下文:
GetDC(), ReleaseDC()
- 高级服务
- 注册表:
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桌面应用程序
wWinMain 是 Windows 图形用户界面(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 函数内部通常会做以下几件事:
- 注册窗口类 (Register Window Class):使用
WNDCLASSEX 结构体定义窗口的各种属性(如窗口过程函数、图标、光标、背景颜色等),然后调用 RegisterClassEx 告诉操作系统“我要创建一种这样的窗口”。
- 创建窗口 (Create Window):调用
CreateWindowEx 函数,使用上一步注册的窗口类来创建一个具体的窗口实例。
- 显示和更新窗口 (Show and Update Window):调用
ShowWindow(并传入 nCmdShow)和 UpdateWindow,让窗口显示在屏幕上并绘制其内容。
- 进入消息循环 (Message Loop):
- 这是一个
while 循环,不断调用 GetMessage 从程序的消息队列中获取消息(如鼠标点击、键盘按键、窗口重绘请求等)。
- 调用
TranslateMessage 进行一些键盘消息的转换。
- 调用
DispatchMessage 将消息分发给对应的窗口过程函数(WndProc)去处理。
- 循环结束,返回:当
GetMessage 收到 WM_QUIT 消息时,它会返回 FALSE,循环结束,wWinMain 函数返回,程序退出。
定位入口程序
这里以win32程序(Debug x86)为例,F8步进,运行到图片光标处,F7步入


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

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

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

返回地址[esp+80]、第一个参数[esp+84]?不错,我们可以双击堆栈中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提供错误查找工具

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

Win32图形界面
事件与消息
在 Windows 中,事件 (Event Object) 是一种内核对象 (Kernel Object),它与进程、线程、互斥体 (Mutex)、信号量 (Semaphore) 处于同一层面
事件特征:
- 用途:主要用于线程间的同步(Synchronization)。一个线程可以等待一个事件,而另一个线程可以触发这个事件,从而唤醒等待的线程。
- 状态:事件只有两种状态——已触发 (Signaled) 和 未触发 (Non-signaled)。
- 类型:
- 手动重置事件 (Manual-reset Event):一旦被触发,它会一直保持“已触发”状态,直到有代码明确地调用
ResetEvent 函数将其重置为“未触发”。它可以同时唤醒所有正在等待它的线程。
- 自动重置事件 (Auto-reset Event):一旦被触发并成功唤醒一个等待的线程后,它会自动变回“未触发”状态。就像一个只能一个人通过的旋转门。
- 操作函数:
CreateEvent, SetEvent (触发), ResetEvent (重置), WaitForSingleObject, WaitForMultipleObjects (等待)。
- 可见性:可以是匿名的(只在当前进程内可见),也可以是命名的(可以跨进程共享和访问)
在 Windows 中,消息 (Message) 是 GUI 子系统(USER32) 用来与应用程序窗口进行通信的数据结构,它驱动着整个 Windows 用户界面的交互
消息特征:
- 用途:用于通知应用程序发生了某个“有趣的事情”。这些事情大多与用户输入或窗口状态变化有关。
- 结构:一个消息通常是一个
MSG结构体,包含以下关键信息:
hwnd: 消息的目标窗口句柄。
message: 消息的类型(一个唯一的ID,如 WM_KEYDOWN, WM_LBUTTONDOWN, WM_PAINT)。
wParam: 附加信息1(具体含义取决于消息类型)。
lParam: 附加信息2(具体含义取决于消息类型)。
- 来源:
- 硬件输入:鼠标移动、点击,键盘按下等。操作系统捕获硬件中断,将其转化为消息。
- 系统通知:窗口需要重绘 (
WM_PAINT),窗口被销毁 (WM_DESTROY)。
- 程序内部:程序可以自己给自己或其他窗口发送/投递消息(使用
SendMessage 或 PostMessage)。
- 处理流程 (消息循环):
- 操作系统将消息放入对应线程的消息队列 (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
- 窗口管理消息 (Window Management Messages)
- 用户输入消息 (User Input Messages)
- 键盘消息
- 鼠标消息
- 绘图消息 (Painting Messages)
- 控件通知消息 (Control Notification Messages)
- 用户自定义消息 (User-Defined Messages)
- 剪贴板消息 (Clipboard Messages)
Windows 系统消息队列与应用程序消息队列:事件触发 -> MSG ->系统消息队列 -> 应用消息队列 -> 消息循环 -> 处理消息

Win32 GUI
Java GUI 编程和 Win32 GUI 编程在最终目标(创建图形用户界面)上是相似的,但在实现方式、抽象层次、开发体验、跨平台性和底层机制上存在巨大差异,可以说是两种截然不同的范式
- 抽象层次与直接性:
-
Win32 GUI: 极其底层。开发者直接调用 Windows 操作系统提供的原生 API(主要是 user32.dll 和 gdi32.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 引擎处理(可以使用硬件加速)。
- 事件处理模型:
-
Win32 GUI: 基于消息传递。所有用户交互(点击、按键、移动鼠标、重绘请求)都转化为特定的 WM_* 消息,被投递到应用程序的消息队列中。你的窗口过程 (WndProc) 必须检查每条消息 (msg.message),使用巨大的 switch-case 或 if-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就是回调函数的地址

右键回调函数地址,转到反汇编,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,消息类型uMsg,wParam,lParam,按W可查看所有句柄

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

程序运行后,只有点击“单选按钮”这个子窗口,程序才会再次停在该断点
资源文件
在 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
2
|
SetDlgItemText(hwndDlg, IDC_EDIT1, L"默认文本");
CheckDlgButton(hwndDlg, IDC_CHECK1, BST_CHECKED);
|
-
设置图标:
1
2
|
HICON hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_APP_ICON));
SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
|
-
设置默认焦点:
1
|
SetFocus(GetDlgItem(hwndDlg, IDC_EDIT1));
|
-
子类化控件(高级用法):
1
2
|
SetWindowLongPtr(GetDlgItem(hwndDlg, IDC_LIST1), GWLP_WNDPROC,
(LONG_PTR)MyListProc);
|
-
加载初始数据:
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 项目
- 打开 Visual Studio,点击“创建新项目”。
- 选择 “Windows 桌面向导”(C++),点击“下一步”。
- 填写项目名称,点击“创建”。
- 在“配置新项目”界面,选择“Windows 应用程序”,点击“创建”
添加Dialog资源
- 在“解决方案资源管理器”中,右键点击“资源文件(.rc)”,选择“打开”。
- 在资源视图中,右键点击“Dialog”,选择“插入对话框”
编辑对话框(添加控件)
- 双击新建的对话框(如 IDD_DIALOG1),进入对话框编辑器。
- 在左侧工具箱(Toolbox)中,可以拖拽以下控件到对话框上
响应控件事件(按钮点击等)
- 在对话框类的消息映射中添加响应函数。
- 以 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);
|
六、添加图片资源
- 在资源视图中,右键“资源”->“导入”,选择图片(如 BMP)。
- 在 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_COMMAND(WM_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 上可真正并行运行
线程生命周期
- 创建:使用
CreateThread() 创建线程
- 运行:线程执行其函数
- 挂起:可暂停执行(
SuspendThread())
- 恢复:继续执行(
ResumeThread())
- 终止:线程函数返回或调用
ExitThread()
- 清理:关闭线程句柄(
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> 头文件中,其主要作用是:
- 保存和恢复线程状态:捕获线程执行时的完整寄存器状态
- 异常处理:当发生异常时,系统提供 CONTEXT 结构显示异常发生时的状态
- 调试器实现:调试器使用它来检查和控制被调试线程
- 线程操作:挂起/恢复线程时保存状态
结构定义(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
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
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
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)**是确保多线程程序正确性的核心机制。它们解决了多线程环境下的共享资源访问问题,防止出现数据竞争和不确定行为
线程互斥
- 概念与目的
-
定义:确保同一时间只有一个线程可以访问共享资源(临界区)
-
目的:防止数据竞争(Data Race) - 多个线程同时修改同一数据导致的不一致
-
锁 (Lock):这就是互斥机制的核心。在 Windows 中,最常用的锁是 互斥体 (Mutex) 和 临界区 (Critical Section)
-
操作流程
- 进入临界区前,先尝试获取锁(锁门)
- 如果锁是空闲的,那么这个线程就成功获取锁,然后进入临界区(进卫生间并锁上门)
- 如果锁已经被其他线程持有,那么这个线程就会被阻塞(在门口排队等待),直到那个线程释放锁
- 离开临界区后,必须释放锁(开门离开),以便其他等待的线程可以获取它
互斥是实现线程同步的基础,它是一种强制性的串行访问,保证了操作的原子性
- 互斥实现机制
互斥锁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;
}
|
线程同步
- 概念与目的
- 定义:协调线程间的执行顺序
- 目的:确保线程按照特定顺序执行(如:生产者-消费者模式)
- 事件 (Event):最常用的同步工具。一个线程可以等待一个事件被触发,另一个线程可以去触发这个事件
- 自动重置事件:像一个旋转门,触发一次只能通过一个等待的线程,然后自动关闭。非常适合“一对一”的通知。
- 手动重置事件:像一个水闸,一旦打开,所有等待的线程都可以通过,直到被手动关闭。适合“一对多”的广播通知。
- 信号量 (Semaphore):一个带计数的锁。允许多个线程(不超过设定的最大数量)同时访问一个资源。就像一个有多把钥匙的房间,或者一个有固定车位的停车场。
- 互斥体 (Mutex):它本身既是互斥锁,也是一个内核同步对象。可以用于跨进程的线程同步,但效率比临界区低。
- 条件变量 (Condition Variable):通常与临界区或互斥锁配合使用,可以实现更复杂的等待/通知逻辑,避免CPU空转。
- 同步实现机制
条件变量(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;
}
|
工作原理:条件变量总是与一个互斥锁配合使用。它允许一个线程在某个条件不满足时,原子地释放锁并进入睡眠,直到另一个线程满足了该条件,并发送通知来唤醒它
工作流程:
- 消费者等待条件满足
- 生产者完成工作后通知消费者
- 消费者被唤醒并检查条件
特点:
- 必须与互斥锁配合使用
- 解决"等待-通知"问题
- 防止忙等待
适用场景:需要等待某个复杂条件成立的生产者-消费者模型,或任何需要精细等待/通知的场景。可以避免使用 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},类似互斥锁
- 计数信号量:控制并发访问数量
适用场景:控制对有限资源的并发访问数量。例如,限制同时连接数据库的线程数,或限制线程池中同时工作的线程数
注意:与互斥锁不同,信号量的 wait 和 post 操作可以由不同的线程执行
互斥体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 - 滴水逆向课程笔记