Sử dụng API web không đồng bộ từ WebAssembly

Các API I/O trên web không đồng bộ nhưng đồng bộ trong hầu hết các ngôn ngữ hệ thống. Thời gian biên dịch mã cho WebAssembly, bạn cần cầu nối một loại API với một loại API khác và cầu nối này là Không đồng bộ. Trong bài đăng này, bạn sẽ tìm hiểu về thời điểm và cách sử dụng tính năng Không đồng bộ hoá cũng như cách hoạt động của tính năng này.

I/O bằng ngôn ngữ hệ thống

Tôi sẽ bắt đầu với một ví dụ đơn giản trong ngôn ngữ C. Giả sử bạn muốn đọc tên người dùng trong tệp và gửi lời chào họ bằng lời nói "Hello, (username)!" (Xin chào, (tên người dùng)!) thông báo:

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

Mặc dù ví dụ này không có nhiều tác dụng, nhưng nó đã thể hiện một điều gì đó mà bạn sẽ tìm thấy trong một ứng dụng thuộc mọi quy mô: nó đọc một số dữ liệu đầu vào từ thế giới bên ngoài, xử lý chúng nội bộ và ghi ra thế giới bên ngoài. Tất cả những tương tác như vậy với thế giới bên ngoài diễn ra thông qua một vài các hàm thường được gọi là hàm đầu vào-đầu ra, cũng được viết tắt thành I/O.

Để đọc tên từ C, bạn cần có ít nhất 2 lệnh gọi I/O quan trọng: fopen để mở tệp và fread để đọc dữ liệu qua đó. Sau khi truy xuất dữ liệu, bạn có thể sử dụng một hàm I/O khác printf để in kết quả ra bảng điều khiển.

Các chức năng đó thoạt nhìn có vẻ khá đơn giản và bạn không phải suy nghĩ kỹ về việc liên quan đến việc đọc hoặc ghi dữ liệu. Tuy nhiên, tuỳ thuộc vào môi trường, có thể có có nhiều điều đang xảy ra bên trong:

  • Nếu tệp nhập nằm trên ổ cục bộ, ứng dụng cần thực hiện một loạt thao tác quyền truy cập vào bộ nhớ và ổ đĩa để tìm tệp, kiểm tra quyền, mở tệp để đọc rồi sau đó đọc từng khối cho đến khi truy xuất được số byte được yêu cầu. Quá trình này có thể khá chậm, tuỳ thuộc vào tốc độ của ổ đĩa và dung lượng được yêu cầu.
  • Hoặc tệp đầu vào có thể nằm trên một vị trí mạng đã gắn, trong trường hợp này, mạng ngăn xếp bây giờ cũng sẽ có liên quan, làm tăng độ phức tạp, độ trễ và số lượng số lần thử lại cho mỗi thao tác.
  • Cuối cùng, ngay cả printf cũng không đảm bảo in mọi thứ lên bảng điều khiển và có thể bị chuyển hướng đến một tệp hoặc vị trí mạng, trong trường hợp đó, tệp hoặc vị trí mạng sẽ phải thực hiện qua các bước tương tự như trên.

Nói tóm lại, I/O có thể diễn ra chậm và bạn không thể dự đoán thời lượng một cuộc gọi cụ thể sẽ do một xem nhanh mã. Trong khi thao tác đó đang chạy, toàn bộ ứng dụng của bạn sẽ có vẻ như bị treo và không phản hồi người dùng.

Điều này không chỉ giới hạn ở C hoặc C++. Hầu hết các ngôn ngữ hệ thống trình bày tất cả I/O dưới dạng đồng bộ. Ví dụ: nếu bạn dịch ví dụ sang Rust, API có thể trông đơn giản hơn, nhưng những nguyên tắc tương tự sẽ được áp dụng. Bạn chỉ cần thực hiện cuộc gọi và đợi cuộc gọi trả về kết quả một cách đồng bộ, trong khi thực hiện tất cả các thao tác tốn kém và cuối cùng trả về kết quả trong một lời gọi:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

Nhưng điều gì sẽ xảy ra khi bạn cố gắng biên dịch bất kỳ mẫu nào trong số đó thành WebAssembly và dịch chúng sang trên web? Hoặc, để cung cấp một ví dụ cụ thể nội dung nào có thể "đã đọc tệp" thao tác dịch sang? Điều đó sẽ cần đọc dữ liệu từ bộ nhớ nào đó.

Mô hình web không đồng bộ

