HTML5 web worker: data transferring
Người viết: Trần Ngọc Anh Trong bài viết này, chúng ta sẽ tìm hiểu sâu hơn về cơ ché truyền và nhận dữ liệu giữa thread chính của trang web và worker. Dữ liệu được truyền và nhận giữa thread chính và worker theo phương pháp clone, chứ không truyền cùng một object. Object được ...
Người viết: Trần Ngọc Anh
Trong bài viết này, chúng ta sẽ tìm hiểu sâu hơn về cơ ché truyền và nhận dữ liệu giữa thread chính của trang web và worker.
Dữ liệu được truyền và nhận giữa thread chính và worker theo phương pháp clone, chứ không truyền cùng một object. Object được serialize sau đó được truyền cho worker, rồi lại deserialize lúc nhận được. Thread chính và worker không bao giờ sử dụng chung một instance, dữ liệu nhận được ở một bên luôn là bản sao của dữ liệu từ bên còn lại.
Phần lớn các trình duyệt đều cài đặt tính năng này bằng thuật toán structured cloning.
Để minh hoạ cho quá trình này, chúng ta hãy xem xét một ví dụ như sau:
1 2 3 4 5 |
const emulateMsg = (val) => { return eval(`(${JSON.stringify(val)})`); } |
Hàm emulateMsg mô phỏng hành vi của các tham số được truyền bằng cách clone chứ không phải giữ nguyên object cũ. Chúng ta có thể test thử xem sao:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
val1 = new Number(10) typeof val1 // "object" typeof emulateMsg(val1) // "number" val2 = true typeof val2 // "boolean" typeof emulateMsg(val2) // "boolean" val3 = new String('foo') typeof val3 // "object" typeof emulateMsg(val3) // "string" val4 = {foo: 'foo', bar: 'bar'} typeof val4 // "object" typeof emulateMsg(val4) // "object" |
Như chúng ta đã biết, việc truyền và nhận dữ liệu từ thread chính và worker được thực hiện bằng cách truyền tham số vào hàm postMessage và giá trị của thuộc tính data trong message event. Với cách truyền tham số bằng clone như trên, việc gửi và nhận dữ liệu của thread chính và worker cũng hoàn toàn là copy.
Đó cũng là lý do khiến cho dữ liệu truyền và nhận giữa worker được gọi là “message”. Thuật toán structured cloning có thể nhận dữ liệu dưới dạng JSON và cả những dữ liệu phức tạp khác nữa.
Nếu cần phải truyền dữ liệu phức tạp, mà dữ liệu đó được gọi ở nhiều nơi cả thread chính lẫn worker thì chúng ta có thể xây dựng một hệ thống gộp chung tất cả.
Trước hết, chúng ta tạo một class QueryableWorker, dùng để track các handler và giúp chúng ta giao tiếp với worker.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class QueryableWorker { constructor(url, defaultHandler, errorHandler) { this.worker = new Worker(url); this.handlers = {}; this.defaultHandler = defaultHandler || function(){}; if (errorHandler) { this.worker.onerror = errorHandler; } this.worker.onmessage = (event) => { if (event.data instanceof Object && event.data.hasOwnProperty('method') && event.data.hasOwnProperty('arguments')) { this.handlers[event.data.method].apply(this, event.data.arguments); } else { this.defaultHandler.call(this, event.data); } } } postMessage(message) { this.worker.postMessage(message); } terminate() { this.worker.terminate(); } addHandler(name, handler) { this.handlers[name] = handler; } removeHandler(name) { delete this.handlers[name]; } sendQuery(...args) { if (args.length < 1 ) { throw new TypeError('at least 1 argument'); } this.worker.postMessage({ method: args[0], arguments: Array.prototype.slice.call(args, 1) }) } } |
Trong class trên, chúng ta sử dụng hai phương thức để thêm và bớt các handler.
Các handler sẽ được thêm vào tuỳ ý sau khi tạo ra object thuộc class trên, cho phép nó có thể linh hoạt trong việc xử lý các phản hồi từ worker (xử lý thế nào hoàn toàn phụ thuộc vào handler được thêm vào).
Còn phương thức sendQuery đùng dể gửi dữ liệu đến worker (bao gồm tên phương thức và tham số), yêu cầu worker thực hiện thao tác tương ứng.
Với worker, để minh hoạ, chúng ta sẽ xây dựng worker thực hiện hai thao tác đơn giản: lấy một số ngẫu nhiên, tạm dừng 1 giây.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const reply = (...args) => { if (args.length < 1 ) { throw new TypeError('at least 1 argument'); } postMessage({ method: args[0], arguments: Array.prototype.slice.call(args, 1) }) } const queryableFunctions = { getRandom: () => { reply('printStuff', Math.random()); }, waitSomeTime: () => { setTimeout(() => {reply('doAlert', 1, 'seconds')}, 1000); } } |
Phương thức onmessage của worker rất đơn giản: gọi và thực thi các phương thức cùng tham số nhận được. Code chỗ này tương tự như onmessage của thread chính, chỉ khác đó là nó sẽ gọi các hàm được định nghĩa sẵn chứ không phải handler được thêm vào sau.
c
1 2 3 4 5 6 7 8 9 10 11 12 |
onmessage = (event) => { if (event.data instanceof Object && event.data.hasOwnProperty('method') && event.data.hasOwnProperty('arguments')) { queryableFunctions[event.data.method].apply(self, event.data.arguments) } else { // do something } } |
Demo cùng toàn bộ code của ví dụ này, mời các bạn xem ở đây.
Structured cloning rất tuyệt vời, nhưng hoạt động của nó cũng là hành động copy dữ liệu mà thôi. Trong một số trường hợp cần truyền dữ liệu với kích thước lớn, structured cloning lại khiến tốc độ bị chậm đi. Ví dụ, nếu chúng ta cần truyền một ArrayBuffer khoảng vài chục MB chẳng hạn, sẽ phải mất kha khá thời gian (khoảng nửa giây).
Nửa giây nghe thì ít nhưng với các ứng dụng hiện nay, như vậy đã là một sự trễ giờ quá lớn. Rất may, các trình duyệt hiện đại đã có một cơ chế trong trường hợp này, đó là Transferable object.
Các object thuộc loại Tranferable sẽ được truyền mà không hề cần tới thao tác clone. Điều đó sẽ khiến cho tốc độ tăng lên đáng kể. Tuy nhiên, việc sử dụng cách truyền dữ liệu này cũng có những tác dụng phụ. Không giống với cách thức truyền dữ liệu thông thường, dữ liệu truyền bằng cách này sẽ không còn truy cập được ở nơi đã truyền nó đi nữa.
Ví dụ, nếu chúng ta truyền một ArrayBuffer từ thread chính vào worker thì dữ liệu của ArrayBuffer đó ở thread chính sẽ bị xoá và không thể truy cập được nữa. Có thể nói, toàn bộ dữ liệu đã được di chuyển hoàn toàn vào worker và không còn tồn tại ở chỗ cũ nữa.
Để truyền dữ liệu vào worker dạng Transferable, chúng ta cần truyền hai tham số cho hàm postMessage: tham số đầu tiền là message, tham số thứ hai là danh sách các item cần transfer. Trong trường hợp này, chúng ta có thể truyền như sau:
1 2 3 4 5 6 7 |
const uInt8Array = new Uint8Array(1024 * 1028 * 32); // 32 MB for (let i = 0; i < uInt8Array.length; i++) { uInt8Array[i] = i; } worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]); |
Dưới đây là cấu hình máy tính dùng để so sánh tốc độ giữa cách truyền dữ liệu thông thường (structured cloning) và cách dùng Transferable.
Với máy tính như vậy, tôi sẽ thực hiện so sánh với các trình duyệt Safari, Chrome và Firefox. Chúng ta sẽ so sánh tốc độ truyền dữ liệu với kích thước lớn (500MB) bằng hai phương pháp khác nhau.
Ở đây, chúng ta chỉ so sánh tốc độ truyền tải dữ liệu giữa thread chính và worker, không so sánh tốc độ xử lý khi nhận dữ liệu. Dưới đây là code dùng để so sánh:
Truyền dử dụng bằng structured cloning
1 2 3 4 5 6 7 8 |
const myWorker = new Worker("worker.js"); const data = new Uint8Array(500 * 1024 * 1024); const startTime = new Date().getTime(); myWorker.postMessage(data); const timeTaken = new Date().getTime() - startTime; console.log(`Tranfer completed in ${timeTaken}ms.`); |
Transferable
1 2 3 4 5 6 7 8 |
const myWorker = new Worker("worker.js"); const data = new Uint8Array(500 * 1024 * 1024); const startTime = new Date().getTime(); myWorker.postMessage(data, [data.buffer]); const timeTaken = new Date().getTime() - startTime; console.log(`Tranfer completed in ${timeTaken}ms.`); |
Dưới đây là bảng kết quả đo được khi sử dụng truyền dữ liệu bằng hai phương thức này:
Browser | Structured Cloning | Transferable |
---|---|---|
Safari | 1168ms | 1ms |
Chrome | 604ms | 10ms |
Firefox | 690ms | 1ms |
Nhìn vào đây chúng ta cũng thấy rằng, sử dụng Transferable để truyền dữ liệu giữa worker và thread chính có tốc độ cao hơn hẳn (nhanh hơn hàng trăm lần, thậm chí với Safari là hơn nghìn lần). Đây có thể là một lựa chọn tốt để truyền những dữ liệu lớn nhưng không có nhu cầu tái sử dụng ở nguồn.
Trong phần trước, chúng ta đã so sánh tốc độ truyền dữ liệu giữa cách thông thường và dùng Transferable. Và chúng ta đã thấy rằng, tốc độ khi sử dụng Transferable đã từng lên đáng kể. Thế nhưng, đó là trường hợp dữ liệu đơn giản, dữ liệu của message và của buffer tương ứng với nhau.
Tuy nhiên, trong các trường hợp phức tạp hơn, mọi chuyện không còn như vậy nữa. Dưới đây là một đoạn code dùng để test tốc độ truyền tải dữ liệu giữa thread chính và worker bằng hai phương pháp đó. Điểm khác biệt ở đây là dữ liệu mà chúng ta truyền tải không phải dữ liệu đơn giản (chỉ là một mảng nữa) mà phức tạp hơn nhiều.
Chúng ta sẽ truyền tải message là một mảng hai chiều, và một mảng buffer cũng là mảng hai chiều của các buffer. Trong thực tế, chúng ta có thể gặp các trường hợp dữ liệu phức tạp hơn nữa.