C, C++, और Rust से मिले WebAssembly थ्रेड का इस्तेमाल करना

दूसरी भाषाओं में लिखे गए मल्टीथ्रेड ऐप्लिकेशन को WebAssembly में लाने का तरीका जानें.

WebAssembly थ्रेड के लिए इस्तेमाल की जाने वाली सुविधा, WebAssembly में जोड़ी गई परफ़ॉर्मेंस के सबसे अहम फ़ायदों में से एक है. इसकी मदद से, कोड के कुछ हिस्सों को अलग-अलग कोर पर साथ-साथ चलाया जा सकता है या इनपुट डेटा के स्वतंत्र हिस्सों पर एक ही कोड को चलाया जा सकता है. साथ ही, इसे उपयोगकर्ता की ज़रूरत के मुताबिक ज़्यादा कोर पर स्केल किया जा सकता है और एक्ज़ीक्यूशन में लगने वाले कुल समय को भी कम किया जा सकता है.

इस लेख में आप C, C++, और Rust जैसी भाषाओं में लिखे गए मल्टीथ्रेड ऐप्लिकेशन को वेब पर लाने के लिए WebAssembly थ्रेड का इस्तेमाल करने का तरीका जानेंगे.

WebAssembly थ्रेड के काम करने का तरीका

WebAssembly थ्रेड कोई अलग सुविधा नहीं है, बल्कि यह कई कॉम्पोनेंट का मिला-जुला रूप है. इसकी मदद से WebAssembly ऐप्लिकेशन, वेब पर पारंपरिक मल्टीथ्रेडिंग मॉडल का इस्तेमाल कर सकते हैं.

वेब वर्कर

पहला कॉम्पोनेंट, ऐसे सामान्य वर्कर हैं जिनके बारे में आपको पता है और जिन्हें JavaScript से प्यार है. WebAssembly थ्रेड, new Worker कंस्ट्रक्टर का इस्तेमाल करके नए थ्रेड बनाने में मदद करते हैं. हर थ्रेड एक JavaScript ग्लू लोड करता है. इसके बाद, मुख्य थ्रेड, कंपाइल किए गए डेटा को शेयर करने के लिए, Worker#postMessage तरीके का इस्तेमाल करता हैWebAssembly.Module. साथ ही, उन अन्य थ्रेड के साथ शेयर किया गया WebAssembly.Memory (नीचे देखें) वाला तरीका भी इस्तेमाल करता है. इससे कम्यूनिकेशन हो जाता है. साथ ही, उन सभी थ्रेड को एक ही शेयर की गई मेमोरी पर एक ही WebAssembly कोड, फिर से JavaScript का इस्तेमाल किए बिना चलाने की अनुमति मिलती है.

वेब वर्कर अब एक दशक से ज़्यादा समय से काम कर रहे हैं. वे बड़े पैमाने पर काम करते हैं और उन्हें किसी खास फ़्लैग की ज़रूरत नहीं है.

SharedArrayBuffer

WebAssembly मेमोरी को JavaScript एपीआई में WebAssembly.Memory ऑब्जेक्ट के तौर पर दिखाया जाता है. डिफ़ॉल्ट रूप से, WebAssembly.Memory, ArrayBuffer के चारों ओर एक रैपर होता है. यह रॉ बाइट बफ़र होता है, जिसे सिर्फ़ एक थ्रेड से ऐक्सेस किया जा सकता है.

> new WebAssembly.Memory({ initial:1, maximum:10 }).buffer
ArrayBuffer { … }

एक से ज़्यादा थ्रेड की सुविधा के लिए, WebAssembly.Memory को शेयर किया गया वैरिएंट भी मिला. JavaScript एपीआई या WebAssembly बाइनरी की मदद से, shared फ़्लैग के साथ बनाए जाने पर, यह SharedArrayBuffer के चारों ओर एक रैपर बन जाता है. यह ArrayBuffer का एक वैरिएशन है. इसे दूसरे थ्रेड के साथ शेयर किया जा सकता है और किसी भी तरफ़ से एक साथ पढ़ा जा सकता है या इसमें बदलाव किया जा सकता है.

> new WebAssembly.Memory({ initial:1, maximum:10, shared:true }).buffer
SharedArrayBuffer { … }

