news 2026/9/29 15:08:43

Windows多线程编程:从线程创建到UI卡死解决

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows多线程编程:从线程创建到UI卡死解决

01 线程创建:先搞清楚你为什么要"多线程"

写 Windows 程序遇到"卡死"几乎是每个初学者都会撞上的墙。我最早用 Win32 API 写一个小工具,主窗口里放一个"开始处理"按钮,点击后要批量读取文件做格式转换。文件一多,窗口立马无响应,拖动标题栏都拖不动,系统还会提示"程序未响应"。当时的第一反应是程序写崩了,后来才明白:所有任务都压在 UI 线程上,消息循环被长时间占用,窗口当然动不了。这就是引入线程创建最直接的理由——不是追技术时髦,而是单线程的模型根本撑不住真实需求。

这个系列面向的是走 Win32 原生路线的开发者,不论你是想搞底层工具、游戏客户端,还是嵌入式上位机,只要用 Windows 的 C/C++ 生态,线程基本绕不开。这篇开篇不谈花哨的并发模型,先回答一个问题:为什么需要线程?把动机嚼碎了,后面讲 CreateThread、讲同步互斥,你才知道每个设计为什么存在。

1. 单线程的困境:为什么你的程序会"卡死"

1.1 被独占的 UI 线程

Windows 的图形界面程序从 WinMain 进入消息循环之后,就靠 GetMessage / DispatchMessage 这一个循环活着。这个循环分发了鼠标点击、键盘输入、窗口重绘等所有消息。问题是:消息循环只有一个线程在执行,如果这条线程在某个消息处理响应里去做了耗时运算,后续所有消息都在队列里排队等着。

用一个最简单的例子说明。你写一个窗口程序,按钮点击后要计算 1 到 100000000 的求和:

case IDC_BTN_START: // 直接在主线程里做耗时计算 long long sum = 0; for (long long i = 1; i <= 100000000; i++) { sum += i; } SetDlgItemText(hDlg, IDC_STATUS, L"计算完成"); break;

运行后你点按钮,界面就像冻住一样。这其实就是单线程模型的天然缺陷,代码本身没写错,是调度方式的局限。

消息循环处理每个消息时,窗口无法响应其他事件。移动窗口、最小化、点击其他按钮,这些操作产生的消息统统排在队列里等待。只要处理函数不返回,界面就一直"假死"。系统在渲染层面也只会保留最后一帧画面,所以背景不变、鼠标手势没反馈,看起来就是卡死了。

1.2 三组常见场景:你其实早就需要线程

卡死的场景总结下来就是三类,都是典型的线程需求信号:

  • 耗时计算:如上所述的循环累加、图像滤镜、数据压缩、加密解密。这些操作占满 CPU 但不需要用户交互,它们霸占主线程时,界面就只能等。
  • 阻塞式 I/O:无论是 ReadFile 读取大文件、connect 网络 socket,还是设备通信等待反馈。这些操作在数据到达之前会让线程睡眠或等待,这段时间窗口完全失去响应。多线程不需要真并行,只要让 UI 线程不被阻塞,哪怕工作线程单核慢慢跑,界面也能保持流畅。
  • 多个独立任务交替进行:比如一边下载文件一边显示进度、一边接收网络包一边响应用户命令。单线程里你要么做这个忘了那个,要么用复杂的轮询逻辑穿插,状态机写到后面自己都绕晕。

这三个场景对应一个核心规律:**只要 UI 线程上存在可能"长时间不返回"的代码路径,就该考虑另起线程。**判断标准很简单——你没法估算这段代码多久能结束,它就是候选的线程任务。

1.3 多线程能解决什么,不能解决什么

多线程不是银弹。它能解决的是"并发等待"和"并行计算"两类问题。前者是任务本身在等 I/O、等网络、等用户事件,一个线程等的时候其他线程能干活;后者是任务可以被拆分成多个独立片段,利用多核 CPU 同时跑。