Web có nhiều lựa chọn về dung lượng lưu trữ mà bạn có thể ánh xạ đến, chẳng hạn như dung lượng lưu trữ trong bộ nhớ (JS) đối tượng), localStorage, IndexedDB, bộ nhớ phía máy chủ, và API Truy cập hệ thống tệp mới.

Tuy nhiên, chỉ có hai trong số các API đó — bộ nhớ trong bộ nhớ và localStorage — có thể sử dụng được một cách đồng bộ và cả hai đều là những lựa chọn hạn chế nhất về nội dung bạn có thể lưu trữ và thời gian lưu trữ. Tất cả các tuỳ chọn khác chỉ cung cấp API không đồng bộ.

Đây là một trong những thuộc tính cốt lõi của việc thực thi mã trên web: bất kỳ thao tác nào tốn thời gian, bao gồm mọi I/O, phải không đồng bộ.

Lý do là trước đây, web là đơn luồng và bất kỳ mã người dùng nào chạm đến giao diện người dùng phải chạy trên cùng một luồng với giao diện người dùng. Công cụ này phải cạnh tranh với các nhiệm vụ quan trọng khác như xử lý bố cục, kết xuất hình ảnh và sự kiện trong thời gian của CPU. Bạn không muốn có một đoạn mã JavaScript hoặc WebAssembly để có thể bắt đầu "đọc tệp" thao tác và chặn mọi thứ khác—toàn bộ tab, hoặc trước đây là toàn bộ trình duyệt—trong khoảng từ mili giây đến vài giây cho đến khi kết thúc.

Thay vào đó, mã chỉ được phép lên lịch cho hoạt động I/O cùng với lệnh gọi lại để thực thi sau khi hoàn tất. Những lệnh gọi lại như vậy được thực thi trong vòng lặp sự kiện của trình duyệt. Tôi sẽ không đi vào chi tiết ở đây, nhưng nếu bạn muốn tìm hiểu sâu hơn về cách hoạt động của vòng lặp sự kiện, trả phòng Việc cần làm, việc nhỏ cần làm, hàng đợi và lịch biểu Giải thích chi tiết về chủ đề này.

Phiên bản ngắn gọn là trình duyệt chạy tất cả các đoạn mã theo một vòng lặp vô hạn, bằng cách đưa chúng ra khỏi hàng đợi từng bước một. Khi một sự kiện nào đó được kích hoạt, trình duyệt sẽ xếp trình xử lý tương ứng và trong vòng lặp tiếp theo, nó sẽ được đưa ra khỏi hàng đợi và được thực thi. Cơ chế này cho phép mô phỏng mô hình đồng thời và chạy nhiều hoạt động song song mà chỉ sử dụng một chuỗi duy nhất.

Điều quan trọng cần nhớ về cơ chế này là trong khi JavaScript tuỳ chỉnh của bạn (hoặc Mã WebAssembly) thực thi, vòng lặp sự kiện sẽ bị chặn và mặc dù vậy, sẽ không có cách nào để phản ứng với mọi trình xử lý bên ngoài, sự kiện, I/O, v.v. Cách duy nhất để nhận lại kết quả I/O là đăng ký lệnh gọi lại, hoàn tất việc thực thi mã và trao lại quyền kiểm soát cho trình duyệt để trình duyệt có thể tiếp tục đang xử lý mọi công việc đang chờ xử lý. Sau khi I/O hoàn tất, trình xử lý của bạn sẽ trở thành một trong những tác vụ đó và sẽ được thực thi.

Ví dụ: nếu bạn muốn viết lại các mẫu ở trên bằng JavaScript hiện đại và quyết định đọc một từ một URL từ xa, bạn sẽ sử dụng API Tìm nạp và cú pháp async-await:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

Mặc dù trông có vẻ đồng bộ, nhưng về cơ bản, mỗi await về cơ bản là đường cú pháp cho lệnh gọi lại:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

Trong ví dụ đơn giản này, rõ ràng hơn một chút, một yêu cầu sẽ được bắt đầu và các phản hồi được đăng ký bằng lệnh gọi lại đầu tiên. Khi trình duyệt nhận được phản hồi ban đầu (chỉ HTTP) tiêu đề—sẽ gọi không đồng bộ lệnh gọi lại này. Lệnh gọi lại bắt đầu đọc nội dung dưới dạng văn bản bằng cách sử dụng response.text() rồi đăng ký nhận kết quả bằng một lệnh gọi lại khác. Cuối cùng, khi fetch đã đã truy xuất tất cả nội dung, lệnh gọi lại này sẽ thực hiện lệnh gọi lại gần đây nhất, in thông báo "Hello, (username)!" (Xin chào, (tên người dùng))! vào Google Play.

