注意
浏览器 已实现并启用 SharedArrayBuffer,将其置于跨域打开程序策略 (COOP) 和跨域嵌入程序策略 (COEP) 标头之后。除非正确设置这些标头,否则 Pthreads 代码将无法在部署环境中工作。有关更多信息,请点击 此
Emscripten 支持使用浏览器中的 SharedArrayBuffer 进行多线程。该 API 允许在主线程和 Web 工作线程之间共享内存,以及用于同步的原子操作,这使 Emscripten 能够实现对 Pthreads(POSIX 线程)API 的支持。这种支持在 Emscripten 中被认为是稳定的。
默认情况下,不会启用 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_join
和 pthread_cond_wait
这样的东西通常旨在阻塞很长一段时间,如果发生在浏览器主线程上,并且同时其他线程期望它做出响应,那么它可能会导致一个令人惊讶的死锁。这可能是由于代理导致的,请参见上一节。如果主线程在工作线程尝试代理到它时阻塞,则可能会发生死锁。
底线是在 Web 上,浏览器主线程等待其他任何东西都是不好的。因此,默认情况下,Emscripten 会警告如果 pthread_join
和 pthread_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 行为,会破坏常见的代码,这些代码创建一个线程并立即加入它,或者以其他同步方式等待观察效果,例如内存写入。对此有几个解决方案
返回到主事件循环(例如,使用 emscripten_set_main_loop
或 Asyncify)。
使用链接器标志 -sPTHREAD_POOL_SIZE=<expression>
。使用池会在调用 main 之前创建 Web Workers,因此它们可以在调用 pthread_create
时直接使用。
使用链接器标志 -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 错误跟踪器中报告错误。