postMessage के उलट, आम तौर पर इसका इस्तेमाल मुख्य थ्रेड और वेब वर्कर के बीच कम्यूनिकेशन के लिए किया जाता है. SharedArrayBuffer में, डेटा कॉपी करने या यहां तक कि इवेंट लूप के मैसेज भेजने और पाने का इंतज़ार करने की ज़रूरत नहीं होती. इसके बजाय, सभी थ्रेड में कोई भी बदलाव तुरंत दिखने लगता है. इससे यह परंपरागत सिंक्रोनाइज़ेशन प्रिमिटिव के लिए बेहतर कंपाइलेशन टारगेट बन जाता है.

SharedArrayBuffer का इतिहास काफ़ी जटिल है. साल 2017 के मध्य तक, इसे कई ब्राउज़र में भेजा गया, लेकिन 2018 की शुरुआत में इसे बंद कर दिया गया था. ऐसा स्पेक्टर की कमियों की खोज की वजह से हुआ था. इसकी वजह यह थी कि स्पेक्टर में डेटा निकालने के लिए, टाइमिंग अटैक का इस्तेमाल किया जाता है. इससे कोड के किसी खास हिस्से के लागू होने में लगने वाले समय को मापा जाता है. इस तरह के हमले को सख्त बनाने के लिए, ब्राउज़र ने Date.now और performance.now जैसे स्टैंडर्ड टाइमिंग एपीआई को सटीक बनाने की प्रोसेस को कम कर दिया है. हालांकि, शेयर की गई मेमोरी और अलग थ्रेड में चलने वाले आसान काउंटर लूप के साथ, ज़्यादा सटीक समय का पता लगाने का यह एक भरोसेमंद तरीका भी है. साथ ही, रनटाइम की परफ़ॉर्मेंस को बहुत ज़्यादा थ्रॉट किए बिना, इसे कम करना बहुत मुश्किल होता है.

इसके बजाय, Chrome 68 (मध्य-2018) में SharedArrayBuffer को फिर से चालू करने के लिए, साइट आइसोलेशन का इस्तेमाल किया गया. यह सुविधा अलग-अलग वेबसाइटों को अलग-अलग प्रोसेस में लाती है. इससे स्पेक्टर जैसे साइड-चैनल हमलों को इस्तेमाल करना और भी मुश्किल हो जाता है. हालांकि, यह प्रतिबंध अब भी सिर्फ़ Chrome डेस्कटॉप तक ही सीमित था, क्योंकि साइट को अलग करना काफ़ी महंगा सुविधा है. साथ ही, इसे कम मेमोरी वाले मोबाइल डिवाइस पर मौजूद सभी साइटों के लिए डिफ़ॉल्ट रूप से चालू नहीं किया जा सका और न ही इसे दूसरे वेंडर ने अभी तक लागू किया था.

साल 2020 से, Chrome और Firefox दोनों में साइट आइसोलेशन की सुविधा लागू होती है. साथ ही, वेबसाइटों के लिए एक स्टैंडर्ड तरीका है कि वे COOP और COEP हेडर की मदद से इस सुविधा में ऑप्ट-इन कर सकें. ऑप्ट-इन करने का तरीका, कम क्षमता वाले डिवाइसों पर भी साइट आइसोलेशन का इस्तेमाल करने देता है. ऐसा तब होता है, जब सभी वेबसाइटों के लिए इसे चालू करना बहुत महंगा होता है. ऑप्ट-इन करने के लिए, अपने सर्वर कॉन्फ़िगरेशन में मुख्य दस्तावेज़ में नीचे दिए गए हेडर जोड़ें:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

ऑप्ट-इन करने पर, आपको SharedArrayBuffer (इसमें WebAssembly.Memory का ऐक्सेस शामिल है) का ऐक्सेस मिल जाता है. इसके लिए, SharedArrayBuffer का इस्तेमाल किया जाता है. साथ ही, आपको सटीक टाइमर, मेमोरी की माप, और अन्य एपीआई का ऐक्सेस भी मिलता है. इन एपीआई के ऐक्सेस के लिए, सुरक्षा को ध्यान में रखते हुए एक ऑरिजिन की ज़रूरत होती है. ज़्यादा जानकारी के लिए, COOP और COEP का इस्तेमाल करके अपनी वेबसाइट को "क्रॉस-ऑरिजिन आइसोलेटेड" बनाना देखें.

वेबअसेंबल ऐटमिक्स