Do tính chất không đồng bộ của các bước đó, nên hàm ban đầu có thể trả về quyền kiểm soát cho của trình duyệt ngay khi I/O được lên lịch và để toàn bộ giao diện người dùng thích ứng cũng như sẵn sàng cho các tác vụ khác, bao gồm kết xuất, cuộn, v.v. trong khi I/O đang thực thi trong nền.

Ví dụ cuối cùng, ngay cả những API đơn giản như "ngủ" sẽ khiến ứng dụng phải đợi một thời gian số giây, cũng là một dạng của thao tác I/O:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

Chắc chắn rồi, bạn có thể dịch theo cách rất đơn giản để chặn chuỗi thư hiện tại cho đến khi hết thời gian:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

Trên thực tế, đó chính xác là những gì Emscripten làm trong cách triển khai mặc định của "ngủ", nhưng cách này không hiệu quả, sẽ chặn toàn bộ giao diện người dùng và không cho phép xử lý bất kỳ sự kiện nào khác trong khi đó. Nói chung, đừng làm như vậy trong mã phát hành chính thức.

Thay vào đó, một phiên bản phù hợp hơn của "ngủ" trong JavaScript sẽ liên quan đến việc gọi setTimeout() và đăng ký với một trình xử lý:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

Đâu là điểm chung của tất cả các ví dụ và API này? Trong mỗi trường hợp, mã thành ngữ trong câu lệnh gốc ngôn ngữ hệ thống sử dụng API chặn cho I/O, trong khi một ví dụ tương đương cho web sử dụng thay vào đó là API không đồng bộ. Khi biên dịch lên web, bạn cần chuyển đổi giữa hai các mô hình thực thi và WebAssembly hiện chưa có khả năng tích hợp sẵn để làm việc này.

Thu hẹp khoảng cách bằng tính năng Không đồng bộ hoá

Đây là lúc Asyncify xuất hiện. Không đồng bộ là thời gian biên dịch được Emscripten hỗ trợ, cho phép tạm dừng toàn bộ chương trình và tiếp tục không đồng bộ sau.

Biểu đồ cuộc gọi
mô tả JavaScript -> WebAssembly -> API web -> lệnh gọi tác vụ không đồng bộ, trong đó Asyncify kết nối
kết quả của nhiệm vụ không đồng bộ trở lại WebAssembly

Cách sử dụng trong C / C++ với Emscripten

Trong ví dụ gần đây nhất, nếu bạn muốn sử dụng tính năng Không đồng bộ hoá để triển khai chế độ ngủ không đồng bộ, sẽ như sau:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});

puts("A");
async_sleep(1);
puts("B");

EM_JS là một cho phép xác định các đoạn mã JavaScript như thể chúng là hàm C. Bên trong, hãy dùng một hàm Asyncify.handleSleep() mã này yêu cầu Emscripten tạm ngưng chương trình và cung cấp một trình xử lý wakeUp() cần được được gọi sau khi hoạt động không đồng bộ kết thúc. Trong ví dụ trên, trình xử lý được chuyển đến setTimeout(), nhưng có thể dùng trong bất kỳ ngữ cảnh nào khác chấp nhận lệnh gọi lại. Cuối cùng, bạn có thể gọi async_sleep() ở bất cứ đâu bạn muốn giống như sleep() thông thường hoặc bất kỳ API đồng bộ nào khác.

Khi biên dịch mã như vậy, bạn cần yêu cầu Emscripten kích hoạt tính năng Asyncify. Thực hiện bằng cách truyền -s ASYNCIFY cũng như -s ASYNCIFY_IMPORTS=[func1, func2] với danh sách các hàm giống mảng có thể không đồng bộ.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

Điều này cho Emscripten biết rằng có thể cần lưu và khôi phục lệnh gọi đến các hàm đó trạng thái, vì vậy trình biên dịch sẽ chèn mã hỗ trợ xung quanh các lệnh gọi như vậy.

Bây giờ, khi thực thi mã này trong trình duyệt, bạn sẽ thấy một nhật ký đầu ra liền mạch như bạn mong đợi, với B xuất hiện sau một khoảng thời gian trễ ngắn sau A.

A
B