它无法解决的是:任务本身顺序依赖、数据共享复杂、单线程性能已经足够的情况。如果任务必须算完上一步才能算下一步,你拆成多线程也只能排队等锁,可能反而更慢。如果你只是改了一个小配置项,耗时不到一毫秒,硬起一个线程的开销都比直接算大得多。

线程多了还有同步、死锁、数据竞争一堆副作用,这些后面展开讲。此处先建立概念:**线程是工具,不是装饰品。**线程创建前必须回问——这个任务真有必要并发吗?谁能等到它完成?共享的数据怎么保护?

2. 线程究竟是什么:从进程到线程的Windows底层视角

2.1 进程是容器,线程才是执行者

Windows 里进程是资源容器:它持有虚拟地址空间、句柄表、安全描述符等一系列资源。但进程本身什么也不做,真正被 CPU 调度执行的是进程内部创建的线程。没有线程的进程是"空壳",Windows 允许你创建一个没有线程的进程,但它在用户看来毫无存在感,因为程序入口本身也是靠主线程跑起来的。

用房子打比方:进程是房子,线程是住在里面干活的人。房子里有各种家具和工具(内存、文件句柄、全局变量),所有住客共用这套家当。但不同房子的住户之间不能互相串门(进程地址空间隔离),除非通过特殊的门——进程间通信机制——才能交换东西。

一个进程至少有一个主线程,它从应用程序入口开始执行。你可以在这个进程里创建更多线程,它们和自己的主线程共享同一个地址空间、访问同样的全局变量、使用同样的堆。这就是多线程程序的基本画面:一个房子,多个住户,共用家具。

2.2 线程在内核里的构成:三个关键部分

Windows 线程在内核层面由三部分组成,理解这三块是后续所有并发概念的基础:

组成部分作用通俗理解
内核线程对象内核用它管理线程生命周期,包括调度状态、等待原因、优先级操作系统的"花名册"
线程栈记录函数调用链、局部变量、返回地址,默认 1MB 保留空间干活时的"草稿纸"
上下文(CONTEXT)保存寄存器值、指令指针、栈指针等执行现场干活干到一半的"进度快照"

当操作系统把 CPU 从一个线程切到另一个线程,本质是把当前线程的上下文保存起来,再把目标线程的上下文恢复到寄存器中,然后让指令指针指到目标线程上次停下的位置。听起来简单,实际上一次上下文切换有几十上百个周期的开销,所以"线程越多越快"是完全错误的直觉——线程切换本身是有代价的。

2.3 线程与 CPU 的关系:并行与并发

很多初学者把"并发"和"并行"混着说。严格讲:

  • 并发是多个任务交替执行,宏观上像同时发生,微观上同一时间只有一个任务占据核心。
  • 并行是多个任务真正同时执行,需要多核 CPU 撑腰。

Windows 提供线程调度功能,它根据处理器数量、线程优先级、就绪队列等因子决定哪个线程占用哪个核。线程调度是抢占式的:时间片到了,或者有更高优先级线程就绪,当前线程被强制让出 CPU。这个机制让多线程程序看起来"自动分工",但也埋下了不确定性的根源——你永远不知道线程在哪个指令被调度器打断。这一条后续讲同步时会变成最重要的坑。

3. Windows线程创建实操:从CreateThread到_beginthreadex

3.1 最小可运行的线程示例

直接给代码,用最标准的 Win32 API 创建线程:

#include <windows.h> #include <stdio.h> DWORD WINAPI ThreadProc(LPVOID lpParam) { // 拿到传入的参数 int* pValue = (int*)lpParam; printf("线程开始执行,参数: %d\n", *pValue); for (int i = 0; i < 3; i++) { printf("线程内部: %d\n", i); Sleep(1000); // 模拟耗时操作 } printf("线程执行完毕\n"); return 0; } int main() { int param = 42; HANDLE hThread = CreateThread( NULL, // 安全属性,默认 0, // 栈大小,0表示默认 ThreadProc, // 线程函数地址 &param, // 传给线程函数的参数 0, // 立即运行 NULL // 线程ID,不需要 ); if (hThread == NULL) { printf("线程创建失败,错误码: %lu\n", GetLastError()); return 1; } // 等待线程结束 WaitForSingleObject(hThread, INFINITE); // 释放内核对象句柄 CloseHandle(hThread); printf("主线程结束\n"); return 0; }