SharedArrayBuffer हर थ्रेड को एक ही मेमोरी में पढ़ने और उसमें बदलाव करने की अनुमति देता है. हालांकि, सही तरीके से बातचीत करने के लिए, आपको यह पक्का करना है कि वे एक ही समय पर अलग-अलग तरह की कार्रवाइयां न करें. उदाहरण के लिए, हो सकता है कि एक थ्रेड, शेयर किए गए पते से डेटा पढ़ना शुरू कर दे, जबकि दूसरा थ्रेड उस पर लिख रहा हो. इसलिए, पहली थ्रेड में अब गड़बड़ी वाला नतीजा मिलेगा. बग की इस कैटगरी को रेस कंडिशन कहा जाता है. रेस की शर्तों को रोकने के लिए, आपको उन ऐक्सेस को किसी तरह सिंक करना होगा. ऐसे में, ऐटॉमिक ऑपरेशन शुरू होते हैं.

WebAssembly एटॉमिक्स WebAssembly के निर्देशों के सेट का एक्सटेंशन है. इसकी मदद से, डेटा के छोटे सेल (आम तौर पर, 32- और 64-बिट के पूर्णांक) को "ऐटॉमिकली" रूप से पढ़ा और लिखा जा सकता है. इससे यह गारंटी मिलती है कि दो थ्रेड, एक ही समय में एक ही सेल में पढ़ या लिख नहीं रहे हैं. साथ ही, ऐसे टकरावों को कम लेवल पर नहीं रोक सकते. इसके अलावा, WebAssembly ऐटॉमिक्स में दो और तरह के निर्देश होते हैं—"wait" और "सूचनाएं"—जिसमें एक थ्रेड को, शेयर की गई मेमोरी में दिए गए पते पर तब तक सोने ("इंतज़ार") करने की अनुमति मिलती है, जब तक कि दूसरी थ्रेड "सूचना" की मदद से स्क्रीन को चालू न कर दे.

चैनल, म्यूटेक्स, और पढ़ने-लिखने के लॉक सहित उच्च-लेवल सिंक करने के सभी प्रिमिटिव उन निर्देशों के हिसाब से बने होते हैं.

WebAssembly थ्रेड इस्तेमाल करने का तरीका

सुविधा की पहचान करने की सुविधा

WebAssembly ऐटॉमिक्स और SharedArrayBuffer कुछ नई सुविधाएं हैं. फ़िलहाल, ये WebAssembly की सुविधा वाले सभी ब्राउज़र में उपलब्ध नहीं हैं. webassembly.org रोडमैप पर जाकर देखें कि किन ब्राउज़र में WebAssembly की नई सुविधाएं काम करती हैं.

यह पक्का करने के लिए कि सभी उपयोगकर्ता आपके ऐप्लिकेशन को लोड कर सकें, आपको Wasm के दो अलग-अलग वर्शन बनाकर, एक-एक करके बेहतर बनाने की सुविधा लागू करनी होगी. इनमें से एक वर्शन, मल्टीथ्रिंग की सुविधा के साथ होगा और दूसरा, बिना. इसके बाद, सुविधा पहचानने के नतीजों के आधार पर उस पर काम करने वाले वर्शन को लोड करें. रनटाइम पर WebAssembly थ्रेड के सपोर्ट का पता लगाने के लिए, vasm-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

C में, खास तौर पर Unix जैसे सिस्टम पर, थ्रेड इस्तेमाल करने का आम तरीका, pthread लाइब्रेरी से मिले POSIX Threads का इस्तेमाल करता है. Emscripten वेब वर्कर, शेयर की गई मेमोरी, और ऐटॉमिक्स पर बनी pthread लाइब्रेरी का एपीआई के साथ काम करने वाला एक काम करता है, ताकि वही कोड वेब पर बिना किसी बदलाव के काम कर सके.

आइए, एक उदाहरण देखें:

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 को बाद में किसी भी समय कॉल किया जा सकता है. यह पहले असाइन किए गए थ्रेड हैंडल के साथ-साथ, नतीजा सेव करने के लिए पॉइंटर भी स्वीकार करता है. इस मामले में, कोई नतीजा नहीं मिलता, इसलिए फ़ंक्शन, 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…]

