早在 VS2022 17.5 版本,Microsoft Visual C 库已经初步支持了 C11 atomics。今天,我们很高兴地宣布,在最新版本 VS2022 17.8 预览版 2 中已正式支持 C11 线程。开发者可以更轻松地将跨平台 C 应用程序移植到 Windows,而无需开发线程在不同平台上的兼容性层。
和 C11 atomics 不同的是,C11 线程并不和 C++ 的
共享同一个 ABI,但是 C++ 程序可以包含 C11 线程的头文件并像调用其他 C 程序那样调用该头文件中的函数。两者都是根据 Windows 提供的原语实现的,因此它们的用法可以在同一程序和同一线程中混合使用。但是,他们的底层实现是不同的,例如,你不能将 C11 互斥体与 C++ 条件变量一起使用。
C11 包含对线程和各种相关并发原语的支持,包括互斥锁、条件变量和特定于线程的存储。所有这些都在VS2022 17.8 预览版 2 中实现。
线程
线程是使用 thrd_create 创建,只需要将线程的函数指针和用户数据指针(可能为 null)以及指向要填充的thrd_t结构的指针传递到该指针。使用 thrd_create 创建 thrd_t 后,你可以调用函数将其与其他 thrd_t 进行比较、联接或分离,还提供了用于休眠或生成当前线程的函数。
>> 请移步至 topomel.com 以查看图片 <<
我们的实现与基于 pthreads 的 C11 线程实现之间的主要区别在于,线程不能使用 thrd_current 和 thrd_detach 自行分离。这是因为线程在 Windows 和类 Unix 上的工作方式存在根本差异,我们需要一个跟踪线程句柄的共享数据结构来实现一些常用的操作。
在类 Unix 操作系统上,整数线程 ID 是线程的句柄,分离只是设置一个标志,导致线程在完成后立即被清理。这使得分离的线程在类 Unix 上使用起来有些危险,因为在分离的线程退出后,对该线程 ID 的任何其他引用都将悬而未决,并且以后可能会完全引用不同的线程。在 Windows 上,线程的句柄是 win32 句柄,并被计算引用。当最后一个句柄关闭时,将清理线程。除了跟踪它们并关闭每个句柄之外,无法关闭线程的所有句柄。
我们可以通过保留线程 ID 的共享映射来实现 Unix/pthreads 行为,并通过 thrd_create 填充。如果你需要此功能,则可以自己实现类似功能,但我们默认不提供此功能,因为即使不使用也会产生成本。还可以使用更好的解决方法,例如通过用户数据指针将指针传递给创建的线程,将指针传递到 thrd_create 填充的 thrd_t 结构体中。
互斥体
互斥体通过 mtx_t 结构和相关功能提供。互斥体可以是普通的、递归的、定时的或这些属性的组合。所有类型的互斥体都使用相同的函数进行操作(类型是动态的)。
>> 请移步至 topomel.com 以查看图片 <<
我们的互斥体始终在 Slim Reader Writer Locks 之上实现,在 x64 上每个为 32 字节(我们的 C++ std::mutex 为 80 字节)。它们由一个 8 字节标记(这比需要的要多得多,但为将来的扩展提供了一些空间)、一个 SRWLock、一个 win32 CONDITION_VARIABLE和一个 32 位所有者和锁计数组成。始终保持所有者和锁计数,即使互斥体不是递归的。
如果你尝试递归锁定非递归互斥体,或解锁不属于你的互斥体,则调用 abort 。结构上有效的 mtx_unlock 调用始终成功,在我们的实现中忽略 mtx_unlock 的返回值是安全的。
在我们的实现中,你不需要调用 mtx_init, 一个置零的 mtx_t 是有效的普通互斥体。互斥体也不需要任何清理,对mtx_destroy 的调用是可选的。这意味着你可以安全地将互斥体用作静态变量和类似变量。
条件变量
条件变量通过 cnd_t 结构和相关函数提供。此结构为 8 个字节,仅存储 win32 CONDITION_VARIABLE。
你可以使用 cnd_wait 或 cnd_timedwait 等待条件变量,也可以使用 cnd_signal 唤醒一个等待线程,也可以使用 cnd_broadcast 唤醒所有等待线程,另外,还支持虚假唤醒。
>> 请移步至 topomel.com 以查看图片 <<
与互斥体类似,清零条件变量是有效的,你可以省略对 cnd_init 和 cnd_destroy 的调用。
线程特定存储
特定于线程的存储通过 _Thread_local(C23 中的thread_local)关键字或通过 tss_ 系列函数提供。_Thread_local的工作方式与__declspec(线程)(请参阅文档)类似,tss_函数的工作方式与 Fls* 或 Tls* 系列函数类似,但不完全相同。
>> 请移步至 topomel.com 以查看图片 <<
C11 TSS 工具支持析构函数,这些析构函数在线程退出时运行,并传递关联的 TSS 键的值(如果该值为非空)。宏TSS_DTOR_ITERATIONS指定在析构函数调用tss_set的情况下,我们将检查要运行的更多析构函数的次数。目前它设置为 1,但是,如果这对你来说是个问题,请告诉我们。析构函数从 DllMain 或 TLS 回调(如果使用静态运行时)运行,并且不会在进程拆卸时运行。这是与 FLS 析构函数的重要区别,后者在进程拆卸时运行,并在任何 DllMain 例程或 TLS 回调之前运行。
TSS 限制和性能特征
使用显式 tss_ 函数时,每个进程限制为 1024 个 TSS 索引,这些索引与用于 Fls* 函数、Tls* 函数或_Thread_local“隐式”TLS 变量的索引不同。如果使用任何
函数(不仅仅是 TSS 函数)并使用静态运行时,则至少使用一个隐式 TLS 索引(用于_Thread_local的索引),即使你不使用隐式 TLS。这是因为我们需要启用 TLS 回调,这会导致加载器分配这样的索引。
如果这是一个问题(例如,由于动态加载此类模块所需的加载器体操),请告诉我们,或者只使用动态运行时。如果你使用 tss_ 函数,那么另外你将使用一个动态 TLS 索引(与 TlsAlloc 使用的索引相同),无论你创建多少个tss_ts,你都将只使用一个。如果曾经在该线程上设置了具有关联析构函数的 TSS 索引,则线程只会在线程退出时花时间处理 TSS 析构函数。创建第一个tss_t时,将分配析构函数表,首次在特定线程上使用tss_set时,将分配每个线程表。内存使用情况与使用 C11 TSS 功能的线程数(而不是进程中的线程总数)成比例。析构函数表为 8KiB(在 32 位平台上为 4KiB),每个线程表为 8209 字节(在 32 位平台上为 4105 字节)。这些性能和内存特性将来可能会发生变化。
新的运行时组件
由于 threads.h 是一项新功能,我们希望实现能够随着时间的推移而更改和改进,因此它作为 vcruntime: vcruntime140_threads.dll 和 vcruntime140_threadsd.dll 的新附属 DLL 发布。如果使用动态版本的 Visual C++ 运行时(/MD 或 /MDd),并且使用新的线程工具,则需要随应用重新分发此文件,或者重新分发足够新的 Visual C++ 运行时修订器,以包含这些文件。如果不接触 C11 线程功能,则应用将不依赖于此 DLL 中的任何内容,并且根本不会加载。
总结
如果你恰好是一个程序员,如果你又恰好是一名 C++ 程序员,那么,今天这篇文章可以作为课后扩展资料,了解下就可以了。
最后
Microsoft Visual C++团队的博客是我非常喜欢的博客之一,里面有很多关于Visual C++的知识和最新开发进展。大浪淘沙,如果你对Visual C++这门古老的技术还是那么感兴趣,则可以经常去他们那(或者我这)逛逛。
本文来自:《C11 Threads in Visual Studio 2022 version 17.8 Preview 2》