这段代码在 Visual Studio 里直接新建控制台项目编译就能跑。核心是 CreateThread 这个 API,它的存在标志着"线程创建"的本字含义——创建一个内核对象,把线程函数挂到调度队列里。

3.2 CreateThread 每个参数到底什么意思

CreateThread 返回一个 HANDLE,这是内核对象句柄。句柄让你能对这个线程做后续操作:等待它结束(WaitForSingleObject)、修改优先级(SetThreadPriority)、终止它(TerminateThread)等。这些操作的共同前提是你必须握着这个句柄。

逐个看参数:

  • lpThreadAttributes:安全描述符。传 NULL 用默认安全属性,子进程无法继承句柄,99% 的场景 NULL 就行。
  • dwStackSize:线程栈大小。传 0 使用模块默认值,Windows 默认每个线程保留 1MB 提交栈空间。如果线程函数里有比较大的局部变量或深递归,可以考虑显式加大,比如 4 * 1024 * 1024(4MB)。大多数情况 0 够用。
  • lpStartAddress:线程函数地址,函数签名必须是 DWORD WINAPI 类型。WINAPI 展开是 __stdcall 调用约定,函数参数从右往左入栈,由被调用者负责栈清理,这是 Windows 系统 API 的标准调用约定。
  • lpParameter:传给线程函数的参数指针。这是线程间通信最朴素的方式——通过一个指针传递任意结构。返回线程执行状态,如果你需要拿到线程的返回值,可以单独用 GetExitCodeThread。
  • dwCreationFlags:0 表示立即运行;CREATE_SUSPENDED 表示先挂起线程,直到你调用 ResumeThread 才启动。这用于提前设置优先级或线程名称后再激活;CREATE_SUSPENDED 本身也是个调试技巧,可以避免线程一创建就跑了,等你准备好再放行。
  • lpThreadId:可选参数,返回线程 ID。如果你不在乎,传 NULL。线程 ID 是系统全局唯一的标识,但注意句柄和 ID 不是一回事:句柄是你在当前进程里的引用,ID 是系统的名字。

3.3 CreateThread 还是 _beginthreadex?

这个问题在 Win32 开发里属于"老程序员一定会问"级别的话题。直接给结论:在 C/C++ 程序里,用 _beginthreadex 而不是 CreateThread。

为什么?因为 CreateThread 是 Windows API,它完全不懂 C 运行库(CRT)。C 程序里经常用到 malloc、printf、strtok 这些 CRT 函数,你去看 CRT 源码会发现很多内部函数使用了 _errno、tiddata 这类线程局部存储(TLS)。CreateThread 创建出来的线程没有初始化 CRT 的 TLS 数据,一旦在线程函数里调用 malloc、printf,轻则数据错乱,重则崩溃。

_beginthreadex 在创建线程之前会分配并初始化这块 TLS 区域,创建完线程后释放。它和 CreateThread 的参数几乎一致,只多了 unsigned 修饰的线程函数和参数类型,用法上唯一的区别是线程函数返回 unsigned,但实际使用基本无感:

#include <process.h> unsigned __stdcall ThreadFunc(void* param) { // 随便用 malloc / printf return 0; } HANDLE hThread = (HANDLE)_beginthreadex( NULL, 0, ThreadFunc, &param, 0, NULL );

如果你写的是纯 Win32 代码,不碰 CRT(比如用 Visual Studio 新建的 Windows 桌面程序也可能用到 CRT 的字符串函数),用 CreateThread 也能跑。但为了稳妥,MSDN 官方也推荐在 C/C++ 代码里使用 _beginthreadex。我自己在项目里统一走 _beginthreadex,避开一个隐藏雷区。前提是你的 CRT 是"正常初始化"的——如果你的项目指定的入口是 mainCRTStartup / WinMainCRTStartup(正常默认),_beginthreadex 是安全的。

