العوازل في Dart ونقل العمليات الثقيلة خارج الخيط الرئيسي
العوازل في Dart ونقل العمليات الثقيلة خارج الخيط الرئيسي
يرسم Flutter واجهته بمعدل 60 أو 120 إطاراً في الثانية، مما يعني أن العازل الرئيسي لديه نحو 8–16 ميلي ثانية لكل إطار لإتمام جميع الأعمال. إذا أجريت مهاماً كثيفة على المعالج — كتحليل ملف JSON كبير، أو تشفير البيانات، أو معالجة الصور، أو تشغيل خوارزميات معقدة — مباشرةً على العازل الرئيسي، ستُقيّد خيط واجهة المستخدم وتسبب تقطعاً واضحاً أو تجميداً في الإطارات. العوازل في Dart هي الحل: كل عازل وحدة تنفيذ مستقلة بذاكرة كومة خاصة بها، تتواصل فقط عبر تمرير الرسائل وليس عبر ذاكرة مشتركة.
كيف تعمل العوازل في Dart
خلافاً للخيوط (threads) في Java أو C++، لا تتشارك عوازل Dart الذاكرة. لكل منها كومتها (heap) الخاصة وحلقة أحداثها وجامع مهملاتها. التواصل بين العوازل يتم عبر تمرير الرسائل من خلال كائنات SendPort وReceivePort. يُلغي هذا التصميم سباقات البيانات والحاجة إلى أقفال، مما يجعل الكود المتزامن أكثر أماناً. القاعدة الأساسية هي: الكائنات المُرسَلة بين العوازل تُنسَخ وليس يُشار إليها (مع بعض الاستثناءات ذات النسخ الصفري للبيانات المُصنَّفة في إصدارات Dart الحديثة).
المسار السهل: compute()
يوفر Flutter الدالة العلوية compute() كغلاف مساعد حول Isolate.spawn. وهي مثالية للمهام البسيطة ذات الاتجاه الواحد حيث تريد تشغيل دالة نقية في عازل خلفي والحصول على نتيجة واحدة. تتولى compute() تلقائياً الإنشاء وتمرير الرسائل والتنظيف.
استخدام compute() لتحليل JSON ثقيل
import 'dart:convert';
import 'package:flutter/foundation.dart';
// يجب أن تكون هذه الدالة علوية المستوى أو ثابتة (static)
// (وليس إغلاقاً أو طريقة نموذج) — لا تستطيع العوازل التقاط الإغلاقات
List<Map<String, dynamic>> _parseProductJson(String jsonString) {
final List<dynamic> decoded = jsonDecode(jsonString) as List<dynamic>;
return decoded
.map((item) => item as Map<String, dynamic>)
.toList();
}
// في فئة الودجت أو الخدمة:
Future<List<Map<String, dynamic>>> loadProducts(String rawJson) async {
// تُشغّل _parseProductJson في عازل خلفي
// يظل خيط واجهة المستخدم حراً أثناء التحليل
final products = await compute(_parseProductJson, rawJson);
return products;
}
compute() وسيطاً واحداً فقط. إذا احتاجت دالتك إلى معاملات متعددة، ضمّها في كائن واحد أو Map. على سبيل المثال، مرر {'data': rawJson, 'locale': 'en'} وافكك محتواه داخل الدالة.التحكم الكامل: Isolate.spawn والمنافذ
عندما تحتاج إلى تواصل ثنائي الاتجاه، أو بث النتائج، أو عمال خلفيين طويلي العمر، استخدم Isolate.spawn مباشرةً مع SendPort وReceivePort. يمنحك هذا تحكماً كاملاً في دورة حياة العازل.
عازل طويل العمر مع تواصل ثنائي الاتجاه
import 'dart:isolate';
import 'dart:convert';
// نقطة دخول العازل الخلفي — يجب أن تكون علوية المستوى
void _encryptionWorker(SendPort mainSendPort) {
final ReceivePort workerReceivePort = ReceivePort();
// أرسل منفذنا إلى العازل الرئيسي حتى يتمكن من التواصل معنا
mainSendPort.send(workerReceivePort.sendPort);
workerReceivePort.listen((message) {
if (message is Map<String, dynamic>) {
final String plainText = message['text'] as String;
// محاكاة عمل تشفير ثقيل
final String encrypted = base64Encode(
utf8.encode(plainText.split('').reversed.join()),
);
mainSendPort.send({'result': encrypted, 'id': message['id']});
} else if (message == 'stop') {
workerReceivePort.close();
}
});
}
class EncryptionService {
Isolate? _isolate;
SendPort? _workerSendPort;
final ReceivePort _mainReceivePort = ReceivePort();
Future<void> start() async {
_isolate = await Isolate.spawn(_encryptionWorker, _mainReceivePort.sendPort);
// أول رسالة من العامل هي SendPort الخاص به
_workerSendPort = await _mainReceivePort.first as SendPort;
}
Future<String> encrypt(String text, int id) async {
_workerSendPort!.send({'text': text, 'id': id});
// انتظر الاستجابة المطابقة
final result = await _mainReceivePort
.where((msg) => msg is Map && msg['id'] == id)
.first as Map<String, dynamic>;
return result['result'] as String;
}
void stop() {
_workerSendPort?.send('stop');
_isolate?.kill(priority: Isolate.immediate);
}
}
متى تستخدم compute() مقابل Isolate.spawn
- compute(): المهام ذات اللقطة الواحدة، الدخل الواحد → الخرج الواحد (تحليل JSON، تصفية الصور، الترتيب الثقيل، تشفير حمولة واحدة). أبسط واجهة برمجة، لا إدارة يدوية للمنافذ.
- Isolate.spawn: العمال طويلو العمر، بث البيانات، رسائل ذهاباً وإياباً متعددة، المهام التي يجب أن تستمر بين تفاعلات المستخدم (مثل معالج تنزيل خلفي أو خط أنابيب صوتي في الوقت الفعلي).
- Dart 2.15+ Isolate.run(): بديل أنظف لـ
compute()يعمل مع أي دالة غير متزامنة وليس فقط تطبيقات Flutter. يؤدي نفس الوظيفة للحالات البسيطة.
حالات الاستخدام الشائعة في العالم الحقيقي
- تحليل JSON: استجابات واجهات برمجة التطبيقات التي تحتوي على آلاف السجلات يجب ألا تُفكَّك أبداً على العازل الرئيسي.
- معالجة الصور: تغيير الحجم، الضغط، تطبيق المرشحات — كلها مرتبطة بالمعالج ومثالية للعوازل الخلفية.
- التشفير / التجزئة: توليد تجزئات كلمات المرور (bcrypt, Argon2) أو تشفير الملفات يمكن أن يستغرق مئات الميلي ثوانٍ.
- البحث والتصفية: البحث النصي الكامل على مجموعات بيانات كبيرة في الذاكرة يُقيّد واجهة المستخدم إذا نُفِّذ بشكل متزامن.
- توليد PDF أو الملفات: بناء وثائق معقدة يتضمن معالجة سلاسل/بايتات ثقيلة يجب أن تعمل خارج الخيط الرئيسي.
TransferableTypedData)، لذا تجنب تمرير هياكل بيانات ضخمة دون حاجة؛ يُفضَّل تمرير قيم أولية فقط أو ترميزات مضغوطة مثل سلاسل JSON.التنميط: التأكيد من أن العمل خارج الخيط الرئيسي
بعد نقل العمل إلى عازل خلفي، افتح Flutter DevTools ← تبويب الأداء وسجّل تتبعاً. يظهر العمل الكثيف على المعالج المُنجَز في العوازل الخلفية في مسارات خيوط منفصلة (مُسمَّاة "Isolate" أو "Worker"). يجب أن يُظهر مسار الخيط الرئيسي لواجهة المستخدم إطارات رفيعة ومتباعدة بانتظام. إذا كانت الإطارات لا تزال سميكة (حمراء/صفراء)، فالعمل الثقيل لا يزال يعمل على العازل الرئيسي — تحقق مجدداً من أن دالتك علوية المستوى وأن compute() أو Isolate.spawn يُنتظَر فعلاً قبل استخدام النتيجة.
الملخص
نموذج العازل في Dart طريقة آمنة وفعّالة لتحقيق التوازي الحقيقي دون سباقات بيانات. استخدم compute() للمهام الكثيفة على المعالج ذات اللقطة الواحدة، وIsolate.spawn (أو Isolate.run في Dart 2.15+) للعمال المستمرين ذوي التواصل الأثرى. تحقق دائماً من أن النقل نجح فعلاً باستخدام DevTools — إبقاء العازل الرئيسي حراً هو أساس رسوم Flutter المتحركة السلسة.