Pthreads 支持

注意

浏览器 已实现并启用 SharedArrayBuffer,将其置于跨域打开程序策略 (COOP) 和跨域嵌入程序策略 (COEP) 标头之后。除非正确设置这些标头,否则 Pthreads 代码将无法在部署环境中工作。有关更多信息,请点击

Emscripten 支持使用浏览器中的 SharedArrayBuffer 进行多线程。该 API 允许在主线程和 Web 工作线程之间共享内存,以及用于同步的原子操作,这使 Emscripten 能够实现对 Pthreads(POSIX 线程)API 的支持。这种支持在 Emscripten 中被认为是稳定的。

使用 pthreads 启用编译

默认情况下,不会启用 pthreads 支持。要启用为 pthreads 生成代码,可以使用以下命令行标志

  • 在编译任何 .c/.cpp 文件时,以及在链接以生成最终输出 .js 文件时,传递编译器标志 -pthread

  • 可选地,传递链接器标志 -sPTHREAD_POOL_SIZE=<expression> 以指定在页面 preRun 时间在应用程序 main() 被调用之前填充的预定义 Web 工作线程池。这很重要,因为如果工作线程尚不存在,那么我们可能需要等待下一次浏览器事件迭代才能执行某些操作,请参见下文。 <expression> 可以是任何有效的 JavaScript 表达式,包括整数,如 8 用于固定数量的线程,或者,例如,navigator.hardwareConcurrency 用于创建与 CPU 内核数量一样多的线程。

应该不需要进行任何其他更改。在 C/C++ 代码中,预处理器检查 #ifdef __EMSCRIPTEN_PTHREADS__ 可用于检测 Emscripten 当前是否针对 pthreads。

注意

无法构建一个二进制文件,该二进制文件能够在可用时利用多线程,并在不可用时回退到单线程。最好的方法是进行两个独立的构建,一个包含线程,一个不包含线程,并在运行时在它们之间进行选择。

其他标志

  • -sPROXY_TO_PTHREAD:在此模式下,您原始的 main() 将被一个新的 main() 替换,该新的 main() 创建一个 pthread 并在其上运行原始的 main()。因此,应用程序的 main() 在浏览器主(UI)线程之外运行,这对响应性很有好处。浏览器主线程在将操作代理到它时仍然会运行代码,例如处理事件、渲染等。主线程还会执行一些操作,例如为您创建 pthreads,因此您可以同步地依赖它们。

请注意,Emscripten 有 --proxy-to-worker 链接器标志,听起来很相似,但实际上无关。该标志不使用 pthreads 或 SharedArrayBuffer,而是使用普通的 Web 工作线程来运行您的主程序(并将消息发回并 forth)。

代理

Web 允许某些操作仅从浏览器主线程执行,例如与 DOM 交互。因此,如果在后台线程上调用了各种操作,它们会被代理到浏览器主线程。参见 错误 3495,了解有关此问题的更多信息以及如何在目前解决此问题的方法。要检查哪些操作被代理,您可以查看 JS 库 (src/library_*) 中函数的实现,并查看它是否用 __proxy: 'sync'__proxy: 'async' 进行注释;但是,请注意,浏览器本身会代理某些操作(例如一些 GL 操作),因此这里没有通用安全方法(除了不阻塞在浏览器主线程上)。

此外,Emscripten 目前只在主应用程序线程上有一个简单的文件 I/O 模型(因为我们支持 JS 插件文件系统,它不能共享内存);这是另一组被代理的操作。

代理在某些情况下会导致问题,请参见下面关于阻塞的部分。

在浏览器主线程上阻塞

请注意,在大多数情况下,“浏览器主线程”与“主应用程序线程”相同。浏览器主线程是网页开始运行 JavaScript 的地方,也是 JavaScript 可以访问 DOM 的地方(页面还可以创建一个 Web 工作线程,该工作线程将不再位于主线程上)。主应用程序线程是您开始运行应用程序的线程(通过加载 Emscripten 发出的主 JS 文件)。如果您在浏览器主线程上启动它 - 因为它是一个普通的 HTML 页面 - 那么两者是相同的。但是,您也可以在工作线程中启动多线程应用程序;在这种情况下,主应用程序线程是该工作线程,并且无法访问浏览器主线程。

用于原子的 Web API 不允许在主线程上阻塞(具体来说,Atomics.wait 无法在那里工作)。此类阻塞在 API 中是必要的,例如 pthread_join 以及任何在幕后使用 futex 等待的东西,例如 usleep()emscripten_futex_wait()pthread_mutex_lock()。为了使它们正常工作,我们在浏览器主线程上使用繁忙等待,这会导致浏览器标签页无响应,还会浪费电量。(在 pthread 上,这不是问题,因为它在 Web 工作线程中运行,在 Web 工作线程中我们不需要繁忙等待。)

在浏览器主线程上进行繁忙等待通常会正常工作,尽管有前面提到的缺点,适用于像等待轻度竞争的互斥体这样的事情。但是,像 pthread_joinpthread_cond_wait 这样的东西通常旨在阻塞很长一段时间,如果发生在浏览器主线程上,并且同时其他线程期望它做出响应,那么它可能会导致一个令人惊讶的死锁。这可能是由于代理导致的,请参见上一节。如果主线程在工作线程尝试代理到它时阻塞,则可能会发生死锁。

底线是在 Web 上,浏览器主线程等待其他任何东西都是不好的。因此,默认情况下,Emscripten 会警告如果 pthread_joinpthread_cond_wait 发生在浏览器主线程上,如果 ALLOW_BLOCKING_ON_MAIN_THREAD 为零(其消息将指向此处),则会抛出错误。

