بیاموزید که چگونه برنامه های چند رشته ای نوشته شده به زبان های دیگر را به WebAssembly بیاورید.
پشتیبانی از رشته های WebAssembly یکی از مهم ترین موارد اضافه شده در عملکرد WebAssembly است. این به شما امکان می دهد بخش هایی از کد خود را به صورت موازی بر روی هسته های جداگانه اجرا کنید، یا همان کد را بر روی بخش های مستقل داده های ورودی، مقیاس آن را به تعداد هسته های کاربر تغییر دهید و زمان اجرای کلی را به میزان قابل توجهی کاهش دهید.
در این مقاله یاد خواهید گرفت که چگونه از رشته های WebAssembly برای آوردن برنامه های چند رشته ای نوشته شده به زبان هایی مانند C، C++ و Rust به وب استفاده کنید.
نحوه کار موضوعات WebAssembly
رشتههای WebAssembly یک ویژگی جداگانه نیست، بلکه ترکیبی از چندین مؤلفه است که به برنامههای WebAssembly اجازه میدهد از پارادایمهای چند رشتهای سنتی در وب استفاده کنند.
کارگران وب
اولین مؤلفه، Workers معمولی است که از جاوا اسکریپت می شناسید و دوست دارید. رشتههای WebAssembly از سازنده new Worker
برای ایجاد رشتههای زیربنایی جدید استفاده میکنند. هر رشته یک چسب جاوا اسکریپت را بارگذاری می کند و سپس رشته اصلی از متد Worker#postMessage
برای به اشتراک گذاشتن WebAssembly.Module
کامپایل شده و همچنین WebAssembly.Memory
مشترک (در زیر) با آن رشته های دیگر استفاده می کند. این ارتباط برقرار میکند و به همه آن رشتهها اجازه میدهد تا کد WebAssembly یکسان را در همان حافظه مشترک بدون مرور مجدد جاوا اسکریپت اجرا کنند.
Web Workers بیش از یک دهه است که وجود دارند، به طور گسترده پشتیبانی می شوند و به هیچ پرچم خاصی نیاز ندارند.
SharedArrayBuffer
حافظه WebAssembly توسط یک شی WebAssembly.Memory
در JavaScript API نشان داده می شود. بهطور پیشفرض WebAssembly.Memory
یک بستهبندی در اطراف یک ArrayBuffer
است - یک بافر خام بایتی که فقط با یک رشته قابل دسترسی است.
> new WebAssembly.Memory({ initial:1, maximum:10 }).buffer
ArrayBuffer { … }
برای پشتیبانی از Multithreading، WebAssembly.Memory
یک نوع مشترک نیز به دست آورد. هنگامی که با یک پرچم shared
از طریق API جاوا اسکریپت یا توسط خود WebAssembly باینری ایجاد می شود، به جای آن تبدیل به یک پوشش در اطراف SharedArrayBuffer
می شود. این یک گونه از ArrayBuffer
است که می تواند با رشته های دیگر به اشتراک گذاشته شود و به طور همزمان از هر طرف خوانده یا اصلاح شود.
> new WebAssembly.Memory({ initial:1, maximum:10, shared:true }).buffer
SharedArrayBuffer { … }
برخلاف postMessage
که معمولاً برای ارتباط بین رشته اصلی و Web Workers استفاده میشود، SharedArrayBuffer
نیازی به کپی کردن دادهها یا حتی منتظر ماندن حلقه رویداد برای ارسال و دریافت پیام ندارد. در عوض، هر تغییری تقریباً بلافاصله توسط همه رشتهها مشاهده میشود، که آن را به یک هدف تلفیقی بسیار بهتر برای همگامسازیهای ابتدایی سنتی تبدیل میکند.
SharedArrayBuffer
تاریخچه پیچیده ای دارد. در ابتدا در اواسط سال 2017 در چندین مرورگر ارسال شد، اما به دلیل کشف آسیبپذیریهای Spectre مجبور شد در اوایل سال 2018 غیرفعال شود. دلیل خاص این بود که استخراج داده در Spectre بر حملات زمان بندی متکی است - اندازه گیری زمان اجرای یک قطعه کد خاص. برای سختتر کردن این نوع حمله، مرورگرها دقت APIهای زمانبندی استاندارد مانند Date.now
و performance.now
را کاهش دادند. با این حال، حافظه مشترک، همراه با یک حلقه شمارنده ساده که در یک رشته مجزا اجرا میشود ، نیز راه بسیار مطمئنی برای به دست آوردن زمانبندی با دقت بالا است ، و کاهش آن بدون کاهش قابل توجه عملکرد زمان اجرا بسیار سختتر است.
در عوض، Chrome 68 (اواسط سال 2018) با استفاده از Site Isolation - قابلیتی که وب سایت های مختلف را در فرآیندهای مختلف قرار می دهد و استفاده از حملات کانال جانبی مانند Spectre را بسیار دشوارتر می کند، SharedArrayBuffer
دوباره فعال کرد. با این حال، این کاهش هنوز فقط به دسکتاپ کروم محدود میشود، زیرا Site Isolation یک ویژگی نسبتاً گران است و نمیتوان آن را بهطور پیشفرض برای همه سایتهای دستگاههای تلفن همراه با حافظه کم فعال کرد و هنوز توسط سایر فروشندگان اجرا نشده است.
کروم و فایرفاکس هر دو دارای پیادهسازی Site Isolation و روشی استاندارد برای شرکت در این ویژگی با سرصفحههای COOP و COEP هستند . مکانیزم انتخاب کردن امکان استفاده از Site Isolation را حتی در دستگاههای کم مصرف میدهد، جایی که فعال کردن آن برای همه وبسایتها بسیار گران است. برای انتخاب، هدرهای زیر را به سند اصلی در پیکربندی سرور خود اضافه کنید:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
هنگامی که شرکت کردید، به SharedArrayBuffer
(از جمله WebAssembly.Memory
با پشتیبانی SharedArrayBuffer
)، تایمرهای دقیق، اندازهگیری حافظه و سایر APIهایی که به دلایل امنیتی نیاز به یک منبع مجزا دارند، دسترسی خواهید داشت. برای جزئیات بیشتر ، با استفاده از COOP و COEP، وبسایت خود را «منشأ متقاطع جدا شده» کنید.
اتمی WebAssembly
در حالی که SharedArrayBuffer
به هر رشته اجازه میدهد تا در حافظه یکسانی بخواند و بنویسد، برای برقراری ارتباط صحیح میخواهید مطمئن شوید که آنها عملیات متضاد را همزمان انجام نمیدهند. به عنوان مثال، ممکن است یک رشته شروع به خواندن داده ها از یک آدرس مشترک کند، در حالی که رشته دیگر در حال نوشتن در آن است، بنابراین اولین رشته اکنون یک نتیجه خراب خواهد داشت. این دسته از اشکالات به عنوان شرایط مسابقه شناخته می شوند. برای جلوگیری از شرایط مسابقه، باید به نحوی آن دسترسی ها را همگام سازی کنید. اینجاست که عملیات اتمی وارد می شود.
WebAssembly atomics یک برنامه افزودنی برای مجموعه دستورات WebAssembly است که اجازه خواندن و نوشتن سلول های کوچک داده (معمولا اعداد صحیح 32 و 64 بیتی) را به صورت "اتمی" می دهد. یعنی به نحوی که تضمین میکند که هیچ دو رشته همزمان در یک سلول نمیخوانند یا نمینویسند و از چنین درگیریهایی در سطح پایین جلوگیری میکند. علاوه بر این، WebAssembly atomics حاوی دو نوع دستورالعمل دیگر است - "wait" و "notify" - که به یک رشته اجازه می دهد تا در یک آدرس معین در یک حافظه مشترک بخوابد ("انتظار") تا زمانی که رشته دیگری آن را از طریق "اعلان" بیدار کند.
همه مقدمات همگام سازی سطح بالاتر، از جمله کانال ها، mutexes، و قفل های خواندن و نوشتن بر اساس این دستورالعمل ها ساخته شده اند.
نحوه استفاده از موضوعات WebAssembly
تشخیص ویژگی
WebAssembly atomics و SharedArrayBuffer
ویژگی های نسبتا جدیدی هستند و هنوز در همه مرورگرهای دارای پشتیبانی WebAssembly در دسترس نیستند. در نقشه راه webassembly.org می توانید ببینید کدام مرورگرها از ویژگی های جدید WebAssembly پشتیبانی می کنند.
برای اطمینان از اینکه همه کاربران میتوانند برنامه شما را بارگذاری کنند، باید با ساخت دو نسخه مختلف Wasm، بهبود تدریجی را پیادهسازی کنید - یکی با پشتیبانی چند رشتهای و دیگری بدون آن. سپس بسته به نتایج تشخیص ویژگی، نسخه پشتیبانی شده را بارگیری کنید. برای شناسایی پشتیبانی از رشته های WebAssembly در زمان اجرا، از کتابخانه wasm-feature-detect استفاده کنید و ماژول را به صورت زیر بارگذاری کنید:
import { threads } from 'wasm-feature-detect';
const hasThreads = await threads();
const module = await (
hasThreads
? import('./module-with-threads.js')
: import('./module-without-threads.js')
);
// …now use `module` as you normally would
حال بیایید نگاهی به نحوه ساخت یک نسخه چند رشته ای از ماژول WebAssembly بیاندازیم.
سی
در C، بهویژه در سیستمهای شبه یونیکس، روش رایج استفاده از نخها از طریق رشتههای POSIX ارائه شده توسط کتابخانه pthread
است. Emscripten یک پیادهسازی سازگار با API از کتابخانه pthread
ساخته شده بر روی Web Workers، حافظه مشترک و اتمیها ارائه میکند، به طوری که همان کد میتواند بدون تغییر در وب کار کند.
بیایید به یک مثال نگاه کنیم:
example.c:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *thread_callback(void *arg)
{
sleep(1);
printf("Inside the thread: %d\n", *(int *)arg);
return NULL;
}
int main()
{
puts("Before the thread");
pthread_t thread_id;
int arg = 42;
pthread_create(&thread_id, NULL, thread_callback, &arg);
pthread_join(thread_id, NULL);
puts("After the thread");
return 0;
}
در اینجا سرصفحه های کتابخانه pthread
از طریق pthread.h
گنجانده شده است. همچنین میتوانید چند کارکرد حیاتی برای برخورد با نخها ببینید.
pthread_create
یک رشته پس زمینه ایجاد می کند. یک مقصد برای ذخیره یک دسته رشته، برخی از ویژگیهای ایجاد رشته (در اینجا هیچ کدام از آنها عبور نمیکند، بنابراین فقط NULL
است)، بازگشت به تماس در رشته جدید (در اینجا thread_callback
) و یک نشانگر آرگومان اختیاری برای ارسال به آن نیاز دارد. در صورتی که می خواهید برخی از داده ها را از رشته اصلی به اشتراک بگذارید - در این مثال ما یک اشاره گر به متغیر arg
به اشتراک می گذاریم.
pthread_join
می توان بعداً در هر زمانی فراخوانی کرد تا منتظر بمانیم تا thread اجرا را به پایان برساند و نتیجه را از callback برگرداند. دسته نخ اختصاص داده شده قبلی و همچنین یک اشاره گر برای ذخیره نتیجه را می پذیرد. در این مورد، هیچ نتیجه ای وجود ندارد، بنابراین تابع یک NULL
به عنوان آرگومان می گیرد.
برای کامپایل کد با استفاده از رشته ها با Emscripten، باید emcc
را فراخوانی کنید و یک پارامتر -pthread
را ارسال کنید، مانند زمانی که همان کد را با Clang یا GCC در پلتفرم های دیگر کامپایل می کنید:
emcc -pthread example.c -o example.js
با این حال، هنگامی که میخواهید آن را در مرورگر یا Node.js اجرا کنید، یک هشدار میبینید و سپس برنامه هنگ میکند:
Before the thread
Tried to spawn a new thread, but the thread pool is exhausted.
This might result in a deadlock unless some threads eventually exit or the code
explicitly breaks out to the event loop.
If you want to increase the pool size, use setting `-s PTHREAD_POOL_SIZE=...`.
If you want to throw an explicit error instead of the risk of deadlocking in those
cases, use setting `-s PTHREAD_POOL_SIZE_STRICT=2`.
[…hangs here…]
چه اتفاقی افتاد؟ مشکل این است که بیشتر API های وقت گیر در وب ناهمزمان هستند و برای اجرا به حلقه رویداد متکی هستند. این محدودیت یک تمایز مهم در مقایسه با محیطهای سنتی است، جایی که برنامهها معمولاً I/O را به صورت همزمان و مسدودکننده اجرا میکنند. اگر میخواهید بیشتر بدانید، پست وبلاگ درباره استفاده از APIهای وب ناهمزمان از WebAssembly را بررسی کنید.
در این مورد، کد به طور همزمان pthread_create
را برای ایجاد یک رشته پسزمینه فراخوانی میکند و با فراخوانی همزمان دیگری به pthread_join
که منتظر میماند تا رشته پسزمینه اجرا شود، ادامه مییابد. با این حال، Web Workers که در پشت صحنه هنگام کامپایل شدن این کد با Emscripten استفاده می شوند، ناهمزمان هستند. بنابراین آنچه اتفاق میافتد این است که pthread_create
فقط یک موضوع Worker جدید را برنامهریزی میکند تا در اجرای حلقه رویداد بعدی ایجاد شود، اما سپس pthread_join
بلافاصله حلقه رویداد را مسدود میکند تا منتظر آن Worker بماند و با انجام این کار از ایجاد آن جلوگیری میکند. این یک نمونه کلاسیک از بن بست است.
یکی از راههای حل این مشکل، ایجاد مجموعهای از کارگران قبل از شروع برنامه است. هنگامی که pthread_create
فراخوانی می شود، می تواند یک Worker آماده برای استفاده را از استخر بگیرد، پاسخ تماس ارائه شده را روی رشته پس زمینه خود اجرا کند و Worker را به استخر بازگرداند. همه اینها را می توان به صورت همزمان انجام داد، بنابراین تا زمانی که استخر به اندازه کافی بزرگ باشد، هیچ بن بست وجود نخواهد داشت.
این دقیقاً همان چیزی است که Emscripten با گزینه -s PTHREAD_POOL_SIZE=...
اجازه می دهد. این اجازه می دهد تا تعدادی رشته را مشخص کنید - یا یک عدد ثابت، یا یک عبارت جاوا اسکریپت مانند navigator.hardwareConcurrency
تا به تعداد هسته های موجود در CPU رشته ایجاد کنید. گزینه دوم زمانی مفید است که کد شما می تواند به تعداد دلخواه رشته ها مقیاس شود.
در مثال بالا، تنها یک رشته در حال ایجاد است، بنابراین به جای رزرو همه هستهها، کافی است از -s PTHREAD_POOL_SIZE=1
استفاده کنید:
emcc -pthread -s PTHREAD_POOL_SIZE=1 example.c -o example.js
این بار، وقتی آن را اجرا می کنید، همه چیز با موفقیت کار می کند:
Before the thread
Inside the thread: 42
After the thread
Pthread 0x701510 exited.
اما مشکل دیگری وجود دارد: sleep(1)
را در مثال کد ببینید؟ در thread callback اجرا می شود، یعنی خارج از رشته اصلی، پس باید خوب باشد، درست است؟ خوب، اینطور نیست.
هنگامی که pthread_join
فراخوانی می شود، باید منتظر بماند تا اجرای thread به پایان برسد، به این معنی که اگر thread ایجاد شده وظایف طولانی مدت را انجام دهد - در این مورد، 1 ثانیه خواب است - آنگاه موضوع اصلی نیز باید به همان مقدار مسدود شود. زمان تا بازگشت نتایج هنگامی که این JS در مرورگر اجرا میشود، رشته رابط کاربری را به مدت 1 ثانیه مسدود میکند تا زمانی که تماس مجدد رشته بازگردد. این منجر به تجربه کاربری ضعیف می شود.
راه حل های کمی برای این وجود دارد:
-
pthread_detach
-
-s PROXY_TO_PTHREAD
- Custom Worker و Comlink
pthread_deach
ابتدا، اگر فقط نیاز به اجرای برخی وظایف از رشته اصلی دارید، اما نیازی نیست منتظر نتایج باشید، میتوانید به جای pthread_join
pthread_detach
کنید. این باعث می شود که تماس مجدد موضوع در پس زمینه اجرا شود. اگر از این گزینه استفاده می کنید، می توانید اخطار را با -s PTHREAD_POOL_SIZE_STRICT=0
خاموش کنید.
PROXY_TO_PTHREAD
دوم، اگر به جای یک کتابخانه، یک برنامه C را کامپایل میکنید، میتوانید از گزینه -s PROXY_TO_PTHREAD
استفاده کنید، که کد برنامه اصلی را علاوه بر رشتههای تودرتو ایجاد شده توسط خود برنامه، در یک رشته جداگانه بارگذاری میکند. به این ترتیب، کد اصلی می تواند در هر زمان و بدون فریز کردن رابط کاربری، با خیال راحت مسدود شود. اتفاقاً، هنگام استفاده از این گزینه، نیازی نیست که thread pool را از قبل ایجاد کنید - در عوض، Emscripten میتواند از رشته اصلی برای ایجاد Workers زیرین جدید استفاده کند و سپس رشته کمکی را در pthread_join
بدون بنبست مسدود کند.
کاملینک
سوم، اگر روی یک کتابخانه کار میکنید و هنوز نیاز به مسدود کردن دارید، میتوانید Worker خود را ایجاد کنید، کد تولید شده توسط Emscripten را وارد کنید و آن را با Comlink در thread اصلی قرار دهید. رشته اصلی قادر خواهد بود هر روش صادر شده را به عنوان توابع ناهمزمان فراخوانی کند و به این ترتیب از مسدود کردن رابط کاربری نیز جلوگیری می کند.
در یک برنامه ساده مانند مثال قبلی -s PROXY_TO_PTHREAD
بهترین گزینه است:
emcc -pthread -s PROXY_TO_PTHREAD example.c -o example.js
C++
تمام اخطارها و منطق یکسان در C++ به همین صورت اعمال می شود. تنها چیز جدیدی که به دست می آورید دسترسی به API های سطح بالاتر مانند std::thread
و std::async
است که از کتابخانه pthread
قبلاً مورد بحث در زیر هود استفاده می کنند.
بنابراین مثال بالا را می توان در C++ اصطلاحی تر مانند این بازنویسی کرد:
example.cpp:
#include <iostream>
#include <thread>
#include <chrono>
int main()
{
puts("Before the thread");
int arg = 42;
std::thread thread([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Inside the thread: " << arg << std::endl;
});
thread.join();
std::cout << "After the thread" << std::endl;
return 0;
}
هنگامی که با پارامترهای مشابه کامپایل و اجرا می شود، مانند مثال C عمل می کند:
emcc -std=c++11 -pthread -s PROXY_TO_PTHREAD example.cpp -o example.js
خروجی:
Before the thread
Inside the thread: 42
Pthread 0xc06190 exited.
After the thread
Proxied main thread 0xa05c18 finished with return code 0. EXIT_RUNTIME=0 set, so
keeping main thread alive for asynchronous event operations.
Pthread 0xa05c18 exited.
زنگ زدگی
برخلاف Emscripten، Rust یک هدف وب تخصصی سرتاسر ندارد، اما در عوض یک هدف عمومی wasm32-unknown-unknown
برای خروجی WebAssembly عمومی ارائه می دهد.
اگر Wasm قرار است در یک محیط وب استفاده شود، هرگونه تعامل با APIهای جاوا اسکریپت به کتابخانههای خارجی و ابزارهایی مانند wasm-bindgen و wasm-pack واگذار میشود. متأسفانه، این بدان معناست که کتابخانه استاندارد از Web Workers آگاه نیست و APIهای استاندارد مانند std::thread
هنگام کامپایل شدن در WebAssembly کار نمی کنند.
خوشبختانه، اکثر اکوسیستم به کتابخانه های سطح بالاتر برای مراقبت از چند رشته ای وابسته است. در آن سطح، انتزاع همه تفاوتهای پلتفرم بسیار سادهتر است.
به طور خاص، Rayon محبوب ترین گزینه برای موازی سازی داده ها در Rust است. این به شما امکان میدهد زنجیرههای متد را روی تکرارکنندههای معمولی بگیرید و معمولاً با یک تغییر خط، آنها را بهگونهای تبدیل کنید که بهجای متوالی، روی همه رشتههای موجود به صورت موازی اجرا شوند. به عنوان مثال:
pub fn sum_of_squares(numbers: &[i32]) -> i32 {
numbers
.iter()
.par_iter()
.map(|x| x * x)
.sum()
}
با این تغییر کوچک، کد داده های ورودی را تقسیم می کند، x * x
و مجموع جزئی را در رشته های موازی محاسبه می کند و در پایان آن نتایج جزئی را با هم جمع می کند.
برای تطبیق برای پلتفرمهایی std::thread
کار نمیکنند، Rayon قلابهایی را ارائه میکند که امکان تعریف منطق سفارشی برای تخمریزی و خروج نخها را فراهم میکند.
wasm-bindgen-rayon به این قلاب ها ضربه می زند تا رشته های WebAssembly را به عنوان Web Workers ایجاد کند. برای استفاده از آن، باید آن را به عنوان یک وابستگی اضافه کنید و مراحل پیکربندی شرح داده شده در اسناد را دنبال کنید. مثال بالا در نهایت به این شکل خواهد بود:
pub use wasm_bindgen_rayon::init_thread_pool;
#[wasm_bindgen]
pub fn sum_of_squares(numbers: &[i32]) -> i32 {
numbers
.par_iter()
.map(|x| x * x)
.sum()
}
پس از اتمام، جاوا اسکریپت تولید شده تابع initThreadPool
اضافی را صادر می کند. این تابع مجموعه ای از Workers را ایجاد می کند و از آنها در طول عمر برنامه برای هر عملیات چند رشته ای انجام شده توسط Rayon استفاده مجدد می کند.
این مکانیسم Pool مشابه گزینه -s PTHREAD_POOL_SIZE=...
در Emscripten است که قبلا توضیح داده شد، و همچنین باید قبل از کد اصلی مقداردهی اولیه شود تا از بن بست جلوگیری شود:
import init, { initThreadPool, sum_of_squares } from './pkg/index.js';
// Regular wasm-bindgen initialization.
await init();
// Thread pool initialization with the given number of threads
// (pass `navigator.hardwareConcurrency` if you want to use all cores).
await initThreadPool(navigator.hardwareConcurrency);
// ...now you can invoke any exported functions as you normally would
console.log(sum_of_squares(new Int32Array([1, 2, 3]))); // 14
توجه داشته باشید که همان اخطارها در مورد مسدود کردن موضوع اصلی در اینجا نیز اعمال می شود. حتی مثال sum_of_squares
همچنان باید رشته اصلی را مسدود کند تا منتظر نتایج جزئی از رشتههای دیگر باشد.
بسته به پیچیدگی تکرارکنندهها و تعداد رشتههای موجود، ممکن است یک انتظار بسیار کوتاه یا طولانی باشد، اما برای حفظ امنیت، موتورهای مرورگر به طور فعال از مسدود کردن رشته اصلی جلوگیری میکنند و چنین کدی با خطا مواجه میشود. در عوض، باید یک Worker ایجاد کنید، کد wasm-bindgen
-generated را در آنجا وارد کنید و API آن را با کتابخانه ای مانند Comlink در رشته اصلی قرار دهید.
نمونه wasm-bindgen-rayon را برای یک نسخه نمایشی پایان به انتها که نشان می دهد، بررسی کنید:
- تشخیص ویژگی رشته ها
- ساختن نسخه های تک و چند رشته ای از همان برنامه Rust.
- بارگیری JS+Wasm تولید شده توسط wasm-bindgen در یک Worker.
- استفاده از wasm-bindgen-rayon برای مقداردهی اولیه یک Thread Pool.
- استفاده از Comlink برای نمایش Worker's API در رشته اصلی .
موارد استفاده در دنیای واقعی
ما به طور فعال از رشتههای WebAssembly در Squoosh.app برای فشردهسازی تصویر سمت سرویس گیرنده استفاده میکنیم – بهویژه برای فرمتهایی مانند AVIF (C++)، JPEG-XL (C++)، OxiPNG (Rust) و WebP v2 (C++). فقط به لطف چند رشته ای، ما شاهد افزایش سرعت ثابت 1.5x-3x بوده ایم (نسبت دقیق در هر کدک متفاوت است)، و توانستیم این اعداد را با ترکیب رشته های WebAssembly با WebAssembly SIMD بیشتر کنیم!
Google Earth سرویس قابل توجه دیگری است که از رشته های WebAssembly برای نسخه وب خود استفاده می کند.
FFMPEG.WASM یک نسخه WebAssembly از زنجیره ابزار چندرسانه ای محبوب FFmpeg است که از رشته های WebAssembly برای رمزگذاری موثر ویدیوها به طور مستقیم در مرورگر استفاده می کند.
نمونه های هیجان انگیز بیشتری با استفاده از موضوعات WebAssembly وجود دارد. حتماً دموها را بررسی کنید و برنامهها و کتابخانههای چند رشتهای خود را به وب بیاورید!