Isolates & compute
จังหวะสะดุดที่ await เอาไม่อยู่
หัวข้อที่มีชื่อว่า “จังหวะสะดุดที่ await เอาไม่อยู่”async/await แก้ปัญหาการ รอ (network, disk) — แต่ไม่ช่วยกับงานหนัก CPU การ parse JSON ก้อนใหญ่, resize รูป หรือรัน crypto hash คือ synchronous computation และมันรันบน UI isolate ระหว่างที่มันรัน event loop จะ render frame ไม่ได้ แอปจึงค้าง
คุณ await หนีปัญหานี้ไม่ได้ เพราะไม่มีอะไรให้รอ — CPU กำลังยุ่งอยู่ วิธีแก้คือย้ายงานไป isolate อื่น เพื่อให้ UI isolate ว่างพอที่จะ render ต่อ
compute(): กรณีที่พบบ่อยที่สุด
หัวข้อที่มีชื่อว่า “compute(): กรณีที่พบบ่อยที่สุด”สำหรับ “รันฟังก์ชันหนักนี้นอก main isolate” แบบที่เจอบ่อย ๆ Flutter ให้ compute() มา ส่งฟังก์ชันกับ argument เข้าไป มันจะรันฟังก์ชันบน background isolate แล้วคืนผลลัพธ์เป็น Future
import 'dart:isolate';import 'package:flutter/foundation.dart';
// A top-level function — self-contained, taking plain data in and// returning plain data out. This is what compute() actually needs.int addOne(int x) => x + 1;
// A version whose closure captures a ReceivePort — a native, isolate-bound// handle. This is what breaks (proven below): the closure itself has to// cross the isolate boundary as compute()'s message, and a ReceivePort// cannot be copied across.Future<int> addOneCapturingPort(ReceivePort port, int x) { return compute<int, int>((int value) { // ignore: unused_local_variable final capturedPort = port; // <-- captures a non-sendable object return value + 1; }, x);}import 'dart:isolate';import 'package:flutter/foundation.dart';import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/async-and-performance__isolates-and-compute/isolate_capture_demo.dart';
void main() { test('compute() with a top-level function and plain data works fine', () async { final result = await compute(addOne, 41); expect(result, 42); });
test('compute() with a closure that captures a ReceivePort throws the real isolate-message error', () async { final port = ReceivePort(); addTearDown(port.close);
await expectLater( addOneCapturingPort(port, 1), throwsA( isA<ArgumentError>().having( (e) => e.toString(), 'message', contains('Illegal argument in isolate message'), ), ), ); });}00:00 +0: compute() with a top-level function and plain data works fine00:00 +1: compute() with a closure that captures a ReceivePort throws the real isolate-message error
00:00 +2: All tests passed!ข้อความ exception ตัวจริงที่ไม่แก้ไขอะไรเลย (จาก Flutter SDK 3.47.5 ตัวเดียวกันนี้เป๊ะ ๆ พิมพ์ผ่าน e.toString() ใน scratch experiment เบื้องหลังบทนี้):
ArgumentError: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:isolate' Class: _ReceivePortImpl@1026248 (see restrictions listed at `SendPort.send()` documentation for more information) <- field port in main.<anonymous closure>.<anonymous closure> (from ...) <- field callback in compute.<anonymous closure> (from package:flutter/src/foundation/_isolates_io.dart) <- computation in Instance of '_RemoteRunner<int>' (from dart:isolate)อ่าน trace จากล่างขึ้นบน: message ที่ข้าม isolate boundary คือ closure computation ทั้งก้อนที่ compute() สร้างครอบ callback ของคุณ; เดินเข้าไปข้างในเจอ field callback (closure ของคุณ) แล้วข้างในนั้นเจอ field port ที่ถูก capture ไว้ — เป็น ReceivePort ซึ่ง error ระบุตรง ๆ ว่า unsendable วิธีแก้คือตรงตามตัวอย่าง top-level addOne: ทำฟังก์ชันให้ self-contained — รับข้อมูลธรรมดาเข้าไป คืนข้อมูลธรรมดาออกมา และอย่า capture native handle (ReceivePort, Socket, pointer จาก dart:ffi, BuildContext/Element) หรือในทางปฏิบัติคืออะไรก็ตามที่ไม่ใช่ข้อมูล Dart ธรรมดา
compute() คือ Isolate.run บน native — พิสูจน์จาก source ของ SDK ที่ติดตั้งจริง
หัวข้อที่มีชื่อว่า “compute() คือ Isolate.run บน native — พิสูจน์จาก source ของ SDK ที่ติดตั้งจริง”ข้ออ้างที่พบบ่อยคือ compute() ห่อ Isolate.run ไว้พร้อม “isolate-pool/lifecycle management เพิ่มเติม” การอ่าน source ของ Flutter SDK 3.47.5 ที่ติดตั้งจริงในคอร์สนี้พิสูจน์ว่าไม่จริง — implementation ของ native (dart:io) ทั้งหมดคือ:
Future<R> compute<M, R>( isolates.ComputeCallback<M, R> callback, M message, { String? debugLabel,}) async { debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
return Isolate.run<R>(() { return callback(message); }, debugName: debugLabel);}แค่นี้เอง — ไม่มี pool ไม่มีการใช้ isolate ซ้ำ ไม่มี lifecycle manager compute(fn, arg) บน Android/iOS/desktop คือ Isolate.run(() => fn(arg)) ตัวจริง บวกกับชื่อ debug สำหรับ DevTools ทุก call spawn isolate ใหม่แล้วปิดมันทิ้งเมื่อเสร็จ — cost profile เดียวกันทุกอย่าง ถ้าคุณเขียน plain Dart (ไม่มี Flutter dependency) และต้องการพฤติกรรมเดียวกันเป๊ะ Isolate.run (ครอบคลุมพร้อมการพิสูจน์ของมันเองในบทเรียน Async in Dart) คือตัว primitive; compute() เพิ่มคุณค่าในแอป Flutter ด้วยเหตุผลที่ครอบคลุมถัดไปเท่านั้น
compute() บน web: ไม่มี isolate เลย — พิสูจน์จาก source และ docs
หัวข้อที่มีชื่อว่า “compute() บน web: ไม่มี isolate เลย — พิสูจน์จาก source และ docs”implementation ของ web (dart2js/dartdevc) เป็นฟังก์ชันคนละตัวไปเลย:
Future<R> compute<M, R>( isolates.ComputeCallback<M, R> callback, M message, { String? debugLabel,}) async { // To avoid blocking the UI immediately for an expensive function call, we // pump a single frame to allow the framework to complete the current set // of work. await null; return callback(message);}ไม่มี isolate ไม่มี message passing — มัน await microtask เดียว (เพื่อให้ frame ปัจจุบันเสร็จก่อน) แล้วเรียกฟังก์ชันของคุณตรง ๆ บน thread เดิม docs ของ compute() จาก api.flutter.dev เอง ยืนยันเรื่องนี้ตรง ๆ: “On web platforms this will run callback on the current eventloop.” นี่คือเหตุผลที่ error จาก closure capture ด้านบนเกิดบน web ไม่ได้ เพราะไม่มี isolate boundary ให้ข้าม และก็เป็นเหตุผลที่งาน synchronous หนัก ๆ ที่ห่อด้วย compute() ยังคง block thread เดียวของ browser บน web; compute() บน web ซื้อเวลาให้คุณแค่หนึ่ง microtask ของการเลื่อนออกไป ไม่ใช่ parallelism จริง
ความไม่สมมาตรนี้เกิดขึ้นเพราะ Isolate.spawn ไม่รองรับบน web compiler จริง ๆ patch ของ dart2js runtime ใน SDK ที่ติดตั้งจริงระบุไว้ตรง ๆ:
@patchstatic Future<Isolate> spawn<T>( void entryPoint(T message), T message, { bool paused = false, bool errorsAreFatal = true, SendPort? onExit, SendPort? onError, String? debugName,}) { throw UnsupportedError("Isolate.spawn");}Isolate.run/Isolate.spawn (และโดยส่วนขยาย ทุกอย่างในบทนี้ที่สร้างบน Isolate.spawn) เป็นเครื่องมือ native-only เท่านั้น compute() คือ API เดียวในบทนี้ที่ปลอดภัยจริง ๆ ที่จะเรียกจากโค้ดที่แชร์ข้าม platform — มันแค่เงียบ ๆ เลิกเป็น “background” work ทันทีที่รันบน web
Long-lived worker isolate: Isolate.spawn กับ ports
หัวข้อที่มีชื่อว่า “Long-lived worker isolate: Isolate.spawn กับ ports”compute()/Isolate.run เหมาะกับการเรียกครั้งเดียว — isolate ใหม่ทุกครั้ง สำหรับ worker ที่คุณคุยด้วยซ้ำ ๆ (parser ที่อยู่ยาว, database connection ที่อยู่นอก thread) ให้ spawn ครั้งเดียวแล้วสื่อสารผ่าน SendPort/ReceivePort ตลอดอายุของ isolate นั้น
flowchart LR main["UI isolate (spawns once)"] -->|SendPort| worker["Worker isolate (lives on, replies to many requests)"] worker -->|SendPort| main note["compute()/Isolate.run instead: a NEW isolate per call"] -.- worker
import 'dart:isolate';
/// Runs INSIDE the worker isolate. Receives a SendPort to reply on, then/// listens forever for (int) requests and replies with the doubled value —/// a long-lived worker, not a fresh isolate per call.void workerEntry(SendPort mainSendPort) { final commandPort = ReceivePort(); mainSendPort.send(commandPort.sendPort); commandPort.listen((message) { final request = message as int; mainSendPort.send(request * 2); });}
/// Spawn ONCE, then send many requests over time — unlike compute()//// Isolate.run, which spawn a fresh isolate per call (simplified: one/// request in flight at a time, no request-id correlation).class DoublerWorker { DoublerWorker._(this._isolate, this._commandPort, this._responses);
final Isolate _isolate; final SendPort _commandPort; final Stream<dynamic> _responses;
static Future<DoublerWorker> spawn() async { final initPort = ReceivePort(); final isolate = await Isolate.spawn(workerEntry, initPort.sendPort); final broadcast = initPort.asBroadcastStream(); final commandPort = await broadcast.first as SendPort; return DoublerWorker._(isolate, commandPort, broadcast); }
Future<int> doubleValue(int value) async { final future = _responses.first; _commandPort.send(value); return await future as int; }
void dispose() => _isolate.kill(priority: Isolate.immediate);}import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/async-and-performance__isolates-and-compute/worker_isolate.dart';
void main() { test('a long-lived worker isolate stays alive across multiple requests', () async { final worker = await DoublerWorker.spawn(); addTearDown(worker.dispose);
final first = await worker.doubleValue(3); final second = await worker.doubleValue(10); final third = await worker.doubleValue(21);
expect([first, second, third], [6, 20, 42]); });}00:00 +0: a long-lived worker isolate stays alive across multiple requests
00:00 +1: All tests passed!Isolate.spawn เรียกครั้งเดียว สามรอบไป-กลับ ใช้ worker isolate เดิมตลอด — ReceivePort ที่คืนมาตอน setup (initPort) คือจุดที่ SendPort ของ worker มาถึง และทุก message ถัดไปใช้ channel เดิมซ้ำ
TransferableTypedData: ส่งต่อ byte โดยไม่ copy
หัวข้อที่มีชื่อว่า “TransferableTypedData: ส่งต่อ byte โดยไม่ copy”message ของ isolate ปกติจะถูก copy สำหรับ raw byte (ไฟล์ที่ decode แล้ว, image buffer) คำอธิบายของ TransferableTypedData จาก api.dart.dev เอง ระบุความต่างไว้ตรง ๆ: “The TransferableTypedData can be moved between isolates, so sending it through a send port will only take constant time.” ความเป็นเจ้าของถูกโอนย้ายแทนที่จะ copy byte ซ้ำ — สำเนาของ isolate ผู้ส่งใช้ไม่ได้อีกต่อไป (materialize() ฝั่งผู้ส่งจะ throw หลังโอนย้าย) เพราะมีแค่ isolate เดียวเท่านั้นที่ถือ buffer จริงได้ในแต่ละครั้ง
import 'dart:isolate';import 'dart:typed_data';
int sumBytes(Uint8List data) { var total = 0; for (final b in data) { total += b; } return total;}
int sumTransferable(TransferableTypedData data) { final bytes = data.materialize().asUint8List(); var total = 0; for (final b in bytes) { total += b; } return total;}
Uint8List makeTenMegabytes() { final data = Uint8List(10 * 1024 * 1024); for (var i = 0; i < data.length; i++) { data[i] = i % 256; } return data;}import 'dart:isolate';import 'package:flutter/foundation.dart';import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/async-and-performance__isolates-and-compute/isolate_bench.dart';
void main() { test('compute() copies a 10MB Uint8List across the isolate boundary — timed', () async { final data = makeTenMegabytes(); final sw = Stopwatch()..start(); final result = await compute(sumBytes, data); sw.stop(); expect(result, 1336934400); // deterministic checksum for this exact byte pattern // A generous sanity bound only — the real number is reported in prose, // measured on this Mac, not strictly asserted here (machine-dependent). expect(sw.elapsedMilliseconds, lessThan(2000)); });
test('TransferableTypedData hands off a 10MB buffer without copying its bytes', () async { final data = makeTenMegabytes(); final transferable = TransferableTypedData.fromList([data]); final sw = Stopwatch()..start(); final result = await Isolate.run(() => sumTransferable(transferable)); sw.stop(); expect(result, 1336934400); expect(sw.elapsedMilliseconds, lessThan(2000)); });}00:00 +0: compute() copies a 10MB Uint8List across the isolate boundary — timed00:00 +1: TransferableTypedData hands off a 10MB buffer without copying its bytes
00:00 +2: All tests passed!วัดจริงบน Mac เครื่องนี้ (นอกเหนือจาก assertion ของ test ที่กำหนดเพดานหลวม ๆ ที่ 2 วินาทีเพื่อให้เชื่อถือได้ข้ามเครื่อง): compute() ที่ copy 10MB ใช้เวลา ~14ms; Isolate.run + TransferableTypedData สำหรับ 10MB เดียวกันใช้เวลา ~12ms ที่ขนาดนี้ความต่างมีน้อยเพราะต้นทุน isolate-spawn คงที่ครอบงำทั้งสองแบบ — ข้อได้เปรียบจริงของ TransferableTypedData จะเห็นชัดที่ payload ใหญ่กว่านี้มาก (หลายร้อย MB ที่ต้นทุนการ copy จริงจะขยายตามขนาด แต่ต้นทุนการโอนย้ายคงที่) หรือตอนที่คุณส่ง buffer เดิมผ่านหลาย isolate ต่อกัน
เมื่อ UI thread ถูก block จริง ๆ: วัดค่า JSON decode
หัวข้อที่มีชื่อว่า “เมื่อ UI thread ถูก block จริง ๆ: วัดค่า JSON decode”import 'dart:convert';
String buildLargeJsonArray(int itemCount) { final buffer = StringBuffer('['); for (var i = 0; i < itemCount; i++) { if (i > 0) buffer.write(','); buffer.write('{"id":$i,"name":"item $i","value":${i * 1.5}}'); } buffer.write(']'); return buffer.toString();}
List<dynamic> decodeJsonArray(String source) => jsonDecode(source) as List<dynamic>;import 'package:flutter/foundation.dart';import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/async-and-performance__isolates-and-compute/json_decode_bench.dart';
void main() { test('synchronous jsonDecode of a ~10MB payload — measured on this Mac', () { final jsonStr = buildLargeJsonArray(200000); final sw = Stopwatch()..start(); final decoded = decodeJsonArray(jsonStr); sw.stop(); expect(decoded.length, 200000); });
test('the same jsonDecode moved to compute() — measured on this Mac', () async { final jsonStr = buildLargeJsonArray(200000); final sw = Stopwatch()..start(); final decoded = await compute(decodeJsonArray, jsonStr); sw.stop(); expect(decoded.length, 200000); });}00:00 +0: synchronous jsonDecode of a ~10MB payload — measured on this Mac00:00 +1: the same jsonDecode moved to compute() — measured on this Mac
00:00 +2: All tests passed!วัดจริงบน Mac เครื่องนี้ (JSON array 200,000 element ~9.6MB): decode แบบ synchronous บน thread ที่เรียกใช้เวลา ~92ms; decode เดียวกันผ่าน compute() ใช้เวลา ~86ms ทั้งหมด (isolate spawn + copy message + การ decode เอง) — โดยรวมไม่ได้ช้ากว่าเลยจริง ๆ และใกล้เคียงกันที่ขนาดนี้เพราะต้นทุนของ jsonDecode เองครอบงำทั้งสองแบบ แต่ตัวเลขที่สำคัญจริง ๆ ไม่ใช่เวลารวม — มันคือ thread ไหนเป็นคนจ่าย: เวอร์ชัน synchronous ใช้เวลา ~92ms นั้นบน UI isolate ซึ่งแย่งเวลากับการ render frame ตรง ๆ (เสี่ยง frame drop ทันทีที่เกิน ~16ms ต่อ frame ที่ 60fps); เวอร์ชัน compute() ใช้เวลาของมันบน isolate อื่นไปเลย UI isolate จึงว่างพอที่จะ render ต่อระหว่างนั้น นี่คือเกณฑ์จริงสำหรับ “คุ้มไหมที่จะใช้ isolate”: ไม่ใช่ “งานนี้ช้าไหม” แต่คือ “งานนี้ช้า และรันแบบ synchronous บน UI isolate ไหม”
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”โค้ด “ย้ายอันนี้ไป background isolate” ที่ AI generate มักหยิบ compute() มาพร้อม closure ที่ capture อะไรบางอย่างจาก scope รอบ ๆ — เพราะนั่นคือวิธีเขียน closure ที่เป็นธรรมชาติ และบั๊กจะโผล่แค่ตอน runtime ไม่ใช่ตอน analyze
// AI-generated: "parse this response off the main thread"class ApiClient { ApiClient(this._logger); final Logger _logger;
Future<List<Item>> fetchItems(String rawJson) { return compute<String, List<Item>>((json) { _logger.log('parsing ${json.length} chars'); // captures `this` via _logger final raw = jsonDecode(json) as List; return raw.map(Item.fromJson).toList(); }, rawJson); }}ก่อนจะเชื่อโค้ด compute()/isolate ที่ generate มาแบบนี้ ให้เช็ค:
- identifier ใด ๆ ข้างในตัว callback ที่ไม่ใช่ parameter หรือ top-level/static reference — ในที่นี้
_loggerเป็น instance field ซึ่งหมายความว่า closure capturethis(ApiClientinstance ทั้งตัว) เพื่อเข้าถึงมัน พิสูจน์แล้วด้านบนด้วยReceivePortว่า object ที่ unsendable ถูก capture จะ throwArgumentErrorจริงที่ระบุ field ที่พังชัดเจน — วิธีแก้คือส่งเฉพาะสิ่งที่ closure ต้องการเป็นข้อมูลธรรมดา (หรือ log นอก isolate หลังผลลัพธ์กลับมา) compute()ที่ใช้กับสิ่งที่ไม่ได้ CPU-bound จริง ๆ —await http.get(...)ปลด event loop ให้ตัวเองอยู่แล้ว การห่อมันด้วยcompute()เพิ่มต้นทุน isolate-spawn และ message-copy โดยไม่ได้อะไรเลย และ (พิสูจน์แล้วด้านบน) เป็นการเลื่อนที่ไม่ฟรีบน web- ไม่มีการจัดการกรณีที่งานเล็กมากจริง ๆ — พิสูจน์แล้วด้านบนว่าต้นทุน isolate-spawn ของ
compute()เองมีจริง (วัดได้ที่นี่ในระดับมิลลิวินาทีต่ำ ๆ) สำหรับงานที่เร็วกว่าหนึ่งมิลลิวินาที ต้นทุนนี้อาจมากกว่าตัวงานเอง Isolate.spawn/Isolate.runตรง ๆ ที่ใดก็ตามที่ต้องรันบน web ด้วย — พิสูจน์แล้วด้านบนจาก source ว่ามัน throwUnsupportedErrorบน web compiler มีแค่compute()เท่านั้นที่ลดระดับตัวเองได้อย่างนุ่มนวลบน web
🐛 หา bug ในโค้ดนี้
class ImageProcessor {ImageProcessor(this.watermarkText);final String watermarkText;
Future<Uint8List> process(Uint8List bytes) { return compute<Uint8List, Uint8List>((data) { return _applyWatermark(data, watermarkText); }, bytes);}
static Uint8List _applyWatermark(Uint8List data, String text) { // pretend this stamps 'text' onto the image bytes return data;}}เฉลย
closure ที่ส่งให้ compute() capture watermarkText ซึ่งเป็น instance field ที่เข้าถึงผ่าน this — พิสูจน์แล้วด้านบนด้วยกรณี ReceivePort ว่าการ capture อะไรก็ตามผ่าน instance reference เสี่ยงกับ ArgumentError: Illegal argument in isolate message แบบเดียวกันเป๊ะ และแม้ค่าที่ capture เองจะเป็นข้อมูลง่าย ๆ (String ในที่นี้) closure ก็ยังถือ reference ของ ImageProcessor instance ทั้งตัวอยู่ดีเพื่อเข้าถึงมัน ซึ่งเปราะบางทันทีที่ class นั้นมี field ที่ unsendable เพิ่มขึ้นมา วิธีแก้: ส่ง watermarkText เป็นส่วนหนึ่งของ message ตรง ๆ (เช่น record (bytes, watermarkText)) แล้วเรียก _applyWatermark ที่เป็น static อยู่แล้วตรง ๆ เพื่อให้ closure ไม่ capture อะไรจาก this เลย
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุก primitive ในบทนี้กำลังตอบคำถามเดียวกับ zone ในบทเรียน async-in-dart แค่ลึกลงไปอีกชั้น: อะไรบ้างที่ข้าม boundary ได้จริง ๆ และการข้ามมันมีต้นทุนเท่าไหร่? Future/Stream ข้าม boundary ของ “ทีหลัง” ได้ฟรี (isolate เดิม memory เดิม) แต่ isolate boundary ไม่ฟรีแม้ตอนที่มันเร็ว — พิสูจน์แล้วด้านบนว่า compute() คือ Isolate.run พร้อม debug label ตัวจริง ซึ่ง spawn OS-level isolate จริงและ copy byte จริงทุกครั้ง error จาก closure capture คือแนวป้องกันสุดท้ายของ type system สำหรับกฎที่ไม่มี static check (“compile-time”) เลย: isolate มี heap ของตัวเอง — analyzer ไม่มีทางรู้แค่จากการอ่าน closure ของคุณว่าค่าทุกตัวที่มัน capture เป็น “ข้อมูลธรรมดา” หรือเป็น handle ที่มีชีวิตเข้าไปยัง isolate ที่สร้างมัน มีแค่การส่ง message จริง ๆ แล้วดูผลเท่านั้นที่พิสูจน์ได้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
compute(fn, arg) | พิสูจน์แล้วด้านบน: พฤติกรรมเหมือน Isolate.run บน native ทุกอย่าง บวก fallback ที่ทำงานได้อย่างนุ่มนวล (thread เดิม) บน web | ทุก call spawn isolate ใหม่ — ต้นทุน spawn + copy จริงที่วัดได้ แม้กับงานเล็ก ๆ |
Isolate.run ตรง ๆ | cost profile เหมือน compute() ไม่ต้องพึ่ง Flutter dependency | พิสูจน์แล้วด้านบนจาก source: throw UnsupportedError บน web — ไม่ปลอดภัยเป็นค่า default ในโค้ดที่แชร์ |
Long-lived Isolate.spawn worker | จ่ายต้นทุน spawn แค่ครั้งเดียว ไม่ใช่ต่อ call — พิสูจน์แล้วด้านบน สามรอบไป-กลับผ่าน isolate เดียว | โค้ดเพิ่มขึ้น (ports, protocol, จัดการ lifecycle/kill() เอง) และ (เหมือน Isolate.spawn) ไม่รองรับ web |
TransferableTypedData | โอนย้ายด้วยเวลาคงที่ไม่ว่า payload จะใหญ่แค่ไหน ตามที่ docs อ้างไว้ด้านบน | ใช้ได้แค่กับ raw byte (TypedData) และสำเนาของผู้ส่งใช้ไม่ได้อีกหลังโอนย้าย |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
compute()ใช้ isolate pool ที่มีการจัดการ ทำให้เรียกซ้ำถูกกว่าIsolate.spawnตรง ๆ” พิสูจน์แล้วด้านบนว่าไม่จริง จากการอ่าน source ของ SDK ที่ติดตั้งจริง: บน nativecompute()เป็น wrapper บรรทัดเดียวรอบIsolate.run— ไม่มี pool ไม่มีการใช้ซ้ำ ต้นทุนต่อ call เท่ากันเป๊ะ - “
compute()รันแบบ background บนทุก platform รวมถึง web ด้วย” พิสูจน์แล้วด้านบนว่าไม่จริง ทั้งจาก source และ docs ทางการ: บน web มันรัน callback ของคุณบน event loop เดิม หลังจากหนึ่ง microtask — parallelism จริงมีแค่บน native platform เท่านั้น ที่Isolate.spawnทำงานได้จริง - “closure ไหนก็ส่งให้
compute()/Isolate.spawnได้ ตราบใดที่ compile ผ่าน” พิสูจน์แล้วด้านบนว่าไม่จริง: analyzer ไม่มีทางดักเรื่องนี้ได้ —ArgumentError: Illegal argument in isolate messageตัวจริงจะโผล่แค่ตอน runtime เท่านั้น พร้อมระบุ field ที่ส่งไม่ได้ - “การส่งข้อมูลก้อนใหญ่ไปยัง isolate มีต้นทุนการ copy ที่มากและหลีกเลี่ยงไม่ได้เสมอ” พิสูจน์แล้วด้านบนว่าไม่จริง:
TransferableTypedDataโอนย้ายความเป็นเจ้าของด้วยเวลาคงที่ — ต้นทุนนั้นใช้กับ message ธรรมดา (ที่ไม่ transferable) เท่านั้น แม้แต่ message แบบนั้นก็วัดได้แค่ ~14ms สำหรับ 10MB ด้านบน ซึ่งครอบงำด้วยต้นทุน isolate-spawn ไม่ใช่การ copy เอง