为了避免这些问题,您可以使用 PROXY_TO_PTHREAD,如前所述,它会将您的 main() 函数移动到一个 pthread,这使得浏览器主线程可以专注于仅接收代理事件。通常建议这样做,但这可能需要一些移植工作,如果应用程序假设 main() 位于浏览器主线程上。

另一个选择是使用非阻塞调用替换阻塞调用。例如,您可以使用 pthread_tryjoin_np 替换 pthread_join。这可能需要您对应用程序进行重构,以便使用异步事件,也许通过 emscripten_set_main_loop()ASYNCIFY

特殊注意事项

Emscripten 对 pthreads API 的实现应该紧密遵循 POSIX 标准,但确实存在一些行为差异

  • 当调用 pthread_create() 时,如果我们需要创建一个新的 Web 工作线程,那么这需要返回主事件循环。也就是说,您不能调用 pthread_create 然后继续运行同步地期望工作线程开始运行的代码 - 它只有在您返回到事件循环后才会运行。这违反了 POSIX 行为,会破坏常见的代码,这些代码创建一个线程并立即加入它,或者以其他同步方式等待观察效果,例如内存写入。对此有几个解决方案

    1. 返回到主事件循环(例如,使用 emscripten_set_main_loop 或 Asyncify)。

    2. 使用链接器标志 -sPTHREAD_POOL_SIZE=<expression>。使用池会在调用 main 之前创建 Web Workers,因此它们可以在调用 pthread_create 时直接使用。

    3. 使用链接器标志 -sPROXY_TO_PTHREAD,这将为你运行 main() 在一个 worker 上。这样做时,pthread_create 被代理到主浏览器线程,在那里它可以根据需要返回到主事件循环。

  • Emscripten 实现不支持 POSIX 信号,这些信号有时与 pthreads 结合使用。这是因为无法向 Web Workers 发送信号并抢占它们的执行。对此的唯一例外是 pthread_kill(),它可以正常使用来强制终止正在运行的线程。

  • Emscripten 实现也不支持通过 fork()join() 进行多进程处理。

  • 出于网络安全目的,在 Firefox Nightly 中运行时,存在一个固定的线程数量限制(默认情况下为 20)。#1052398。要调整限制,请导航到 about:config 并更改首选项“dom.workers.maxPerDomain”的值。

  • pthreads 规范中的一些功能不受支持,因为 Emscripten 使用的上游 musl 库不支持它们,或者它们被标记为可选,并且符合规范的实现不需要支持它们。Emscripten 中的此类不受支持的功能包括线程优先级,pthread_rwlock_unlock() 不是按线程优先级顺序执行的。函数 pthread_mutexattr_set/getprotocol()、pthread_mutexattr_set/getprioceiling() 和 pthread_attr_set/getscope() 是无操作的。

  • 移植时需要注意的一点是,有时在现有代码库中,pthread_create() 和 pthread_cleanup_push() 的回调函数指针会省略 void* 参数,严格来说,这在 C/C++ 中是未定义的行为,但在几种 x86 调用约定中有效。在 Emscripten 中这样做会发出编译器警告,并且在尝试使用错误签名调用函数指针时可能会在运行时中止,因此在出现此类错误时,最好检查线程回调函数的签名。

  • 请注意,函数 emscripten_num_logical_cores() 将始终返回 navigator.hardwareConcurrency 的值,即系统上的逻辑核心数量,即使不支持共享内存也是如此。这意味着 emscripten_num_logical_cores() 可能返回大于 1 的值,而同时 emscripten_has_threading_support() 可能会返回 false。emscripten_has_threading_support() 的返回值表示浏览器是否支持共享内存。

  • Pthreads + 内存增长 (ALLOW_MEMORY_GROWTH) 尤其棘手,请参见 Wasm 设计问题 #1271。这目前会导致 JS 访问 Wasm 内存速度变慢 - 但这可能只会当 JS 进行大量内存读写操作时才会显着(Wasm 以全速运行,因此将工作转移过来可以解决此问题)。这也要求你的 JS 意识到 HEAP* 视图可能需要更新 - 使用 --js-library 等嵌入的 JS 代码将自动转换为使用 GROWABLE_HEAP_* 帮助函数,其中使用 HEAP*,但直接使用 Module.HEAP* 的外部代码可能会遇到视图比内存小的问题。

分配器性能

Emscripten 中的默认系统分配器 dlmalloc 在单线程程序中非常高效,但它有一个全局锁,这意味着如果在 malloc 上存在争用,则你可以看到开销。你可以使用 mimalloc 来代替,方法是使用 -sMALLOC=mimalloc,它是一个针对多线程性能进行了优化的更复杂的分配器。 mimalloc 在每个线程上都有独立的分配上下文,允许性能在 malloc/free 争用下更好地扩展。

请注意,mimalloc 的代码大小大于 dlmalloc,并且在运行时也使用更多内存(因此你可能需要将 INITIAL_MEMORY 调整为更高的值),因此这里存在权衡。

运行代码和测试

目前,任何使用 pthreads 支持编译的代码都只能在 Firefox Nightly 通道中运行,因为 SharedArrayBuffer 规范仍处于实验研究阶段,尚未标准化。存在两个测试套件,可用于验证 Emscripten 中 pthreads API 实现的行为

  • Emscripten 单元测试套件在“browser。”中包含几个特定于 pthreads 的测试。套件。运行任何名为 browser.test_pthread_* 的测试。

  • Emscripten 专用版本的 Open POSIX Test Suite 可在 juj/posixtestsuite GitHub 存储库中找到。该套件包含大约 300 个 pthreads 符合性测试。要运行此套件,首选项 dom.workers.maxPerDomain 应首先增加到至少 50。

如果遇到任何问题,请先检查这些问题。与往常一样,可以在 Emscripten 错误跟踪器中报告错误。