先说我为什么想把 pthreads4w v3.0.0 的源码编译过程专门写一遍。最近在 Windows 上移植一个原本跑在 Linux 下的网络服务组件里面大量用到了 pthread 的线程接口。网上能直接下到的 pthreads4w 预编译包大多停留在旧版本要么只有 x86要么 Debug 和 Release 混着来要么库的命名跟我项目的构建链根本对不上。折腾一圈之后我决定不偷懒了直接自己动手源码编译 v3.0.0后面所有工程都用自己的产物。这篇就完整记录我从拿到源码到编出可用库文件的全部过程包括几个我踩完之后觉得必须提醒别人的坑。如果你也是因为某个开源项目绕不开 pthread API、又必须在纯 Windows 环境里编出 64 位/32 位库的人这篇应该能让你省掉不少时间。1. 为什么要费劲自己编译而不是直接拿现成包1.1 pthreads4w 到底解决了什么问题pthreads4w 这个项目的前身大家更熟悉叫 pthreads-win32。它做的事情很纯粹在 Windows 平台上提供一份接近 POSIX 标准的线程 API 实现。说得再直接一点就是让pthread_create、pthread_mutex_lock、pthread_cond_wait这些函数在 Windows 的进程模型里真的能跑起来。Windows 本身的线程机制和 POSIX 线程在语义上有差别。最典型的是互斥锁和条件变量Linux 上的pthread_mutex_t通常是一个很小的结构体可以静态初始化Windows 上对应的是 CRITICAL_SECTION 或各种WaitForSingleObject的组合用法差异很大。如果你的项目里已经有大量基于 pthread 写的业务逻辑要在 Windows 上重新用 Win32 API 改写一遍工作量根本不是改几个函数名那么简单涉及锁的超时、析构、线程局部存储、屏障等一堆语义细节。这种时候直接引入 pthreads4w把 API 层对齐是最稳也最省事的路线。pthreads4w 覆盖得很全不光是基础的线程创建和退出还包括线程私有数据pthread_key_t对应pthread_getspecific/pthread_setspecific读写锁pthread_rwlock_t条件变量包括带超时的pthread_cond_timedwait信号量sem_t系列接口屏障pthread_barrier_t线程取消pthread_cancel线程调度参数接口如果你的代码里用到了这些里的任何一项pthreads4w 就基本是不可替代的。1.2 v3.0.0 和旧版本最大的不同如果你以前用过 pthreads-win32你多半对那套老的构建方式有印象仓库里躺着一堆.sln、.vcxproj或者一个大型的 GNU Makefile。你说不上它哪里不好但每次想调整一个编译选项、换个编译器版本、或者想集成到自己的 CMake 工程里都会隐隐觉得别扭。pthreads4w v3.0.0 是把构建系统整体迁移到了 CMake。官方 CMakeLists.txt 已经写好你在 Windows 上可以像编译其他现代 C 项目一样cmake -S . -B build cmake --build build --config Release这对我来说是很大的解脱因为我的主项目就是用 CMake 组织的把 pthreads4w 作为外部依赖编进来不用再写一堆自定义规则去调用它老旧的 Makefile 了。v3.0.0 还引入了一个叫 “isolation” 的选项我记得对应的 CMake 缓存变量和宏跟 PTHREADS4W_ISOLATION 有关。它能更好地区分线程库在不同 DLL 之间的加载边界规避一些跨模块使用线程句柄时的归属问题。这个在插件架构、动态加载 SDK 的项目里特别有用。旧版本想要类似效果得自己想办法现在有官方选项了。另外v3.0.0 的源码目录结构也比老版本清爽source下面按功能拆分了pthread.c、barrier.c、cleanup.c、errno.c这些文件不再是一个巨大无比的单文件pthread.c。排查问题、改代码的体验好很多。1.3 什么情况下源码编译是必须的我整理了一下大致是这几类情况你可以对照自己是不是在里面官方预编译包不覆盖你的架构。比如你需要 x64 的 Debug 版静态库或者需要 Win32 的 Release 动态库网上找的包未必都齐全。你需要调整运行时库选项。比如你的项目统一使用/MT静态链接运行时库而预编译的 pthread 库可能是/MD编译的混用会在链接阶段出问题最典型的就是 LNK2038 之类的运行时库不匹配错误。你需要集成到自己的 CMake 超级工程里用FetchContent或add_subdirectory直接编进去而不是手动把 lib 拷来拷去。你需要改源码比如调试某个底层 API、增加日志、或者裁剪用不到的模块。你不想信任来路不明的二进制。源码编译虽然费点功夫但每一行代码都摆在那。我这次就是前三条全占所以没有任何犹豫直接源码编译。环境准备上我用的是 Visual Studio 2022 CMake 3.28 的组合下面是完整记录。2. 动手前的环境准备与工具链选型2.1 工具清单你真正需要的东西就三样Visual Studio、CMake、以及能拉源码的工具。我用的是工具版本/组件用途Visual Studio 2022需要“使用 C 的桌面开发”工作负载提供 MSVC 编译器、Windows SDK、链接器CMake3.28 或更高生成构建系统Git任意较新版本拉取 v3.0.0 tag 的源码Visual Studio 的最低版本我建议不要低于 2019。pthreads4w v3.0.0 的 CMake 里对编译器版本有基础检查太老的工具链可能在 Windows SDK 版本匹配和 C11/C17 支持上有麻烦。C 标准方面默认按 C11 / C17 走VS2019 16.8 以上已经能很好地支持。这里有个小细节提醒安装 VS 的时候别只装“通用 Windows 平台开发”一定要勾上“使用 C 的桌面开发”否则系统里根本没有cl.exe、没有 Windows SDK 的头文件CMake 配置阶段就会报无法识别编译器。2.2 x64 还是 Win32先想清楚再动手pthreads4w 的源码是同一份理论上支持各架构但必须通过 CMake 的-A参数指定平台架构。我的项目是 x64 的所以我用的是cmake -S . -B build -A x64如果你的依赖需要 32 位的 DLL很多老插件和第三方组件还是 x86 的那就用-A Win32。需要特别注意-A只能在第一次配置时设置配置完之后再想切架构最干净的做法是删掉 build 目录重新来一遍。不要想着在一个 build 目录里反复横跳。还有一个我见过不少同学踩的坑VS 自带的 Developer PowerShell 和普通 PowerShell 打开以后的默认环境变量不一样。如果你在命令行里敲cmake -S . -B build时用的是普通 PowerShellCMake 可能找不到 MSVC 编译器报错类似 “CMAKE_C_COMPILER not found”。解决办法很简单用“x64 Native Tools Command Prompt for VS 2022”或者 Developer PowerShell 来运行命令。我后面所有命令都是在“x64 Native Tools Command Prompt”里执行的没有出现编译器和环境问题。2.3 获取源码版本 tag 与目录规划pthreads4w 的 GitHub 仓库里v3.0.0 是有对应 tag 的。我建议你不要直接拉 master 分支去做“正式”构建除非你就是想尝鲜开发版而是用 tag 固定版本保证可复现git clone https://github.com/pthreads4w/pthreads4w.git cd pthreads4w git checkout v3.0.0目录规划也有讲究。我把源码放在D:\libs\pthreads4w\src然后打算把最终产物放到D:\libs\pthreads4w\install。这样一来其他项目引用这个库的时候只需要跟 install 目录打交道不用关心源码里乱七八糟的中间文件。3. 源码编译完整实操从 CMake 配置到出库3.1 CMake 配置命令和参数含义这是整个过程的核心。我最终使用的配置命令是cmake -S . -B build -G Visual Studio 17 2022 -A x64 \ -DBUILD_SHARED_LIBSON \ -DBUILD_TESTSOFF \ -DCMAKE_INSTALL_PREFIXD:/libs/pthreads4w/install逐项解释一下免得你盲目复制以后不知道发生了什么-S . -B build指定源码目录为当前目录构建目录是build。CMake 的所有中间文件都会扔到build下源码目录保持干净。-G Visual Studio 17 2022显式指定生成器。因为我已经在 VS 环境里这行其实可以省但写上更保险。-A x64指定架构。-DBUILD_SHARED_LIBSON决定生成动态库还是静态库。ON会生成 DLL 导入库如果设成OFF则编译出静态链接库.a或.lib。这个变量的语义遵循 CMake 惯例pthreads4w 的 CMakeLists 也是按这个惯例走的。-DBUILD_TESTSOFF不编译自带的测试程序。我第一次没关编到一半要么编译测试要么链接测试失败白白浪费时间。如果你是想研究库的行为后面可以单独开。-DCMAKE_INSTALL_PREFIX...指定后续install时产物的安装根目录。另外还有一个值得一提的选项是跟异常语义有关的。pthreads4w 内部对线程取消、转储等行为的实现分不同模型CMake 配置阶段有一个变量控制是构建 C 异常模型还是 C 异常模型具体名字不同版本略有差异你可以在配置完成后用cmake -L build查看所有可配置缓存变量。它会打印出这个项目支持的 CMake 选项列表比看 README 更直接。我当时就通过这个命令找到了几个跟isolation、gc相关的开关。我的建议是第一次先全部用默认值只改BUILD_SHARED_LIBS和BUILD_TESTS把默认流程跑通再回头看要不要开高级选项。3.2 编译命令与配置切换配置完成后执行编译cmake --build build --config Release --parallel这里--config Release是因为 VS 多配置生成器会产生 Release/Debug/MinSizeRel/RelWithDebInfo 多套配置编译时必须明确选一套。--parallel让 MSBuild 并行编译速度会快不少。我这个机器上大概不到两分钟全部编完没有任何报错。如果你需要 Debug 版可以再执行cmake --build build --config Debug --parallel同一套 build 目录可以同时产出 Debug 和 Release 两个配置的产物它们会分别放在build/Release/和build/Debug/下互不干扰。这一点在后续集成其他项目时非常方便别人的动态库只需要一个 Release DLL而你自己的调试工程却需要 Debug 版本的 pthread 库。3.3 产物集中存放install 与自定义安装目录编译出来的库和头文件最初散落在build/Release等目录里用起来很乱。我习惯执行一次 installcmake --install build --config Release如果你前面设置了CMAKE_INSTALL_PREFIX这会把头文件、库文件、CMake 配置文件都拷贝到那个目录下。没有设置的话默认会装到C:\Program Files\pthreads4w之类的位置容易碰到权限问题所以强烈建议显式设置。装完之后我的目录结构是这样的D:\libs\pthreads4w\install\ ├── include\ │ ├── pthread.h │ ├── sched.h │ └── semaphore.h ├── lib\ │ ├── cmake\ │ │ └── pthreads4w\ │ │ ├── pthreads4w-config.cmake │ │ └── pthreads4w-targets-release.cmake │ ├── pthreadVC3.lib (动态库导入库) │ ├── libpthreadVC3.a (静态库文件如果你的 BUILD_SHARED_LIBSOFF) │ └── libpthreadVCE3.a (或者对应的 C 异常语义版本) ├── bin\ │ └── pthreadVC3.dll看到cmake目录下有pthreads4w-config.cmake没有这意味着你可以通过find_package(pthreads4w)把它集成进 CMake 工程不需要手动管理包含目录和库文件路径。这是 v3.0.0 迁移到 CMake 之后给我最大的一个便利。4. 认识一下编译产物DLL/LIB/A 文件的来龙去脉4.1 动态库 vs 静态库的选择也许你会跟我一样面对 CMake 里BUILD_SHARED_LIBS这个选项有点纠结。我先说结论如果你不是要打一个完全单文件发布的小工具优先选动态库。动态库DLL的好处是多个进程和组件可以共享同一份 pthread 实现内存占用小升级 pthreads4w 时只要替换 DLL 不用重新编译整个主程序。坏处是部署时需要把 DLL 一起带上否则运行时报“找不到 pthreadVC3.dll”。静态库.a/.lib文件的好处是链接进主程序后不依赖外部 DLL发布就是一个 exe。坏处是如果系统里有多个模块都静态链接了 pthread每个模块会维护自己的一份线程上下文跨模块传pthread_t、或者跨模块执行清理逻辑时容易出奇奇怪怪的问题。这正是我前面提到 v3.0.0 加 isolation 选项想缓解的场景之一。如果你决定用静态库需要记得同时定义一个宏PTHREADS4W_STATIC老 pthreads-win32 里也有类似的宏。这个宏的作用是告诉头文件不要声明__declspec(dllimport)。不然链接阶段会提示一堆未解析的外部符号。这个坑我在第一次用静态库时踩过印象特别深。大小方面Release 配置下我这边编出来的 pthreadVC3.dll 大约 120KB 左右静态库文件大约 140KB 左右差别不算大所以不用太纠结体积。4.2 命名后缀VC、VCE、VSE 和 GC 语义你编译完以后会发现库文件名后缀不止一种常见的有文件名含义pthreadVC3.dll / pthreadVC3.lib使用 C 运行时、C 异常取消模型pthreadVCE3.dll / pthreadVCE3.lib使用 C 异常取消模型pthreadVSE3.dll / pthreadVSE3.lib使用结构异常处理SEH取消模型“3”代表版本系列对应 pthreads4w v3.x / 系列接口版本。我平时用的最多的是VC这个后缀因为它对纯 C 项目最友好。如果你的代码里有 C 代码并且依赖 pthread 取消功能去跑 C 析构函数可以考虑VCE。大部分普通业务代码用VC就足够了。这里还得解释一个很容易被误解的概念pthreads4w 里的GC。它会出现在一些中间产物或文档里比如pthreadGC3.dll这种老命名。这里的 GC 不是垃圾回收而是指线程句柄的“自动回收”语义线程退出后如果没有被 join资源会被自动清理不会造成句柄泄漏。这是 pthreads-win32 为了兼容老代码的默认行为。严格 POSIX 标准其实要求线程退出后必须由另一个线程 join 来回收资源。v3.0.0 里你依然能看到相关选项默认打开。这个对大部分应用没影响但如果你严格依赖“必须先 join 才能销毁线程句柄”的 POSIX 语义需要自己确认一下库的默认配置。4.3 这些库怎么和你的项目对应拿到 lib 文件后第一件事是确认它和你的项目配置对得上你的项目是 x64库也必须来自-A x64的构建。你的项目是 Debug 构建最好链接 Debug 版的 pthread 库避免调试时变量内容被优化掉导致误判。你的项目是/MD动态运行时库pthread 库也应该用/MD编译如果你是/MT那最好自己用/MT编一份 pthreads4w。v3.0.0 用 CMake 编译时运行时库选项默认跟随 CMake 的配置但如果你在超级工程里用add_subdirectory集成可以给它单独设置 MSVC_RUNTIME_LIBRARY 属性。如果你在集成测试时发现锁的等待行为不对、死锁复现不了、或者跨 DLL 传递锁句柄时崩溃先别怀疑 pthread 的算法优先去查“库的运行库选项和主程序是否一致”“是不是 Thread 句柄跨模块了”这两个问题。5. 编译链路里的经典踩坑与排查记录5.1 Visual Studio 打开目录导致 CMake 缓存被重置我第一轮编译其实不是在命令行完成的而是直接用 VS 打开了build目录让 VS 加载 CMake 缓存。看起来没问题但当我改掉BUILD_SHARED_LIBS选项后VS 有时候会自作主张重新运行 CMake 配置把我在命令行里设置的其他变量冲掉一部分。最终的教训是命令行配置就全程命令行VS 生成器只负责编译。不要一会儿用 VS 改缓存、一会儿用命令行改缓存。如果确实需要修改 CMake 选项直接在命令行 re-run 一次cmake -S . -B build -DBUILD_SHARED_LIBSOFF这样最可控。5.2 LNK2019: unresolved external symbol 的排查这种报错最常见的原因是你的工程 include 了 pthread.h但链接时并没有把 pthread 库路径加进链接器。在 Visual Studio 工程里你需要配置“C/C → 常规 → 附加包含目录”指向install/include配置“链接器 → 常规 → 附加库目录”指向install/lib配置“链接器 → 输入 → 附加依赖项”添加pthreadVC3.lib如果你是纯 CMake 工程并且 install 目录里有 config.cmake最省心的是find_package(pthreads4w REQUIRED) target_link_libraries(your_target PRIVATE pthreads4w::pthreads4w)如果找不到 pthreads4w package用set(pthreads4w_DIR D:/libs/pthreads4w/install/lib/cmake/pthreads4w)指定一下再 find。还有另一种 LNK2019 的情况你把动态库的导入库.lib用来链接但运行时 DLL 不在系统 PATH 里。这通常不是链接错误而是运行时错误表现不同。5.3 Debug 和 Release 混用的连锁反应如果你主程序是 Debug 构建但 pthread 库是 Release 版本在纯 C 接口层面通常能链接过函数签名一样但一旦涉及到句柄结构体、锁内部状态的调试就可能因为编译器优化级别不同产生非常隐蔽的问题。特别是当你开了内存调试工具、启用了 CRT 的 debug heap 后跨越 debug/release 边界的内存操作很容易崩。所以我的原则很简单Debug 工程配 Debug 库Release 工程配 Release 库缺哪个就补编哪个。反正源码编译一次很便宜比起排查跨配置内存问题划算多了。5.4 运行时找不到 DLL这个属于最经典的问题了。你编译通过了、链接通过了但一运行 exe 就弹窗提示“代码执行无法继续因为找不到 pthreadVC3.dll”。处理方式有几种把 DLL 放到 exe 同目录最简单粗暴。把install/bin加入系统 PATH注意别污染全局环境。在开发阶段把install/bin设为 VS 调试环境的 PATH 变量。用 CMake 加 post-build 命令自动拷贝 DLL。我自己的做法是开发阶段用第三种发布时用第一种。不要把 DLL 路径硬编码到系统目录里一旦升级版本容易造成别的程序也跟着用新库出现莫名其妙的兼容问题。5.5 与 vcpkg 等其他线程库并存如果你机器上装了 vcpkg而且 vcpkg 也提供 pthreads4w那你得注意头文件冲突。假设你的主工程通过 vcpkg 引入了某个依赖那个依赖又把 vcpkg 版的 pthread 头目录加到全局 include而你的 pthread 库又是自己编在 install 目录下的那么就有两套pthread.h在抢预处理器搜索顺序。结果要么编译过不了要么链接时用到两个不同版本的库。排查方法是打开“C/C → 命令行 → 其他选项”里查看/I参数的实际顺序。靠前的 include 目录会先被搜索。确保你自己编的 install/include 排在最前面并且在项目里尽量避免同时依赖两个来源的 pthread。这个坑其实挺隐蔽的因为它不是每次都出错。只有当两个版本的 ABI 不一致时才出问题而很多 pthreads4w 版本的 ABI 碰巧是兼容的所以问题会被掩盖很久直到某天你更新了 vcpkg。6. 写一个最小验证程序确保你的库真能跑6.1 创建线程并拿到返回值编译成功只是第一步我每次都会写一个最小程序来验证库的文件、头文件、链接配置都正确。这段验证代码故意用到了pthread_create、pthread_join和一个锁#include stdio.h #include stdlib.h #include pthread.h typedef struct { int input; int output; } worker_arg_t; static void* worker(void* arg) { worker_arg_t* p (worker_arg_t*)arg; p-output p-input * 2; pthread_exit((void*)1); return NULL; } int main(void) { worker_arg_t arg {21, 0}; pthread_t tid; void* ret NULL; if (pthread_create(tid, NULL, worker, arg) ! 0) { fprintf(stderr, pthread_create failed\n); return EXIT_FAILURE; } if (pthread_join(tid, ret) ! 0) { fprintf(stderr, pthread_join failed\n); return EXIT_FAILURE; } printf(worker output %d\n, arg.output); printf(thread exit code %ld\n, (long)ret); return EXIT_SUCCESS; }如果库工作正常输出应该是worker output 42 thread exit code 1注意我故意让线程通过pthread_exit退出就是为了验证线程退出码能通过pthread_join正确取回。这个细节在很多 Windows 下的移植代码里很容易被忽略线程返回值和GetExitCodeThread的语义并不是完全一样的。6.2 配置头文件和库路径的两种方式在 Visual Studio 工程里配置的话无非就是前面提过的三步附加包含目录、附加库目录、附加依赖项。这里不再重复。我更推荐的方式是直接把这个验证程序做成一个 CMake 工程因为它同时能验证find_package能不能找到 pthreads4w 的 CMake 配置文件cmake_minimum_required(VERSION 3.16) project(pthread4w_demo C) find_package(pthreads4w REQUIRED) add_executable(demo main.c) target_link_libraries(demo PRIVATE pthreads4w::pthreads4w)如果你连这个 CMake 工程都能一次配过、一次编过说明 install 目录里的头文件、库文件、CMake 配置文件全部是完整的。以后你的主工程直接复用同样的find_package即可。6.3 从命令行 cl 编译的快速验证如果你不想建一堆工程文件也可以在 x64 Native Tools 命令行里直接用cl.exe验证。先确认路径cl /I D:/libs/pthreads4w/install/include main.c \ /link /LIBPATH:D:/libs/pthreads4w/install/lib pthreadVC3.lib然后运行main.exe。注意这时候 DLL 要在搜索路径中可以先把它拷到 exe 旁边copy D:\libs\pthreads4w\install\bin\pthreadVC3.dll . main.exe如果输出正常整个工具链就确认没问题了。最后再说一点体会pthreads4w v3.0.0 源码编译本身并不难难的是理解不同构建选项背后的语义以及提前预判自己项目会遇到哪些坑。如果你只是想快速跑通无脑照着一套默认选项编出来也能用但如果你想把它作为一个长期依赖集成进工程建议还是花 20 分钟把cmake -L打印出来的变量逐个看一遍尤其是异常模型、GC 回收、isolation 这几个跟行为强相关的选项。磨刀不误砍柴工后面省下来的排查时间远比这 20 分钟多。
阅读完成 · 觉得有帮助?