Bạn có thể trả về các giá trị từ Không đồng bộ hoá. Mục tiêu bạn cần làm là trả về kết quả của handleSleep() rồi truyền kết quả đó vào wakeUp() . Ví dụ: nếu bạn muốn tìm nạp một số từ điều khiển từ xa, thay vì đọc từ tệp bạn có thể sử dụng một đoạn mã như đoạn mã dưới đây để đưa ra yêu cầu, tạm ngưng mã C và tiếp tục sau khi nội dung phản hồi được truy xuất. Mọi thứ đều được thực hiện liền mạch như thể lệnh gọi đồng bộ.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

Trên thực tế, đối với các API dựa trên Promise như fetch(), bạn thậm chí có thể kết hợp Asyncify với JavaScript tính năng async-await thay vì sử dụng API dựa trên lệnh gọi lại. Để làm được điều đó, thay vì Asyncify.handleSleep(), gọi Asyncify.handleAsync(). Sau đó, thay vì phải lên lịch một wakeUp(), bạn có thể truyền hàm JavaScript async, đồng thời sử dụng awaitreturn giúp mã trông tự nhiên và đồng bộ hơn mà không làm mất đi bất kỳ lợi ích nào của I/O không đồng bộ.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

Đang chờ các giá trị phức tạp

Nhưng ví dụ này vẫn chỉ giới hạn bạn ở các con số. Nếu bạn muốn triển khai phiên bản gốc ví dụ: nơi tôi cố lấy tên của người dùng từ tệp dưới dạng một chuỗi? Bạn cũng có thể làm được!

Emscripten cung cấp một tính năng được gọi là Được liên kết để cho phép bạn để xử lý lượt chuyển đổi giữa các giá trị JavaScript và C++. Công cụ này cũng hỗ trợ tính năng Không đồng bộ hoá, vì vậy bạn có thể gọi await() trên các Promise bên ngoài và nó sẽ hoạt động giống như await trong chế độ chờ không đồng bộ Mã JavaScript:

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

Khi sử dụng phương thức này, bạn thậm chí không cần truyền ASYNCIFY_IMPORTS dưới dạng cờ biên dịch, vì đây là đã được đưa vào theo mặc định.

Được rồi, tất cả đều hoạt động tốt trong Emscripten. Còn các chuỗi công cụ và ngôn ngữ khác thì sao?

Thông tin sử dụng từ các ngôn ngữ khác

Giả sử bạn có một lệnh gọi đồng bộ tương tự ở đâu đó trong mã Rust mà bạn muốn ánh xạ đến một async API trên web. Hoá ra bạn cũng có thể làm được!

Trước tiên, bạn cần xác định một hàm như vậy là một hàm nhập thông thường qua khối extern (hoặc bạn đã chọn cú pháp của ngôn ngữ cho hàm nước ngoài).

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

Và biên dịch mã của bạn thành WebAssembly:

cargo build --target wasm32-unknown-unknown

Bây giờ, bạn cần đo lường tệp WebAssembly bằng mã để lưu trữ/khôi phục ngăn xếp. Cho C / C++, Emscripten sẽ thực hiện việc này cho chúng ta, nhưng nó không được sử dụng ở đây, vì vậy quy trình này thủ công hơn một chút.

Thật may là bản thân biến đổi Asyncify hoàn toàn không phụ thuộc vào chuỗi công cụ. Có thể chuyển đổi tuỳ ý WebAssembly, bất kể tệp đó được tạo bằng trình biên dịch nào. Phép biến đổi này được cung cấp riêng như một phần của trình tối ưu hoá wasm-opt từ Binaryen chuỗi công cụ và có thể được gọi như sau:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

Truyền --asyncify để bật phép biến đổi, sau đó sử dụng --pass-arg=… để cung cấp dấu phẩy được phân tách bằng dấu phẩy danh sách hàm không đồng bộ, trong đó trạng thái chương trình sẽ bị tạm ngưng và sau đó được tiếp tục.

Tất cả những gì còn lại là cung cấp mã thời gian chạy hỗ trợ sẽ thực sự làm điều đó, đó là tạm ngưng và tiếp tục Mã WebAssembly. Xin nhắc lại, trong trường hợp C / C++, thành phần này sẽ được Emscripten đưa vào, nhưng bây giờ bạn cần mã keo JavaScript tuỳ chỉnh sẽ xử lý các tệp WebAssembly tuỳ ý. Chúng tôi đã tạo một thư viện chỉ dành cho điều đó.

Bạn có thể tìm thấy công cụ này trên GitHub tại https://github.com/GoogleChromeLabs/asyncify or npm bên dưới tên asyncify-wasm.

