अन्य भाषाओं में लिखे गए मल्टी-थ्रेड वाले ऐप्लिकेशन को 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 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 के मध्य में कई ब्राउज़र पर भेजा गया था. हालांकि, स्पेक्टर के जोखिमों का पता चलने की वजह से 2018 की शुरुआत में इसे बंद कर दिया गया था. इसकी खास वजह यह थी कि Spectre में डेटा निकालने की सुविधा, टाइमिंग अटैक पर निर्भर करती है. इसमें किसी खास कोड के लागू होने में लगने वाले समय को मेज़र किया जाता है. इस तरह के हमले को मुश्किल बनाने के लिए, ब्राउज़र ने 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 का इस्तेमाल करके, अपनी वेबसाइट को "क्रॉस-ऑरिजिन आइसोलेटेड" बनाना लेख पढ़ें.
WebAssembly के एटमिक
हालांकि, SharedArrayBuffer
हर थ्रेड को एक ही मेमोरी में पढ़ने और उसमें बदलाव करने की अनुमति देता है. हालांकि, सही बातचीत के लिए यह पक्का किया जा सकता है कि वे एक ही समय में अलग-अलग कार्रवाइयां न करें. उदाहरण के लिए, हो सकता है कि एक थ्रेड, शेयर किए गए पते से डेटा पढ़ना शुरू करे, जबकि दूसरी थ्रेड उसमें डेटा लिख रही हो. ऐसे में, पहली थ्रेड को गलत नतीजा मिलेगा. इस कैटगरी के बग को रेस कंडीशन कहा जाता है. रेस कंडीशन से बचने के लिए, आपको उन ऐक्सेस को किसी तरह सिंक करना होगा.
यहीं से ऐटॉमिक ऑपरेशन होते हैं.
WebAssembly के ऐटोमिक्स, WebAssembly के निर्देशों के सेट का एक एक्सटेंशन है. इसकी मदद से, डेटा की छोटी सेल (आम तौर पर 32- और 64-बिट पूर्णांक) को "एक साथ" पढ़ा और लिखा जा सकता है. इसका मतलब यह है कि इस तरह से यह गारंटी मिलती है कि कोई भी दो थ्रेड, एक ही सेल में एक साथ पढ़ या लिख नहीं रहे हैं. इससे इस तरह के टकरावों को कम लेवल पर रोका जा सकता है. इसके अलावा, WebAssembly के एटमिक में दो और निर्देश होते हैं—"wait" और "notify"—जिनकी मदद से, किसी थ्रेड को शेयर की गई मेमोरी में किसी पते पर तब तक स्लीप ("wait") किया जा सकता है, जब तक कि कोई दूसरी थ्रेड "notify" के ज़रिए उसे जगा नहीं देती.
चैनल, म्यूटेक्स, और रीड-राइट लॉक जैसे हाई-लेवल सिंक्रोनाइज़ेशन प्रिमिटिव, इन निर्देशों के आधार पर बनते हैं.
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
थ्रेड का इस्तेमाल करना है. 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
में हेल्पर थ्रेड को ब्लॉक कर सकता है.
Comlink
तीसरा, अगर किसी लाइब्रेरी पर काम किया जा रहा है और आपको अब भी ब्लॉक करने की ज़रूरत है, तो अपना वर्कर्स बनाएं. इसके बाद, 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. इसका मतलब है कि इसका मतलब है कि स्टैंडर्ड लाइब्रेरी को WebAssembly में कंपाइल किए जाने पर, WebWorkers और स्टैंडर्ड एपीआई की जानकारी नहीं है. जैसे,std::thread
.
ज़रूरी नहीं है कि हर ऐप्लिकेशन में मल्टीथ्रेडिंग की सुविधा हो. ज़्यादातर ऐप्लिकेशन, मल्टीथ्रेडिंग की सुविधा के लिए, हाई लेवल लाइब्रेरी का इस्तेमाल करते हैं. इस लेवल पर, प्लैटफ़ॉर्म के बीच के सभी अंतरों को आसानी से हटाया जा सकता है.
खास तौर पर, Rust में डेटा-पैरैलेलिज्म के लिए, Rayon सबसे लोकप्रिय विकल्प है. इसकी मदद से, रेगुलर इटरेटर्स पर मेथड चेन ली जा सकती हैं. आम तौर पर, एक लाइन में बदलाव करके, उन्हें इस तरह बदला जा सकता है कि वे क्रम से चलने के बजाय, सभी उपलब्ध थ्रेड पर एक साथ चलें. उदाहरण के लिए:
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 थ्रेड को बढ़ावा देता है. इसका इस्तेमाल करने के लिए, आपको इसे डिपेंडेंसी के तौर पर जोड़ना होगा. साथ ही, दस्तावेज़ में बताए गए कॉन्फ़िगरेशन के चरणों का पालन करना होगा. ऊपर दिया गया उदाहरण, आखिर में ऐसा दिखेगा:
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 का उदाहरण देखें:
- थ्रेड की सुविधा का पता लगाना.
- एक ही Rust ऐप्लिकेशन के सिंगल- और मल्टी-थ्रेड वर्शन बनाना.
- wasm-bindgen की मदद से जनरेट किए गए JS+Wasm को, वर्कर्स में लोड करना.
- थ्रेड पूल को शुरू करने के लिए, wasm-bindgen-rayon का इस्तेमाल करना.
- मुख्य धागे में Worker के एपीआई को एक्सपोज़ करने के लिए, Comlink का इस्तेमाल करना.
असल दुनिया में इस्तेमाल के उदाहरण
हम क्लाइंट-साइड इमेज को कम करने के लिए, 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 थ्रेड का इस्तेमाल करने के कई और दिलचस्प उदाहरण हैं. डेमो ज़रूर देखें और अपने मल्टीथ्रेड वाले ऐप्लिकेशन और लाइब्रेरी को वेब पर ज़रूर लाएं!