顺带一提,以前老旧资料里还出现过 CreateThread 和 _beginthread 的对比,后者返回的是线程句柄但语义不完整,_beginthread 在线程退出时会自动关闭句柄,反而容易因为句柄提前关闭导致 WaitForSingleObject 拿到无效句柄。_beginthreadex 则明确要求你手动 CloseHandle。所以记住:C/C++ 场景选 _beginthreadex,纯 C 库且不涉及 CRT 场景才选 CreateThread。

3.4 线程终止:正常返回 vs ExitThread vs TerminateThread

线程函数 return 是最干净的退出方式。返回值通过 GetExitCodeThread 可以主动获取。比如线程函数 return 1,主线程调 GetExitCodeThread 就能拿到 1,这能作为线程执行结果的传递通道。

也有两个不太干净的退出方式:

  • ExitThread:在线程函数内部调用立即退出当前线程。这有点类似 exit 对进程的粗暴退出——它会跳过一些清理代码,C++ 的局部对象析构不会执行(因为栈帧没有被正常 unwrap)。能用 return 就别用 ExitThread。
  • TerminateThread:从另一个线程强制"枪毙"目标线程。它不执行 DLL 的线程分离通知、不执行栈解退、不释放线程持有的锁。被 TerminateThread 的线程如果正好持有一个临界区或堆锁,程序后续极可能死锁或内存崩溃。这 API 基本只用于紧急关机,比如检测到线程彻底卡死在不可等待区域时采用,日常代码不该出现。

线程函数 return 后,线程走到了执行流末尾,系统负责清理线程内核对象和栈。但注意:**句柄不关闭,内核对象不会立即销毁。**所以主线程里用完句柄要 CloseHandle,这叫资源管理常识。你若不关闭,每次创建线程就会泄漏一个内核句柄——程序跑上上千次线程操作,任务管理器里句柄数会持续增长。

4. 线程可不是免费的:同步、竞态与正确退出

4.1 线程间共享数据:不锁就翻车

多线程的优势来自共享内存,坑也来自共享内存。你用两个线程同时对一个全局变量做累加:

volatile long g_counter = 0; DWORD WINAPI ThreadProc(LPVOID) { for (int i = 0; i < 1000000; i++) { g_counter++; } return 0; }

两个线程各加 100 万次,期望结果是 200 万,实际跑出来经常是 180 万、190 万这种数字。原因在底层:g_counter++ 在 CPU 级别不是一条指令,它至少是"读-改-写"三步。线程 A 读到了当前值,线程 B 也读到了同样的值,然后各自加一再写回,其中一个人的修改被覆盖了。

更麻烦的是现代 CPU 的多级缓存架构。每个核心有自己的 L1/L2 缓存,线程可能在不同核心上跑,写回主内存的时机不一致。即使加了 volatile,也保证不了原子性和内存可见性——volatile 在 MSVC 下对多线程的语义和 C++11 标准指定的 std::atomic 有差异,从 C++ 标准角度讲,volatile 不解决数据竞争。经典教训就是:多线程共享变量,必须用同步原语或原子操作。

在这里给三句话的入门指导:

  1. 如果只是计数器、标志位这种简单场景,用 InterlockedIncrement / InterlockedExchange 系列原子函数,或者 C++11 的 std::atomic。
  2. 如果涉及多步操作,比如"读配置-改配置-写配置",原子函数救不了你,得用锁。
  3. 如果锁的范围一变大,注意性能和死锁风险。

4.2 临界区与互斥量的入门用法

Windows 下最轻量的线程互斥原语是 CRITICAL_SECTION(临界区),锁开销极小,适合进程内线程互斥。基本用法:

CRITICAL_SECTION g_cs; InitializeCriticalSection(&g_cs); // 进入保护区 EnterCriticalSection(&g_cs); g_counter++; // 离开保护区 LeaveCriticalSection(&g_cs); // 程序结束 DeleteCriticalSection(&g_cs);