Công cụ này mô phỏng quy trình tạo thực thể WebAssembly tiêu chuẩn API, nhưng trong không gian tên riêng. Chỉ khác biệt ở chỗ, trong API WebAssembly thông thường, bạn chỉ có thể cung cấp các hàm đồng bộ như dữ liệu nhập, mặc dù trong trình bao bọc Asyncify, bạn cũng có thể cung cấp các lệnh nhập không đồng bộ:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});

await instance.exports.main();

Sau khi bạn cố gắng gọi một hàm không đồng bộ như vậy, chẳng hạn như get_answer() trong ví dụ trên, từ phía WebAssembly, thư viện sẽ phát hiện Promise được trả về, tạm ngưng và lưu trạng thái của ứng dụng WebAssembly, đăng ký bản cam kết hoàn thành, sau đó đăng ký sau khi giải quyết xong, khôi phục liền mạch ngăn xếp lệnh gọi và trạng thái và tiếp tục thực thi như thể không có gì xảy ra.

Do bất kỳ hàm nào trong mô-đun đều có thể thực hiện lệnh gọi không đồng bộ, nên tất cả tệp xuất có thể trở thành không đồng bộ, vì vậy chúng cũng được gói. Có thể bạn đã nhận thấy trong ví dụ ở trên rằng cần phải await kết quả của instance.exports.main() để biết thời điểm thực thi thực sự đã hoàn tất.

Tính năng này hoạt động như thế nào?

Khi phát hiện lệnh gọi đến một trong các hàm ASYNCIFY_IMPORTS, tính năng Asyncify sẽ bắt đầu một lệnh gọi không đồng bộ lưu toàn bộ trạng thái của ứng dụng, bao gồm ngăn xếp lệnh gọi và mọi thao tác tạm thời cục bộ, sau đó khi thao tác này kết thúc sẽ khôi phục toàn bộ bộ nhớ, ngăn xếp lệnh gọi và tiếp tục từ cùng một nơi và với cùng trạng thái như thể chương trình chưa bao giờ dừng lại.

Điều này khá giống với tính năng async-await trong JavaScript mà tôi đã trình bày trước đó, nhưng không giống như JavaScript một, không yêu cầu bất kỳ cú pháp đặc biệt hoặc hỗ trợ thời gian chạy nào từ ngôn ngữ và thay vào đó hoạt động bằng cách biến đổi các hàm đồng bộ đơn giản vào thời gian biên dịch.

Khi biên dịch ví dụ về giấc ngủ không đồng bộ được hiển thị trước đó:

puts("A");
async_sleep(1);
puts("B");

Asyncify sẽ lấy mã này và biến đổi mã thành mã gần giống như sau (mã giả, mã thực thì biến đổi còn phức tạp hơn):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

Ban đầu, mode được đặt thành NORMAL_EXECUTION. Tương ứng, lần đầu tiên mã biến đổi như vậy sẽ được thực thi thì chỉ phần dẫn đến async_sleep() sẽ được đánh giá. Ngay khi hoạt động không đồng bộ được lên lịch, Asyncify sẽ lưu tất cả các cục bộ và gỡ bỏ ngăn xếp bằng cách quay lại từ mỗi hàm lên đến đầu trang, bằng cách này sẽ trao lại quyền kiểm soát cho trình duyệt vòng lặp sự kiện.

Sau đó, khi async_sleep() phân giải, mã hỗ trợ Không đồng bộ hoá sẽ thay đổi mode thành REWINDING và gọi lại hàm này. Lần này, lệnh "thực thi bình thường" nhánh đã được bỏ qua - vì nó đã làm vậy lệnh lần cuối và tôi muốn tránh in "A" và thay vào đó, báo cáo này chuyển thẳng đến "tua lại" nhánh. Sau khi đạt đến, chế độ này sẽ khôi phục tất cả dữ liệu cục bộ được lưu trữ, thay đổi chế độ về "bình thường" và tiếp tục quá trình thực thi như thể mã chưa từng bị dừng ngay từ đầu.

Chi phí chuyển đổi

Thật không may, biến đổi Asyncify không phải là hoàn toàn miễn phí vì phải chèn khá nhiều mã hỗ trợ để lưu trữ và khôi phục tất cả các cục bộ đó, điều hướng ngăn xếp lệnh gọi trong chế độ khác nhau, v.v. Hàm này chỉ cố sửa đổi các hàm được đánh dấu là không đồng bộ trong lệnh , cũng như bất kỳ phương thức gọi tiềm năng nào, nhưng chi phí kích thước mã vẫn có thể tăng thêm khoảng 50% trước khi nén.

