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

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

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

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

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

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

वेब वर्कर

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

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

SharedArrayBuffer

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

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

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

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

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

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

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

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

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

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

WebAssembly के एटमिक

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

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

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

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

फ़ीचर का पता लगाना

WebAssembly के एटमिक और SharedArrayBuffer, अपेक्षाकृत नई सुविधाएं हैं. फ़िलहाल, ये सुविधाएं WebAssembly के साथ काम करने वाले सभी ब्राउज़र में उपलब्ध नहीं हैं. webassembly.org के रोडमैप पर जाकर, यह पता लगाया जा सकता है कि कौनसे ब्राउज़र, WebAssembly की नई सुविधाओं के साथ काम करते हैं.

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

C में, खास तौर पर Unix जैसे सिस्टम पर, थ्रेड का इस्तेमाल करने का सामान्य तरीका pthread लाइब्रेरी से मिलने वाले POSIX थ्रेड का इस्तेमाल करना है. 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 में चलाने पर, आपको एक चेतावनी दिखेगी और फिर प्रोग्राम hang हो जाएगा:

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

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 को कॉल करने पर, उसे थ्रेड के पूरा होने का इंतज़ार करना पड़ता है. इसका मतलब है कि अगर बनाई गई थ्रेड लंबे समय तक चलने वाले टास्क कर रही है, तो मुख्य थ्रेड को भी नतीजे मिलने तक उतने ही समय के लिए ब्लॉक करना होगा. इस मामले में, एक सेकंड के लिए ब्लॉक करना होगा. जब इस 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 API के साथ इंटरैक्शन करने की ज़िम्मेदारी, बाहरी लाइब्रेरी और टूल पर छोड़ दी जाती है. जैसे, wasm-bindgen और wasm-pack. माफ़ करें, इसका मतलब है कि स्टैंडर्ड लाइब्रेरी में वेब वर्कर के बारे में जानकारी नहीं है. साथ ही, std::thread जैसे स्टैंडर्ड एपीआई, WebAssembly में कंपाइल होने पर काम नहीं करेंगे.

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

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

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

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

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

एंड-टू-एंड डेमो देखने के लिए, wasm-bindgen-rayon का उदाहरण देखें. इसमें यह दिखाया गया है:

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

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

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

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

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