关键点:EnterCriticalSection 会阻塞线程直到获得锁。如果线程 A 在保护区里卡住了,线程 B 就一直等着,无法被抢占。保护区的代码要尽可能短,别把 Sleep、文件读取、网络通信这些慢操作放进去,否则锁的范围变大,并发性能直线下降。

进程间的互斥得用 Mutex(互斥量),它和临界区本质都是锁,但 Mutex 是内核对象,可以跨进程用。使用上 InitializeCriticalSection 替换为 CreateMutex,等待函数是 WaitForSingleObject 而不是 EnterCriticalSection。这两个的区别要牢记:临界区更轻,但不跨进程;Mutex 能跨进程,但创建内核对象开销稍高。

4.3 等待线程退出:WaitForSingleObject 与阻塞陷阱

线程创建后,主线程往往要等它结束拿结果。WaitForSingleObject 是最常用的等待函数:

// 第二参数是等待毫秒数,INFINITE 表示无限期等待 DWORD ret = WaitForSingleObject(hThread, INFINITE); if (ret == WAIT_OBJECT_0) { // 线程已结束 }

这看起来简单,实际有个很常见的误导:INFINITE 等待意味着如果线程一直不退出,调用线程就永久阻塞。如果主线程还需要处理窗口消息,你千万别在主线程里直接 WaitForSingleObject(hThread, INFINITE) 等工作线程——这会把 UI 又卡死一次,白费了当初"线程让 UI 流畅"的目的。

正确的做法是:主线程不等,直接返回,然后在线程函数内部把结果投递回去。或者用消息通知的方式,等线程完成后给主窗口 PostMessage。一个典型的"异步工作线程 + UI 反馈"模式是:

DWORD WINAPI WorkerThread(LPVOID param) { // 执行耗时操作 // 最后把结果通过 PostMessage 发送给窗口 HWND hWnd = (HWND)param; PostMessage(hWnd, WM_UPDATE_UI, 0, (LPARAM)&result); return 0; }

4.4 线程退出时的资源清理陷阱

线程函数 return 后,栈上的局部变量自动析构(C++),内核句柄不会自动关闭,CRT 的 TLS 数据会被释放。这套机制本身没问题,但有一个隐蔽陷阱:**如果线程函数里创建了其他内核对象(文件句柄、事件句柄等),return 时不会自动关闭。**你必须自己记得 CloseHandle。

另一个陷阱是全局变量和静态变量的生命周期。多个线程共享的全局变量,如果某个线程持有指向动态分配内存的指针,而另一个线程退出时释放了这块内存,剩下的线程访问就会访问野指针。这就是内存安全问题的经典战场。

多线程程序的资源管理,最靠谱的方法是:**每个线程自己负责自己创建的资源的清理,跨线程共享的内存要有统一的所有权约定。**约定文档写清楚,谁创建谁释放,绝不暧昧。

5. 线程创建的进阶实践:从"能跑"到"好跑"

5.1 线程数量不是越多越好

多线程最经典的误区是"线程越多越好"。Windows 线程创建不是免费的:每次创建线程要分配内核对象和栈,大概几十微秒的代价,还有内存消耗(默认 1MB 保留栈,提交一页左右)。更致命的是,线程多了以后,调度开销和上下文切换成本会逐渐盖过并行收益。

经验法则是:**CPU 密集任务的线程数,一般不超过 CPU 逻辑核心数。**I/O 密集任务可以多一些,但也不是无上限。在 Windows 上想查询核心数很简单:

SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); printf("处理器数量: %u\n", sysInfo.dwNumberOfProcessors);

需要反复创建和销毁线程的场景,最好直接考虑线程池。Windows 提供了 CreateThreadpool 接口,也兼容 C++11 的 std::thread + std::async。线程池帮你管理线程生命周期,避免频繁"创建-销毁"带来的积压开销。

5.2 线程参数传递:小心栈上临时变量

看代码的时候特别注意这里。CreateThread 的参数是个 void*,你可以传任何东西,但传指向栈上临时变量的指针是最常见的雷。例子:

for (int i = 0; i < 10; i++) { HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, &i, 0, NULL); // 所有线程可能拿到同一个 i 的地址,而且 i 的值一直在变 }

糟糕之处在于:所有线程共享同一个 i 的栈地址,循环结束时 i 已经是 10,每个线程拿到的可能都是 10。第二个方案是在循环内分配局部一块内存,把值拷贝进去,再把地址传给线程,线程结束时自己释放这块内存。第三个方案是给每个线程分配独立的数组项。总之,线程参数必须指向稳定、在整个线程生命周期内不会变更的生命周期的内存。

5.3 调试多线程程序:附加进程与输出追踪

写多线程程序,调试器体验比单线程糟十倍。常规手段是会"暂停所有线程"的和"只暂停当前线程"之分:Visual Studio 调试时默认新线程碰到断点会中断,你可以选择"允许其他线程继续运行";这个选项对排查死锁、竞态特别重要——你可以在断点处 F10 单步,只让其他线程继续,观察数据怎么被改动。

打印输出是另一个利器。在多线程环境里,printf/OutputDebugString 输出要加锁,不然多条线程的输出会穿插乱序。Windows 的 OutputDebugString 本身在系统层面就是串行化的,用它来做调试输出比 printf 安全些。如果要用 printf,自己加一个输出锁:

CRITICAL_SECTION g_printCs; void SafePrint(const char* fmt, ...) { EnterCriticalSection(&g_printCs); va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); LeaveCriticalSection(&g_printCs); }

再配合线程 ID 输出(GetCurrentThreadId),你能清晰追踪每行日志来自哪个线程,排查竞态时思路会清晰很多。

5.4 两个容易忽略的细节:栈大小与线程命名

线程栈大小用默认值 1MB 时,如果线程函数里创建超大局部数组(例如 int buff[512][512],约 1MB),可能直接爆栈崩溃。遇到这种场景要主动指定 dwStackSize,比如 _beginthreadex(..., 4 * 1024 * 1024, ...)。Windows 默认栈大小是 1MB(32 位下),64 位下也是 1MB,所以 4MB 是常见的显式配置。

线程命名这个技巧也值得记录。Visual Studio 的调试器在"线程"窗口里能看到线程 ID,但看不见语义。用 SetThreadDescription API(Windows 10 1607+)可以给线程起名:

#include <winnt.h> SetThreadDescription(GetCurrentThread(), L"文件处理线程");

老一点的系统得用异常法(设置线程名称异常),因为 SetThreadDescription 在老系统不可用。给线程命名对稍复杂的项目价值极高——调试器里一眼看出哪个线程在做什么。

6. 线程创建的常见踩坑清单与实测经验总结

6.1 线程创建失败:GetLastError 能告诉你的

线程创建失败的几率很低,但不代表不会发生。常见原因是系统资源耗尽——创线程太频繁,内核句柄和内存被耗光。排查方法是看 CreateThread/_beginthreadex 返回的 NULL,立刻调 GetLastError 拿错误码。如果是 ERROR_NOT_ENOUGH_MEMORY(8),基本就是内存或句柄泄漏。

另一个隐藏失败点:线程函数地址错误。函数指针签名不对时(比如用了 cdecl 约定而不是 __stdcall),编译可能警告,运行时第一次调用就崩溃。所以线程函数务必写全签名:DWORD WINAPI 或 unsigned __stdcall。

6.2 主线程退出,子线程还活着

main 函数 return 后进程就结束了,所有线程被系统强制终止。很多人没意识到这个,结果日志分支里看到子线程没执行完就没了,以为代码有 bug。

所以如果你的工作线程需要继续跑,主线程必须等待它结束——用 WaitForSingleObject,或者让工作线程注册到全局队列等待主线程遍历等待。这个机制一定要清楚:进程退出是"推土机",不管你线程跑没跑完。要么不退出主线程,要么确保退出前所有线程都收尾完。

6.3 一个实战中的典型例子:文件批量处理的并发改造