इसकी क्या वजह है? समस्या यह है कि वेब पर समय लेने वाले ज़्यादातर एपीआई एसिंक्रोनस होते हैं और एक्ज़ीक्यूशन के लिए इवेंट लूप पर निर्भर करते हैं. यह सीमा पारंपरिक परिवेश की तुलना में एक अहम अंतर है, जहां ऐप्लिकेशन आम तौर पर I/O को सिंक्रोनस, ब्लॉक करने वाले तरीके से चलाते हैं. अगर आपको इस बारे में ज़्यादा जानना है, तो WebAssembly से एसिंक्रोनस वेब एपीआई इस्तेमाल करने के बारे में ब्लॉग पोस्ट देखें.

इस मामले में, कोड एक बैकग्राउंड थ्रेड बनाने के लिए, सिंक्रोनस रूप से pthread_create को शुरू करता है. इसके बाद, pthread_join को एक अन्य सिंक्रोनस कॉल किया जाता है, जो बैकग्राउंड थ्रेड के एक्ज़ीक्यूशन के पूरा होने का इंतज़ार करता है. हालांकि, Emscripten के साथ इस कोड को कंपाइल करते समय पर्दे के पीछे इस्तेमाल किए जाने वाले वेब वर्कर एसिंक्रोनस होते हैं. तो क्या होता है, pthread_create सिर्फ़ शेड्यूल करता है कि एक नया वर्कर थ्रेड शुरू होगा और उसे अगली बार इवेंट लूप चलाने पर बनाया जाएगा. इसके बाद, pthread_join उस वर्कर की इंतज़ार करने के लिए, इवेंट लूप को तुरंत ब्लॉक कर देता है. यह डेडलॉक का एक क्लासिक उदाहरण है.

इस समस्या को हल करने का एक तरीका यह है कि प्रोग्राम शुरू होने से पहले ही, कर्मचारियों का एक पूल बना दिया जाए. pthread_create को शुरू करने पर, यह पूल से इस्तेमाल के लिए तैयार वर्कर को पूल से ले सकता है, दिए गए कॉलबैक को उसके बैकग्राउंड थ्रेड पर चला सकता है, और वर्कर को वापस पूल में लौटा सकता है. ये सभी काम एक साथ किए जा सकते हैं, इसलिए जब तक पूल काफ़ी बड़ा नहीं है, तब तक कोई डेडलॉक नहीं दिखेगा.

Emscripten की मदद से, -s PTHREAD_POOL_SIZE=... विकल्प इस्तेमाल करने की अनुमति मिलती है. इसकी मदद से, कई थ्रेड की जानकारी दी जा सकती है. जैसे, एक तय संख्या या navigator.hardwareConcurrency जैसे JavaScript एक्सप्रेशन का इस्तेमाल करके, सीपीयू पर कोर के तौर पर जितने चाहें उतने थ्रेड बनाए जा सकते हैं. बाद वाला विकल्प तब मददगार होता है, जब आपके कोड को मनचाहे तरीके से थ्रेड तक बढ़ाया जा सकता है.

ऊपर दिए गए उदाहरण में, सिर्फ़ एक थ्रेड बनाया जा रहा है. इसलिए, सभी कोर रिज़र्व करने के बजाय, -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) देखना है? यह थ्रेड कॉलबैक में चलता है, यानी कि मुख्य थ्रेड से अलग है. इसलिए, यह ठीक होना चाहिए, है न? खैर, ऐसा नहीं है.

जब pthread_join को कॉल किया जाता है, तो इसे थ्रेड एक्ज़ीक्यूशन के पूरा होने तक इंतज़ार करना पड़ता है. इसका मतलब है कि अगर बनाया गया थ्रेड लंबे समय तक चलने वाले टास्क कर रहा है—इस मामले में, 1 सेकंड की नींद—तो मुख्य थ्रेड को भी उतने ही समय के लिए ब्लॉक करना होगा, जब तक नतीजे वापस नहीं आते. ब्राउज़र में इस JS को लागू करने पर, यह यूज़र इंटरफ़ेस (यूआई) थ्रेड को एक सेकंड के लिए ब्लॉक कर देगा. ऐसा तब तक होगा, जब तक थ्रेड कॉलबैक वापस नहीं दिखता. इससे उपयोगकर्ता को खराब अनुभव मिलता है.

इसके कुछ समाधान हैं:

  • pthread_detach
  • -s PROXY_TO_PTHREAD
  • कस्टम वर्कर और Comlink

pthread_detach

