对于有高性能要求的可并发任务来说,在程序一开始设计的时候就要把多线程协作考虑进去,因此介绍完了一些emscripten的基本操作之后,我立刻就来到了多线程的章节。
Emscripten提供了两种实现多线程的方式:
- POSIX Threads (Pthreads) API (以下简称pthreads)(此工具中标准库的
std::thread
也基于pthreads实现) - Wasm Workers API (以下简称wasmworkers)(在这里继续再批判一次官方这破API文档竟然不写每个函数的用法,其它东西扯了一堆)
我参阅了一下wasmworkers的文档,以上两种API基本能实现相同的功能,区别在于它的wasmworkers是直接根据js原生worker原本的API实现的,因此输出的编译结果会比较小,而pthreads实现了完整的pthreads api,所以输出的编译结果会比较大。由于考虑程序的可移植性,所以这里我只学习使用pthreads实现多线程,如果对pthreads不熟悉的话可以看一看这个Rookie-Note上的学习文档简单了解一下,还是挺简单的,我也刚学会才来写了这篇笔记。
本篇笔记不会对pthreads api如何使用进行全面的说明,因为内容比较多且不是专属于wasm的内容,在代码编写上也和普通的 C pthread程序没有什么区别,可以看我上面贴的菜鸟笔记链接去学习,这里主要就说明一下emscripten中pthread的实现方式和举个例子,顺便贴上emscripten的pthread指导链接。
首先要注意:
- 使用以上两种多线程API的时候都不可以和 `-sSINGLE_FILE` 编译参数搭配使用,也就是使用多线程就无法把输出文件全部打包到一个js文件里了。
- 要使用pthreads需要浏览器开启SharedArrayBuffer支持(在目前的浏览器中由于存在安全原因默认是禁用的),在nodejs中可以直接使用。wasmworkers我没有测试,但应该也是一样的。
- 编译参数:要使用pthreads库,需要在编译时添加 `-pthread` 参数。
- 由于标准库中的
std::thread
也是基于pthread实现的,因此如果你想要使用`std::thread`,也需要添加 `-pthread` 编译参数。
emscripten中的pthreads其实也是基于js原生的worker实现的,在官方的这个文档中提到在wasm中创建新的线程需要js线程先去创建worker,但对于pthreads api来说创建线程是一个阻塞操作,也就是说这个过程中wasm不会返回到js线程,就没法去调用api创建。然而我测试下来即使没有添加任何额外的参数,依然可以正常创建线程,不知是现在的版本中内部做了一些优化但文档没改,还是nodejs的实现不一样。
如果在创建线程时出现问题可以参考文档中的三个方法,推荐比较不影响源码的后两个方法,即使用`-sPTHREAD_POOL_SIZE=<表达式>`可以在进入wasm主入口之前先预创建指定数量的线程,或者使用`-sPROXY_TO_PTHREAD`参数让wasm主线程不跑在js主线程里,就不会卡着js的执行了。
以上 “<表达式>” 在文档中特别提醒,可以设置为 navigator.hardwareConcurrency ,即和处理器逻辑线程相同的数量。
示例:
这里我写了一个多线程示例,且故意不对计算资源竞争进行加锁,以此证明这些线程确实是并行执行的、线程间共享一部分内存、互斥锁可以正常工作。
将test.cpp中的内容修改为下,其做的事情是开启4个线程来同时对一个sum变量做非原子性的自增一亿次操作,理论上最终sum的结果应该是一个不等于四亿的随机数,另外我定义了一个log函数,其中对输出操作进行加锁,防止不同的线程同时输出内容串在一起:
// emcc -sMODULARIZE=1 -sASSERTIONS -pthread -o dist/test.mjs test.cpp #include <iostream> #include <emscripten.h> #include <pthread.h> // 引入线程库 using std::cout, std::endl; // 将创建的线程数 constexpr size_t NUM_THREADS = 4; // 创建一个变量用来实验多线程的非原子性操作 uint64_t sum = 0; // 创建一个互斥锁来保证每个线程的cout语句不会被其它线程的cout打断 // 尝试注释掉此锁以及下面的加/解锁,你将看到乱七八糟的输出 pthread_mutex_t cout_mutex = PTHREAD_MUTEX_INITIALIZER; // 临时定义一个log函数,在实验加锁排他输出的同时,避免参数过多的cout调用看起来太混乱 template <typename... Args> void log(Args... args) { pthread_mutex_lock(&cout_mutex); // 加锁 ((cout << args), ...) << endl; pthread_mutex_unlock(&cout_mutex); // 解锁 } // 线程的入口函数,接受一个代表线程编号的参数 void *ThreadEntry(void *_idx) { const long idx = (long)_idx; const long pid = pthread_self(); // 可以获取到线程真实的pid log("Thread ", idx, "(pid:", pid, ") : starting..."); const double startTime = emscripten_get_now();//获取当前的时间,用于之后计算线程执行时间 // 这里执行一个简单的循环自增操作,把所有线程共享的sum变量自增一亿次 for (size_t i = 0; i < 100000000; i++) { sum++; } log("Thread ", idx, " : done in ",emscripten_get_now()-startTime,"ms."); pthread_exit((void *)sum); } int main() { log("Hello, World!"); pthread_t thread[NUM_THREADS]; // 创建线程数组,后面创建的线程将保存在这里 for (size_t idx = 0; idx < NUM_THREADS; idx++) { log("Main: creating thread ", idx); // 创建线程,传入线程入口函数以及线程参数(线程编号) auto code = pthread_create(&thread[idx], NULL, ThreadEntry, (void *)idx); if (code) { // 创建成功时返回值应为0 log("Main error: return code from pthread_create() is ", code); return -1; } } void *returnValue; for (size_t idx = 0; idx < NUM_THREADS; idx++) { // 依次等待线程结束,并获取线程的返回值 auto code = pthread_join(thread[idx], &returnValue); if (code) { log("Main error: return code from pthread_join() is ", code); return -1; } log("Main: completed join with thread ", idx, " having a returnValue of ", (unsigned long)returnValue); } log("Main: sum = ", sum); log("Main: program completed. Exiting."); return 0; }
然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -pthread -o dist/test.mjs test.cpp
,注意在这个示例中不要添加优化参数`-O`,因为它可能会在编译阶段把循环内的结果计算完,导致最终输出结果看起来不像并行执行的。
接着在start.js中直接加载运行即可,下面我把输出内容也放在里面:
(async () => { const { default: wasm } = await import('./dist/test.mjs'); const instance = await wasm(); /* 控制台中将输出以下内容: Hello, World! Main: creating thread 0 Main: creating thread 1 Main: creating thread 2 Main: creating thread 3 Thread 2(pid:229176) : starting... Thread 1(pid:161352) : starting... Thread 0(pid:93528) : starting... Thread 3(pid:297000) : starting... Thread 3 : done. Thread 2 : done. Thread 0 : done. Main: completed join with thread 0 having a returnValue of 108925498 Thread 1 : done. Main: completed join with thread 1 having a returnValue of 148142535 Main: completed join with thread 2 having a returnValue of 102042998 Main: completed join with thread 3 having a returnValue of 101243148 Main: sum = 148142535 Main: program completed. Exiting. */ })();
从上面的输出结果可以看出如我们所料,在多线程不对资源加锁的情况下进行竞争,每个线程会对sum变量各自进行读取和覆盖,所以最后得到的总和一定不是四亿,而是一个接近一亿的随机数字,每次执行都会不一样。
其它小提示:
- 阻塞:由于当wasm运行在js线程中时,wasm 的一些操作可能会阻塞js线程,尤其是js的主线程被阻塞的时候可能导致网页无响应或者nodejs程序卡住,所以emscripten提供了一个方便的功能,可以帮我们把我们的wasm主入口单独分到一个线程,只要添加前文已经提到过的编译参数`-sPROXY_TO_PTHREAD`即可。文档里没有写这么做是否会有什么额外的开销,暂且认为没有。
- 使用 #ifdef __EMSCRIPTEN_PTHREADS__ 宏可以在代码中判断编译阶段是否启用了pthreads,由于运行阶段在同一个wasm程序中不存在一种方式切换单线程或多线程逻辑,因此如果有从不支持多线程环境回退到单线程的需求的话,需要编译两个版本出来。
本文发布于 https://luojia.me
本站文章未经文下加注授权不得拷贝发布。