การใช้ชุดข้อความ WebAssembly จาก C, C++ และ Rust

ดูวิธีนำแอปพลิเคชันแบบมัลติเทรดที่เขียนด้วยภาษาอื่นมาไว้ใน 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 ได้ในการสาธิตตั้งแต่ต้นจนจบซึ่งแสดงข้อมูลต่อไปนี้

กรณีการใช้งานจริง

เราใช้เทรด 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 อย่าลืมดูการสาธิตและนำแอปพลิเคชันและไลบรารีแบบมัลติชุดข้อความของคุณเองมาไว้ในเว็บ!