ดูวิธีนำแอปพลิเคชันแบบมัลติเทรดที่เขียนด้วยภาษาอื่นมาไว้ใน WebAssembly
การรองรับชุดข้อความของ WebAssembly เป็นหนึ่งในประสิทธิภาพที่สําคัญที่สุดอย่างหนึ่งของ WebAssembly ซึ่งช่วยให้คุณเรียกใช้ส่วนของโค้ดแบบขนานกันบนแกนที่แยกต่างหาก และเรียกใช้โค้ดเดียวกันบนส่วนที่ไม่อิสระของข้อมูลอินพุต เพื่อปรับขนาดไปยังแกนได้เท่ากับที่ผู้ใช้มี และช่วยลดเวลาในการดำเนินการโดยรวมได้อย่างมาก
ในบทความนี้ คุณจะได้ทราบวิธีใช้ชุดข้อความ WebAssembly เพื่อนำแอปพลิเคชันแบบมัลติเทรด ที่เขียนด้วยภาษาต่างๆ เช่น C, C++ และ Rust ไปใช้ในเว็บ
วิธีการทำงานของชุดข้อความ WebAssembly
ชุดข้อความ WebAssembly ไม่ใช่ฟีเจอร์แยกต่างหาก แต่เป็นการรวมคอมโพเนนต์หลายอย่างที่ช่วยให้แอป WebAssembly สามารถใช้รูปแบบมัลติเทรดแบบดั้งเดิมบนเว็บได้
Web Worker
คอมโพเนนต์แรกคือผู้ปฏิบัติงานทั่วไปที่คุณรู้จักและชื่นชอบจาก JavaScript เทรด WebAssembly ใช้ตัวสร้าง new Worker
เพื่อสร้างเทรดพื้นฐานใหม่ แต่ละเทรดจะโหลดกาวของ JavaScript จากนั้นเทรดหลักจะใช้เมธอด Worker#postMessage
เพื่อแชร์ WebAssembly.Module
ที่คอมไพล์แล้ว รวมถึง WebAssembly.Memory
ที่แชร์ (ดูด้านล่าง) กับเทรดอื่นๆ เหล่านั้น วิธีนี้จะสร้างการสื่อสารและอนุญาตให้เทรดเหล่านั้นเรียกใช้โค้ด WebAssembly เดียวกันในหน่วยความจำที่ใช้ร่วมกันอันเดียวกันได้โดยไม่ต้องผ่าน JavaScript อีกเลย
Web Worker ทำงานมานานกว่า 1 ทศวรรษและได้รับการสนับสนุนอย่างกว้างขวาง และไม่จำเป็นต้องมีการแจ้งว่าไม่เหมาะสมใดๆ เป็นพิเศษ
SharedArrayBuffer
หน่วยความจำ WebAssembly จะแสดงด้วยออบเจ็กต์ WebAssembly.Memory
ใน JavaScript API โดยค่าเริ่มต้น WebAssembly.Memory
คือ Wrapper ที่มี ArrayBuffer
ซึ่งเป็นบัฟเฟอร์ไบต์ดิบที่เข้าถึงได้ด้วยเทรดเดียวเท่านั้น
> new WebAssembly.Memory({ initial:1, maximum:10 }).buffer
ArrayBuffer { … }
WebAssembly.Memory
ได้รับตัวแปรที่แชร์ด้วยเพื่อรองรับการจัดชุดข้อความหลายชุด เมื่อสร้างด้วยแฟล็ก shared
ผ่าน JavaScript API หรือสร้างโดยไบนารีของ WebAssembly พารามิเตอร์ดังกล่าวจะกลายเป็น Wrapper ที่มี SharedArrayBuffer
แทน ซึ่งเป็นรูปแบบของ ArrayBuffer
ที่สามารถแชร์กับชุดข้อความอื่น รวมถึงอ่านหรือแก้ไขพร้อมกันได้จากทั้ง 2 ฝั่ง
> new WebAssembly.Memory({ initial:1, maximum:10, shared:true }).buffer
SharedArrayBuffer { … }
SharedArrayBuffer
ช่วยให้ไม่ต้องคัดลอกข้อมูลหรือแม้กระทั่งรอเหตุการณ์ส่งและรับข้อความ ซึ่งต่างจาก postMessage
ที่โดยปกติแล้วใช้สำหรับการสื่อสารระหว่างเทรดหลักและ Web Worker
แต่เทรดทั้งหมดจะเห็นการเปลี่ยนแปลงได้แทบจะในทันที จึงกลายเป็นเป้าหมายในการคอมไพล์ที่ดียิ่งขึ้นมากสำหรับพื้นฐานของการซิงค์แบบดั้งเดิม
SharedArrayBuffer
มีประวัติที่ซับซ้อน ตอนแรกมีการจัดส่งในหลายเบราว์เซอร์ช่วงกลางปี 2017 แต่ก็ต้องปิดใช้ในช่วงต้นปี 2018 เนื่องจากพบช่องโหว่ใน Spectre เหตุผลที่เฉพาะเจาะจงคือการดึงข้อมูลใน Spectre ใช้การโจมตีแบบจับเวลา ซึ่งวัดเวลาในการประมวลผลของโค้ดชิ้นใดชิ้นหนึ่ง และเพื่อทำให้การโจมตีแบบนี้ยากขึ้น เบราว์เซอร์จะลดความแม่นยำของ API การจับเวลามาตรฐาน เช่น 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
) ตัวจับเวลาที่แม่นยำ การวัดหน่วยความจำ และ API อื่นๆ ที่จำเป็นต้องใช้ต้นทางแยกต่างหากเพื่อเหตุผลด้านความปลอดภัย ดูรายละเอียดเพิ่มเติมได้ที่การทำให้เว็บไซต์ "แยกแบบข้ามต้นทาง" โดยใช้ COOP และ COEP
อะตอมของ WebAssembly
แม้ว่า SharedArrayBuffer
จะอนุญาตให้แต่ละชุดข้อความอ่านและเขียนไปยังหน่วยความจำเดียวกันได้ แต่สำหรับการสื่อสารที่ถูกต้อง คุณต้องตรวจสอบว่าชุดข้อความดังกล่าวจะไม่ดำเนินการที่ขัดแย้งกันในเวลาเดียวกัน ตัวอย่างเช่น เป็นไปได้ว่าชุดข้อความหนึ่งอาจเริ่มอ่านข้อมูลจากที่อยู่ที่แชร์ ในขณะที่ชุดข้อความอื่นกำลังเขียนไปยังชุดข้อความดังกล่าว ดังนั้นชุดข้อความแรกจะได้รับผลลัพธ์ที่เสียหาย ข้อบกพร่องในหมวดหมู่นี้เรียกว่า
เงื่อนไขทางเชื้อชาติ เพื่อป้องกันไม่ให้เกิดเงื่อนไขทางการแข่งขัน คุณจะต้องซิงค์การเข้าถึงเหล่านั้นด้วยวิธีใดวิธีหนึ่ง
นี่คือที่มาของกระบวนการอะตอม
WebAssembly atomics เป็นส่วนขยายของชุดคำสั่ง WebAssembly ที่อนุญาตให้อ่านและเขียนเซลล์ข้อมูลขนาดเล็ก (โดยทั่วไปจะเป็นจำนวนเต็ม 32 และ 64 บิต) "แบบอะตอม" ซึ่งทำให้มั่นใจว่าไม่มีเทรด 2 รายการที่อ่านหรือเขียนไปยังเซลล์เดียวกันในเวลาเดียวกัน และป้องกันความขัดแย้งดังกล่าวในระดับต่ำ นอกจากนี้ WebAssembly atomics ยังมีวิธีการอีก 2 ประเภท คือ "wait" และ "notify" ซึ่งช่วยให้เทรดหนึ่งเข้าสู่โหมดสลีป ("รอ") ในที่อยู่หนึ่งๆ ในหน่วยความจำที่ใช้ร่วมกันจนกว่าเทรดอื่นจะปลุกระบบผ่าน "notify"
ประเภทพื้นฐานการซิงค์ระดับสูงทั้งหมด รวมถึงช่อง, Mutex และล็อกแบบอ่าน-เขียนที่สร้างขึ้นจากคำสั่งเหล่านั้น
วิธีใช้ชุดข้อความ WebAssembly
การตรวจหาฟีเจอร์
WebAssembly atomics และ SharedArrayBuffer
เป็นฟีเจอร์ที่ค่อนข้างใหม่และยังไม่พร้อมใช้งานในเบราว์เซอร์ทั้งหมดที่มีการรองรับ WebAssembly คุณดูเบราว์เซอร์ที่รองรับฟีเจอร์ใหม่ของ WebAssembly ได้ในแผนกลยุทธ์ webassembly.org
เพื่อให้แน่ใจว่าผู้ใช้ทั้งหมดจะโหลดแอปพลิเคชันได้ คุณจะต้องใช้การเพิ่มประสิทธิภาพแบบก้าวหน้าโดยการสร้าง Wasm ที่แตกต่างกัน 2 เวอร์ชัน โดยเวอร์ชันหนึ่งรองรับเทรดแบบมัลติเทรด และเวอร์ชันหนึ่งไม่มีเทรด จากนั้นโหลดเวอร์ชันที่รองรับโดยขึ้นอยู่กับผลลัพธ์ของการตรวจหาฟีเจอร์ หากต้องการตรวจหาการรองรับเทรด WebAssembly ขณะรันไทม์ ให้ใช้ wasm-feature-detect library แล้วโหลดโมดูลตามตัวอย่างต่อไปนี้
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 วิธีทั่วไปในการใช้ชุดข้อความคือผ่าน POSIX
Threads ที่ไลบรารี pthread
มีให้ Emscripten มอบการใช้งานที่เข้ากันได้กับ API ของไลบรารี pthread
ที่สร้างขึ้นบน Web Worker หน่วยความจำที่ใช้ร่วมกัน และ Atomics เพื่อให้โค้ดเดียวกันทำงานบนเว็บได้โดยไม่ต้องมีการเปลี่ยนแปลง
มาดูตัวอย่างกัน
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
คุณยังดูฟังก์ชันที่สำคัญ 2 ข้อ
ในการจัดการกับชุดข้อความได้อีกด้วย
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…]
เกิดอะไรขึ้น ปัญหาคือ API ที่ใช้เวลาส่วนใหญ่บนเว็บเป็นแบบไม่พร้อมกันและต้องใช้ลูปเหตุการณ์ในการดำเนินการ ข้อจำกัดนี้เป็นความแตกต่างที่สำคัญเมื่อเทียบกับสภาพแวดล้อมแบบดั้งเดิม ซึ่งโดยปกติแอปพลิเคชันจะเรียกใช้ I/O แบบพร้อมกันและการบล็อก ลองอ่านบล็อกโพสต์เกี่ยวกับการใช้ API ของเว็บแบบอะซิงโครนัสจาก WebAssembly หากต้องการดูข้อมูลเพิ่มเติม
ในกรณีนี้ โค้ดจะเรียกใช้ pthread_create
แบบพร้อมกันเพื่อสร้างเทรดในเบื้องหลัง จากนั้นตามการเรียกใช้แบบพร้อมกันอีกครั้งไปยัง pthread_join
ซึ่งรอให้เทรดในเบื้องหลังดำเนินการเสร็จสิ้น อย่างไรก็ตาม Web Workers ซึ่งใช้ในเบื้องหลังเมื่อโค้ดนี้คอมไพล์ด้วย Emscripten จะเป็นแบบไม่พร้อมกัน ผลที่ได้คือ pthread_create
จะกำหนดเวลาสร้างเทรดผู้ปฏิบัติงานใหม่ในการวนซ้ำเหตุการณ์ถัดไปเท่านั้น แต่ pthread_join
จะบล็อกเหตุการณ์วนซ้ำทันทีเพื่อรอผู้ปฏิบัติงานคนนั้น และการทำเช่นนั้นจะทำให้ระบบไม่สร้างเหตุการณ์ดังกล่าวอีก ตัวอย่างนี้เป็นตัวอย่างแบบคลาสสิกของการติดตาย
วิธีหนึ่งในการแก้ปัญหานี้คือการสร้างกลุ่มผู้ปฏิบัติงานไว้ล่วงหน้าก่อนที่โปรแกรมจะเริ่มต้น โดยเมื่อมีการเรียกใช้ pthread_create
อาจทำให้ผู้ปฏิบัติงานที่พร้อมใช้งานออกจากกลุ่มได้ เรียกใช้โค้ดเรียกกลับที่ระบุบนเทรดเบื้องหลัง และแสดงผลผู้ปฏิบัติงานกลับไปยังกลุ่ม การดำเนินการทั้งหมดสามารถทำได้พร้อมกัน ดังนั้นจะไม่มีปัญหาการติดตายตราบใดที่สระว่ายน้ำมีขนาดใหญ่พอ
นี่คือสิ่งที่ Emscripten อนุญาตสำหรับตัวเลือก -s
PTHREAD_POOL_SIZE=...
โดยจะช่วยระบุชุดข้อความจำนวนหนึ่ง ไม่ว่าจะเป็นตัวเลขคงที่ หรือนิพจน์ JavaScript เช่น 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)
ในตัวอย่างโค้ดไหม คำสั่งจะทำงานในโค้ดเรียกกลับของเทรด
หมายถึงออกจากเทรดหลัก ก็น่าจะไม่มีปัญหาใช่ไหม แต่ไม่ใช่
เมื่อมีการเรียกใช้ pthread_join
เทรดจะต้องรอให้การดำเนินการเทรดเสร็จสิ้น ซึ่งหมายความว่าหากเทรดที่สร้างขึ้นทำงานที่ใช้เวลานาน ในกรณีนี้ เมื่ออยู่ในสลีป 1 วินาที เทรดหลักจะต้องบล็อกเป็นเวลาเท่าเดิมจนกว่าผลลัพธ์จะกลับมาแสดงอีกครั้ง เมื่อมีการเรียกใช้ JS ในเบราว์เซอร์ เทรด UI จะบล็อกเทรด UI เป็นเวลา 1 วินาทีจนกว่าโค้ดเรียกกลับของเทรดจะปรากฏขึ้น ซึ่งทำให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดี
วิธีแก้ปัญหาที่ทำได้มีดังนี้
pthread_detach
-s PROXY_TO_PTHREAD
- ผู้ปฏิบัติงานที่กำหนดเองและ Comlink
pthread_detach
ก่อนอื่น หากต้องการเรียกใช้เพียงบางงานออกจากเทรดหลัก แต่ไม่จำเป็นต้องรอผลลัพธ์ ให้ใช้ pthread_detach
แทน pthread_join
การดำเนินการนี้จะทำให้การเรียกกลับของชุดข้อความทำงานอยู่เบื้องหลัง หากใช้ตัวเลือกนี้ คุณสามารถปิดคำเตือนได้ด้วย -s
PTHREAD_POOL_SIZE_STRICT=0
PROXY_TO_PTHREAD
ประการที่ 2 หากกำลังคอมไพล์แอปพลิเคชัน C แทนที่จะเป็นไลบรารี คุณจะใช้ตัวเลือก -s
PROXY_TO_PTHREAD
ได้ ซึ่งจะย้ายโค้ดของแอปพลิเคชันหลักไปยังเทรดแยกต่างหาก นอกเหนือจากเทรดที่ซ้อนกันซึ่งสร้างโดยแอปพลิเคชันเอง ด้วยวิธีนี้ โค้ดหลักจะสามารถบล็อกได้อย่างปลอดภัยทุกเมื่อโดยไม่ต้องหยุด UI ชั่วคราว
โดยปกติแล้ว เมื่อใช้ตัวเลือกนี้ คุณไม่จำเป็นต้องสร้างกลุ่มเทรดไว้ล่วงหน้า แต่ Emscripten จะใช้ประโยชน์จากเทรดหลักเพื่อสร้างผู้ปฏิบัติงานที่เกี่ยวข้องรายใหม่ แล้วบล็อกเทรดของตัวช่วยใน pthread_join
ได้โดยไม่ติดตาย
ลิงก์คอมลิงก์
อย่างที่สาม หากกำลังทำงานในไลบรารีและยังคงต้องบล็อก คุณสามารถสร้างผู้ปฏิบัติงานของคุณเอง นำเข้าโค้ดที่สร้างขึ้นจาก Emscripten และแสดงโค้ดดังกล่าวโดยใช้ Comlink ในชุดข้อความหลัก เทรดหลักจะเรียกใช้เมธอดที่ส่งออกเป็นฟังก์ชันแบบไม่พร้อมกันได้ และวิธีนี้จะช่วยให้ไม่บล็อก UI ด้วย
ในแอปพลิเคชันง่ายๆ เช่น ตัวอย่าง -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.
Rust
Rust ไม่มีเป้าหมายเว็บแบบต้นทางถึงปลายทางแบบพิเศษ แต่ให้เป้าหมาย wasm32-unknown-unknown
ทั่วไปสำหรับเอาต์พุต WebAssembly ทั่วไปแทน Emscripten
หากมีวัตถุประสงค์เพื่อให้ใช้ Wasm ในสภาพแวดล้อมแบบเว็บ การโต้ตอบกับ JavaScript 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
และผลรวมบางส่วนในชุดข้อความแบบขนาน และสุดท้ายแล้วจะนำผลลัพธ์บางส่วนมารวมเข้าด้วยกัน
Rayon มีฮุกที่ช่วยให้กำหนดตรรกะที่กำหนดเองสำหรับการแพร่และออกจากเทรดได้เพื่อรองรับแพลตฟอร์มที่ไม่มี std::thread
ที่ใช้งานได้
wasm-bindgen-rayon ใช้ประโยชน์จากตะขอเหล่านั้น เพื่อสร้างชุดข้อความของ WebAssembly เป็น Web Workers หากต้องการใช้งาน คุณจะต้องเพิ่มทรัพยากรเป็นทรัพยากร Dependency และทำตามขั้นตอนการกำหนดค่าที่อธิบายไว้ใน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
กลไกพูลนี้คล้ายกับตัวเลือก -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
สร้างไว้ที่นั่น และแสดง API ของผู้ปฏิบัติงานที่มีไลบรารี เช่น Comlink ในเทรดหลัก
ดูตัวอย่าง Wasm-bindgen-rayon ได้ในการสาธิตตั้งแต่ต้นจนจบซึ่งแสดงข้อมูลต่อไปนี้
- การตรวจหาฟีเจอร์ของชุดข้อความ
- การสร้างแอป Rust เดียวกันในเวอร์ชันเดียวและหลายเทรด
- การโหลด JS+Wasm ที่สร้างโดย Wasm-bindgen ในผู้ปฏิบัติงาน
- การใช้ Wasm-bindgen-rayon เพื่อเริ่มต้นกลุ่มเทรด
- การใช้ Comlink เพื่อแสดง API ของผู้ปฏิบัติงานกับเทรดหลัก
กรณีการใช้งานจริง
เราใช้เทรด WebAssembly ใน Squoosh.app อย่างต่อเนื่องสำหรับการบีบอัดรูปภาพฝั่งไคลเอ็นต์ โดยเฉพาะสำหรับรูปแบบอย่าง AVIF (C++), JPEG-XL (C++), OxiPNG (Rust) และ WebP v2 (C++) ด้วยความสามารถในการบีบอัดรูปภาพแบบมัลติเทรด โดยเพียงอย่างเดียว เราเห็นสัดส่วนของ WebAss (ความเร็ว 1.5 เท่า-3 เท่า) ที่ใกล้เคียงกัน การพุชของ WebAss
Google Earth เป็นอีกบริการที่โดดเด่นที่ใช้ชุดข้อความ WebAssembly สำหรับเวอร์ชันเว็บ
FFMPEG.WASM เป็นเวอร์ชัน WebAssembly ของชุดเครื่องมือมัลติมีเดีย FFmpeg ยอดนิยมที่ใช้ชุดข้อความ WebAssembly ในการเข้ารหัสวิดีโอในเบราว์เซอร์โดยตรงอย่างมีประสิทธิภาพ
มีตัวอย่างที่น่าตื่นเต้นอีกมากมายที่ใช้ชุดข้อความ WebAssembly อย่าลืมดูการสาธิตและนำแอปพลิเคชันและไลบรารีแบบมัลติชุดข้อความของคุณเองมาไว้ในเว็บ!