Một biểu đồ cho thấy mã
mức hao tổn kích thước lớn cho các điểm chuẩn khác nhau, từ gần 0% trong điều kiện tinh chỉnh đến trên 100% trong điều kiện kém nhất
ốp lưng

Điều này không lý tưởng, nhưng trong nhiều trường hợp có thể chấp nhận được khi giải pháp thay thế không có chức năng hoặc phải viết lại đáng kể mã gốc.

Hãy nhớ luôn bật tính năng tối ưu hoá cho bản dựng cuối cùng để tránh làm tăng hiệu suất. Bạn có thể hãy chọn cả mục Tối ưu hoá cụ thể không đồng bộ hoá các lựa chọn nhằm giảm chi phí vận hành bằng cách chỉ giới hạn các phép biến đổi đối với các hàm được chỉ định và/hoặc chỉ các lệnh gọi hàm trực tiếp. Ngoài ra còn có chi phí nhỏ cho hiệu suất thời gian chạy, nhưng chỉ giới hạn ở chính các lệnh gọi không đồng bộ. Tuy nhiên, so với so với chi phí thực tế của công việc, thường không đáng kể.

Bản minh hoạ thực tế

Giờ bạn đã xem qua các ví dụ đơn giản, tôi sẽ chuyển sang những tình huống phức tạp hơn.

Như đã đề cập ở đầu bài viết, một trong những cách lưu trữ trên web là API Truy cập hệ thống tệp không đồng bộ. Công cụ này cấp quyền truy cập vào hệ thống tệp lưu trữ thực từ ứng dụng web.

Mặt khác, có một tiêu chuẩn phi thực tế tên là WASI cho WebAssembly I/O trong bảng điều khiển và phía máy chủ. Công cụ này được thiết kế làm mục tiêu tổng hợp cho ngôn ngữ hệ thống và hiển thị tất cả các loại hệ thống tệp và các thao tác khác theo cách truyền thống hình thức đồng bộ.

Điều gì sẽ xảy ra nếu bạn có thể ánh xạ một kết nối với một thành phần khác? Sau đó, bạn có thể biên dịch bất kỳ ứng dụng nào bằng ngôn ngữ nguồn bất kỳ với bất kỳ chuỗi công cụ nào hỗ trợ mục tiêu WASI và chạy công cụ trong một hộp cát trên web trong khi vẫn cho phép ứng dụng hoạt động trên các tệp của người dùng thực! Với Không đồng bộ, bạn có thể làm việc đó.

Trong bản minh hoạ này, tôi đã biên dịch crate coreutils của Rust với một một vài bản vá nhỏ cho WASI, được truyền thông qua biến đổi Asyncify và triển khai không đồng bộ liên kết từ WASI vào API Truy cập hệ thống tệp ở phía JavaScript. Sau khi kết hợp với Thành phần đầu cuối Xterm.js, cung cấp một shell thực tế chạy trong thẻ trình duyệt và hoạt động trên các tệp của người dùng thực - giống như một thiết bị đầu cuối thực tế.

Hãy xem trực tiếp tại https://wasi.rreverser.com/.

Các trường hợp sử dụng không đồng bộ hoá không chỉ giới hạn ở bộ tính giờ và hệ thống tệp. Bạn có thể tìm hiểu thêm và sử dụng thêm các API thích hợp trên web.

Ví dụ, với sự trợ giúp của Không đồng bộ hoá, bạn có thể lập bản đồ libusb – có thể là thư viện gốc phổ biến nhất để làm việc với Thiết bị USB – thành một API WebUSB để cấp quyền truy cập không đồng bộ vào các thiết bị đó trên web. Sau khi được ánh xạ và biên dịch, tôi đã nhận được các thử nghiệm libusb tiêu chuẩn và các ví dụ để chạy dựa trên các thử nghiệm đã chọn thiết bị ngay trong hộp cát của trang web.

Ảnh chụp màn hình libusb
đầu ra gỡ lỗi trên một trang web, cho thấy thông tin về máy ảnh Canon đã kết nối

Tuy nhiên, đây có thể là một câu chuyện cho một bài đăng khác trên blog.

Những ví dụ đó chứng minh hiệu quả của tính năng Asyncify trong việc thu hẹp khoảng cách và chuyển đổi tất cả nhiều loại ứng dụng lên web, giúp bạn có quyền truy cập đa nền tảng, hộp cát, v.v. bảo mật, tất cả mà không bị mất chức năng.