我给一个辅助工具做了并发改造,把单线程的批量文件转码改成 4 线程并行。改造前:500 个文件,每个 50ms 处理时间,总共 25 秒,界面完全卡死。改造后:4 线程同时处理,总耗时缩到 6-7 秒,界面还能拖动。

实现要点有三:一是创建 4 个工作线程,每个线程从共享任务队列取文件;二是队列有锁保护,用 CRITICAL_SECTION 包住取任务动作;三是 UI 线程不等待线程结束,而是线程每处理完一个文件就 PostMessage 刷新进度。核心思路是把"任务分配"和"任务执行"分离,UI 只做显示。

关键代码如下:

// 共享任务队列 CRITICAL_SECTION g_queueCs; std::queue<std::wstring> g_fileQueue; DWORD WINAPI WorkerThread(LPVOID) { while (true) { EnterCriticalSection(&g_queueCs); if (g_fileQueue.empty()) { LeaveCriticalSection(&g_queueCs); break; } std::wstring file = g_fileQueue.front(); g_fileQueue.pop(); LeaveCriticalSection(&g_queueCs); // 真正的转码工作,不持锁 ConvertFile(file.c_str()); // 通知 UI 更新 PostMessage(g_hWnd, WM_PROGRESS, 0, 0); } return 0; }

这个模式本质是"线程池"的简化版。只要你按这个思路走,批量任务的并发性能基本靠谱,健壮性也能保证。真正的线程池只是在这个模式上加上了线程复用和任务调度,思路一模一样。

6.4 最后,回顾一下"为什么需要线程"这个标题

写这个系列的开篇时,我的想法很简单:你得先知道自己是来解决什么问题的,才谈得上怎么解决。线程创建的学习曲线容易让你一头扎进 CreateThread 的细节里,反而忘了出发点。出发点永远是需求——某个任务的执行会挡住其他任务的脚步,才是线程登场的时候。

这篇把线程的最核心逻辑都过了一遍:为什么需要线程、线程本质是什么、怎么创建、怎么同步、怎么退出。有了这些基础,后面讲线程同步、讲锁的粒度、讲生产者消费者模式时,你可以直接对照本文的概念来理解。Windows 开发里线程是一块硬骨头,啃下来之后,你会发现自己对程序运行的理解完全变了一个层次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 15:08:03

内容被写进了哪个文件:重定向这一层的三条规则

授权与合规声明 本文全部操作对象均为自建隔离靶场&#xff08;本机容器或隔离虚拟机&#xff09;&#xff0c;涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款&#xff0c;须承担相应法律责任。本文只讲环…

作者头像 李华
网站建设 2026/9/29 15:07:51

光盾苍穹:国防、航天与安全的光电融合革命

国防、航天与安全是光电融合技术中“要求最极端、对抗最激烈、战略价值最高”的领域。在这里&#xff0c;光电融合的核心逻辑是&#xff1a;光承担高速保密通信、高精度侦察感知、定向能毁伤和量子安全密钥分发&#xff0c;电承担实时决策、火控解算、多源融合和平台控制&#…

作者头像 李华
网站建设 2026/9/29 15:02:57

Python如何输出变量的类型

在&#xff08;中&#xff09;, 输出变量的类型的方法之一是使用内置函数type(), 另外一种方法是通过运用()函数来判断类型, 也可以利用属性来获取类型, 还可以使用types模块。其中, 使用type()函数这种办法是最常见的一种。具体的实现过程可以通过以下步骤来完成:# 示例代码x …

作者头像 李华
网站建设 2026/9/29 15:01:28

在日常办公中,Excel文件处理是高频需求。Python的pandas库可以高效完成数据读取、清洗、计算和导出任务

Python作为一种高级编程语言&#xff0c;凭借其简洁的语法、丰富的库生态和强大的跨领域应用能力&#xff0c;已成为当前最流行的编程语言之一。本报告将围绕Python在自动化办公与数据分析领域的实际应用展开&#xff0c;通过具体代码示例、详细解析和项目亮点&#xff0c;展示…

作者头像 李华