सबसे पहले, अगर आपको मुख्य थ्रेड से कुछ ही टास्क चलाने हैं, लेकिन नतीजों के लिए इंतज़ार नहीं करना है, तो pthread_join के बजाय pthread_detach का इस्तेमाल करें. इससे थ्रेड कॉलबैक बैकग्राउंड में चलता रहेगा. अगर इस विकल्प का इस्तेमाल किया जा रहा है, तो चेतावनी को बंद करने के लिए -s PTHREAD_POOL_SIZE_STRICT=0 का इस्तेमाल करें.

PROXY_TO_PTHREAD

दूसरा, अगर लाइब्रेरी के बजाय C ऐप्लिकेशन को कंपाइल किया जा रहा है, तो -s PROXY_TO_PTHREAD विकल्प का इस्तेमाल किया जा सकता है. यह ऐप्लिकेशन के ज़रिए बनाए गए नेस्ट किए गए थ्रेड के अलावा, मुख्य ऐप्लिकेशन कोड को एक अलग थ्रेड में ऑफ़लोड करेगा. इस तरह, मुख्य कोड किसी भी समय यूज़र इंटरफ़ेस (यूआई) को फ़्रीज़ किए बिना, सुरक्षित तरीके से ब्लॉक कर सकता है. आम तौर पर, इस विकल्प का इस्तेमाल करते समय आपको पहले से ही थ्रेड पूल बनाने की ज़रूरत नहीं होती. इसके बजाय, Emscripten मुख्य थ्रेड का इस्तेमाल करके, नए वर्कर बनाने के लिए इस्तेमाल कर सकता है. इसके बाद, pthread_join में हेल्पर थ्रेड को बिना डेडलॉक किए ब्लॉक कर सकता है.

तीसरा, अगर लाइब्रेरी पर काम किया जा रहा है और फिर भी आपको ब्लॉक करना है, तो खुद का वर्कर बनाया जा सकता है. इसके अलावा, Emscripten से जनरेट किया गया कोड इंपोर्ट किया जा सकता है और उसे मुख्य थ्रेड में Comlink की मदद से दिखाया जा सकता है. मुख्य थ्रेड, एक्सपोर्ट किए गए किसी भी तरीके को एसिंक्रोनस फ़ंक्शन के तौर पर शुरू कर पाएगा. ऐसा करने से, यूज़र इंटरफ़ेस (यूआई) ब्लॉक भी नहीं होगा.

नीचे दिए गए उदाहरण जैसे आसान आवेदन में, -s PROXY_TO_PTHREAD सबसे अच्छा विकल्प है:

emcc -pthread -s PROXY_TO_PTHREAD example.c -o example.js

C++

सभी तरह की चेतावनियां और लॉजिक, C++ पर भी एक ही तरह से लागू होते हैं. आपको सिर्फ़ 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.

Rust

Emscripten से अलग, Rust के पास पूरी तरह से तैयार वेब टारगेट नहीं है. इसके बजाय, यह सामान्य WebAssembly आउटपुट के लिए सामान्य wasm32-unknown-unknown टारगेट देता है.

अगर Wasm को वेब एनवायरमेंट में इस्तेमाल करना है, तो JavaScript एपीआई के साथ होने वाले किसी भी इंटरैक्शन को बाहरी लाइब्रेरी और vasm-bindgen और vasm-pack जैसे टूल पर छोड़ दिया जाता है. माफ़ करें, इसका मतलब यह है कि स्टैंडर्ड लाइब्रेरी को वेब वर्कर के बारे में जानकारी नहीं है. साथ ही, WebAssembly में कंपाइल किए जाने पर, std::thread जैसे स्टैंडर्ड एपीआई काम नहीं करेंगे.

अच्छी बात यह है कि ज़्यादातर नेटवर्क मल्टीथ्रेडिंग की समस्या को ठीक करने के लिए बड़े लेवल की लाइब्रेरी पर निर्भर करता है. उस लेवल पर, प्लैटफ़ॉर्म के सभी अंतरों को दूर करना ज़्यादा आसान होता है.

खास तौर पर, रेऑन, रस्ट में डेटा-समानता के लिए सबसे लोकप्रिय विकल्प है. इससे आपको नियमित इटरेटर पर, तरीकों की चेन लेने की सुविधा मिलती है. आम तौर पर, एक लाइन में बदलाव करके, उन्हें इस तरह से बदला जा सकता है कि वे सभी उपलब्ध थ्रेड पर एक क्रम में चलने के बजाय, समान रूप से चलें. उदाहरण के लिए:

pub fn sum_of_squares(numbers: &[i32]) -> i32 {
  numbers
  .iter()
  .par_iter()
  .map(|x| x * x)
  .sum()
}

इस छोटे से बदलाव से, कोड इनपुट डेटा को अलग-अलग कर देगा, पैरलल थ्रेड में x * x और आंशिक योग का हिसाब लगाएगा और आखिर में उन आंशिक नतीजों को एक साथ जोड़ देगा.

std::thread का इस्तेमाल किए बिना प्लैटफ़ॉर्म के हिसाब से काम करने के लिए, Rayon ने हुक मुहैया कराए हैं. इनसे थ्रेड को बढ़ाने और बाहर निकलने के लिए, पसंद के मुताबिक लॉजिक तय करने में मदद मिलती है.

vasm-bindgen-rayon इन हुक को टैप करके वेब वर्कर के रूप में WebAssembly थ्रेड को तैयार करता है. इसका इस्तेमाल करने के लिए, आपको इसे डिपेंडेंसी के तौर पर जोड़ना होगा. साथ ही, docs में बताए गए कॉन्फ़िगरेशन के चरणों का पालन करना होगा. ऊपर दिया गया उदाहरण कुछ ऐसा दिखेगा:

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()
}

सेट अप पूरा होने के बाद, जनरेट किया गया JavaScript एक और initThreadPool फ़ंक्शन एक्सपोर्ट करेगा. इस फ़ंक्शन से कर्मचारियों का एक पूल बन जाएगा और प्रोग्राम के दौरान, Rayon के कई कई थ्रेड वाले कामों में उन्हें पूरे प्रोग्राम के लिए इस्तेमाल किया जाएगा.

पूल करने का यह तरीका, Emscripten में बताए गए -s PTHREAD_POOL_SIZE=... विकल्प से मिलता-जुलता है. साथ ही, डेडलॉक से बचने के लिए, इसे मुख्य कोड से पहले शुरू करना ज़रूरी है:

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 के उदाहरण को भी दूसरी थ्रेड से आंशिक नतीजों का इंतज़ार करने के लिए मुख्य थ्रेड को ब्लॉक करना होगा.

इसमें बहुत कम इंतज़ार करना पड़ सकता है या फिर लंबा इंतज़ार करना पड़ सकता है. यह काफ़ी मुश्किल काम और उपलब्ध थ्रेड की संख्या पर निर्भर करता है. हालांकि, सुरक्षित रहने के लिए, ब्राउज़र इंजन मुख्य थ्रेड को ब्लॉक करने से सक्रिय रूप से रोक देते हैं. ऐसे कोड से गड़बड़ी हो सकती है. इसके बजाय, आपको एक वर्कर बनाना चाहिए और wasm-bindgen से जनरेट किए गए कोड को वहां इंपोर्ट करना चाहिए. साथ ही, इसके एपीआई को मुख्य थ्रेड में, Comlink जैसी लाइब्रेरी की मदद से दिखाना चाहिए.

शुरू से अंत तक के डेमो के लिए, Wasm-bindgen-rayon का उदाहरण देखें:

असल दुनिया में इस्तेमाल के उदाहरण

हम क्लाइंट-साइड इमेज को कंप्रेस करने के लिए, Squoosh.app में WebAssembly थ्रेड का इस्तेमाल करते हैं. खास तौर पर, AVIF (C++), JPEG-XL (C++), OxiPNG (रस्ट) और WebP v2 (C++) जैसे फ़ॉर्मैट के लिए. मल्टीथ्रेडिंग की बदौलत, हमने 1.5x-3x के बराबर स्पीड-अप कोड को एक साथ (एजुकेबल कोड) के हिसाब से मिलाया है.

Google Earth एक और खास सेवा है, जो अपने वेब वर्शन के लिए WebAssembly थ्रेड का इस्तेमाल कर रही है.

FFMPEG.WASM, एक मशहूर FFmpeg मल्टीमीडिया टूलचेन का WebAssembly वर्शन है. यह ब्राउज़र में वीडियो को बेहतर तरीके से कोड में बदलने के लिए, WebAssembly थ्रेड का इस्तेमाल करता है.

WebAssembly थ्रेड का इस्तेमाल करने के और भी दिलचस्प उदाहरण हैं. डेमो देखना न भूलें और अपने मल्टीथ्रेड ऐप्लिकेशन और लाइब्रेरी को वेब पर लाएं!