ميزات Dart المتقدمة

العزلات والتزامن

55 دقيقة الدرس 5 من 16

فهم نموذج Dart أحادي الخيط

على عكس لغات مثل Java أو C++ التي تعتمد على خيوط ذاكرة مشتركة، فإن Dart أحادية الخيط بشكل أساسي. يعمل تطبيقك بالكامل على خيط واحد يسمى العزلة الرئيسية. يقضي هذا التصميم على فئة كاملة من الأخطاء -- حالات السباق، والجمود، وتلف البيانات من الوصول المتزامن للذاكرة -- لكنه يعني أيضاً أن العمل المكثف لوحدة المعالجة المركزية يمكن أن يحجب حلقة الأحداث ويجمد واجهة المستخدم.

تعالج حلقة أحداث Dart الأحداث واحداً تلو الآخر: نقرات المستخدم، واستجابات الشبكة، واستدعاءات المؤقت، وإكمالات Future كلها تصطف وتنفذ بالتسلسل. async/await يتيح لك كتابة كود إدخال/إخراج غير حاجب، لكنه لا يشغل الكود بالتوازي. await ببساطة يعيد التحكم لحلقة الأحداث أثناء انتظار نتيجة خارجية (مثل استجابة شبكة). إذا كانت لديك دالة تقضي 500 مللي ثانية في معالجة الأرقام، فإن async/await لن يساعد -- الخيط الرئيسي لا يزال محجوباً لمدة 500 مللي ثانية.

حلقة الأحداث في العمل

void main() async {
  print('1. Start');

  // هذا Future مجدول على حلقة الأحداث، لا يعمل بالتوازي
  Future.delayed(Duration(seconds: 1), () {
    print('3. Timer fired (after 1 second)');
  });

  // هذا يحجب الخيط الرئيسي لمدة ~2 ثانية
  // async/await لا يساعد هنا -- إنه عمل CPU بحت
  final result = heavyComputation();
  print('2. Heavy computation done: $result');
}

int heavyComputation() {
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
}
ملاحظة: يُطبع استدعاء المؤقت بعد الحساب الثقيل، على الرغم من أن المؤقت كان مضبوطاً على ثانية واحدة. هذا لأن حلقة الأحداث لا تستطيع معالجة حدث المؤقت بينما الخيط الرئيسي مشغول بالحساب. في تطبيق Flutter، هذا يعني واجهة مستخدم مجمدة طوال مدة الحساب.

ما هي العزلات؟

العزلة هي وحدة التزامن في Dart. كل عزلة لها كومة ذاكرة خاصة بها، وحلقة أحداث خاصة، وتعمل بشكل مستقل عن جميع العزلات الأخرى. يأتي اسم "العزلة" من حقيقة أن الذاكرة معزولة -- لا تستطيع أي عزلة الوصول مباشرة إلى متغيرات أو كائنات أو حالة عزلة أخرى. يحدث التواصل حصرياً عبر تمرير الرسائل.

فكر في العزلات كعمال منفصلين في غرف مختلفة، كل منهم بمكتبه وأدواته الخاصة. لا يستطيعون الوصول إلى مكاتب بعضهم البعض، لكن يمكنهم تمرير ملاحظات (رسائل) عبر فتحة بريد (منافذ). تضمن هذه البنية سلامة الذاكرة بدون أقفال أو كائنات المزامنة.

نصيحة: يبدأ كل برنامج Dart بعزلة واحدة -- العزلة الرئيسية. عندما تستدعي main()، أنت تعمل داخل العزلة الرئيسية. يمكنك إنشاء عزلات إضافية لأداء العمل بتوازٍ حقيقي، على أنوية CPU منفصلة.

Isolate.run() -- الواجهة البسيطة (Dart 2.19+)

قدم Dart 2.19 الدالة Isolate.run()، وهي طريقة عالية المستوى تتعامل مع كل التفاصيل المملة لإنشاء عزلة، وإرسال البيانات، واستقبال النتائج، والتنظيف. هذه هي الطريقة الموصى بها لنقل العمل المكثف لوحدة المعالجة المركزية.

الاستخدام الأساسي لـ Isolate.run()

import 'dart:isolate';

Future<void> main() async {
  print('Main isolate: starting heavy work on background isolate');

  // Isolate.run تأخذ دالة من المستوى الأعلى أو ثابتة
  // وتُرجع Future بالنتيجة
  final result = await Isolate.run(() {
    // هذا يعمل في عزلة منفصلة!
    int sum = 0;
    for (int i = 0; i < 1000000000; i++) {
      sum += i;
    }
    return sum;
  });

  print('Main isolate: result = $result');
  // يتم إنهاء العزلة الخلفية تلقائياً
}

يمكنك أيضاً تمرير بيانات إلى دالة العزلة. يجب أن تكون الدالة دالة من المستوى الأعلى أو طريقة ثابتة -- لا يمكن أن تكون طريقة مثيل أو إغلاق يلتقط حالة متغيرة.

تمرير البيانات إلى Isolate.run()

import 'dart:isolate';
import 'dart:convert';

// دالة من المستوى الأعلى -- مطلوبة للعزلات
List<Map<String, dynamic>> parseJsonList(String jsonString) {
  final List<dynamic> decoded = jsonDecode(jsonString);
  return decoded
      .map((item) => Map<String, dynamic>.from(item as Map))
      .toList();
}

Future<void> main() async {
  // محاكاة سلسلة JSON كبيرة
  final largeJson = List.generate(
    100000,
    (i) => '{"id": $i, "name": "Item $i", "value": ${i * 1.5}}',
  ).join(',');
  final jsonString = '[$largeJson]';

  print('Parsing ${jsonString.length} characters of JSON...');

  // التحليل في عزلة خلفية للحفاظ على استجابة واجهة المستخدم
  final parsed = await Isolate.run(() => parseJsonList(jsonString));

  print('Parsed ${parsed.length} items');
  print('First item: ${parsed.first}');
}
تحذير: يجب أن تكون البيانات الممررة إلى العزلة والمُرجعة منها قابلة للإرسال. الأنواع البدائية (int، double، String، bool، null)، والقوائم، والخرائط، والبيانات المنمطة قابلة للإرسال. الكائنات ذات الموارد الأصلية (مثل المقابس، ومقابض الملفات، أو RawReceivePort) غير قابلة للإرسال. منذ Dart 2.15، يمكن للفئات تنفيذ بروتوكول رسائل SendPort، ومنذ Dart 3.x، يمكنك استخدام TransferableTypedData لنقل بيانات كبيرة بكفاءة.

Isolate.spawn() -- الواجهة المنخفضة المستوى

Isolate.spawn() يمنحك مزيداً من التحكم في دورة حياة العزلة. أنت تدير المنافذ، والتواصل، والتنظيف بنفسك. هذا مفيد عندما تحتاج تواصلاً مستمراً مع العزلة (مثل مجمع عمال) بدلاً من طلب-استجابة واحدة.

استخدام Isolate.spawn() مع المنافذ

import 'dart:isolate';

// نقطة الدخول للعزلة المُنشأة
// يجب أن تكون دالة من المستوى الأعلى تأخذ معاملاً واحداً
void workerEntryPoint(SendPort mainSendPort) {
  // إنشاء منفذ لاستقبال الرسائل من الرئيسي
  final workerReceivePort = ReceivePort();

  // إرسال منفذ الاستقبال الخاص بنا إلى الرئيسي ليتمكن من التحدث إلينا
  mainSendPort.send(workerReceivePort.sendPort);

  // الاستماع للرسائل من الرئيسي
  workerReceivePort.listen((message) {
    if (message is int) {
      // إجراء حساب مكلف
      int result = 0;
      for (int i = 1; i <= message; i++) {
        result += i * i;
      }
      // إرسال النتيجة إلى الرئيسي
      mainSendPort.send(result);
    } else if (message == 'close') {
      workerReceivePort.close();
    }
  });
}

Future<void> main() async {
  // إنشاء منفذ لاستقبال الرسائل من العامل
  final mainReceivePort = ReceivePort();

  // إنشاء العزلة
  final isolate = await Isolate.spawn(
    workerEntryPoint,
    mainReceivePort.sendPort,
  );

  // انتظار أن يرسل العامل SendPort الخاص به
  final workerSendPort = await mainReceivePort.first as SendPort;

  // الآن أعد إعداد منفذ استقبال جديد للتواصل المستمر
  final responsePort = ReceivePort();

  // للتواصل المستمر، استخدم تيار
  final stream = mainReceivePort.asBroadcastStream();

  // الحصول على SendPort الخاص بالعامل (الرسالة الأولى)
  final SendPort sendPort = await stream.first as SendPort;

  // إرسال رقم للحساب
  sendPort.send(1000000);

  // انتظار النتيجة
  final result = await stream.first;
  print('Sum of squares up to 1000000: $result');

  // التنظيف
  sendPort.send('close');
  mainReceivePort.close();
  isolate.kill(priority: Isolate.immediate);
}

شرح SendPort و ReceivePort

المنافذ هي آلية التواصل بين العزلات. تعمل مثل قنوات رسائل أحادية الاتجاه:

ReceivePort -- منفذ يستمع للرسائل الواردة. ينفذ Stream، لذا يمكنك استخدام listen()، و first، و forEach()، وطرق التيار الأخرى. كل ReceivePort له SendPort مقابل.

SendPort -- مرجع خفيف يمكنه إرسال رسائل إلى ReceivePort المقابل له. على عكس ReceivePort، يمكن إرسال SendPort إلى عزلات أخرى (قابل للإرسال). هكذا تتشارك العزلات قنوات التواصل.

نمط التواصل بالمنافذ

import 'dart:isolate';

Future<void> main() async {
  // الخطوة 1: الرئيسي ينشئ ReceivePort
  final mainReceivePort = ReceivePort();

  // الخطوة 2: الرئيسي ينشئ العزلة، ممرراً SendPort الخاص به
  await Isolate.spawn(
    workerFunction,
    mainReceivePort.sendPort,
  );

  // الخطوة 4: الرئيسي يستقبل SendPort الخاص بالعامل
  final workerSendPort = await mainReceivePort.first as SendPort;

  // الآن الرئيسي يمكنه إرسال رسائل للعامل
  // والعامل يمكنه إرسال رسائل للرئيسي
}

void workerFunction(SendPort mainSendPort) {
  // الخطوة 3: العامل ينشئ ReceivePort الخاص به
  final workerReceivePort = ReceivePort();

  // العامل يرسل SendPort الخاص به للرئيسي
  mainSendPort.send(workerReceivePort.sendPort);

  // العامل يستمع لرسائل الرئيسي
  workerReceivePort.listen((message) {
    print('Worker received: $message');
    // المعالجة وإرسال الاستجابة
    mainSendPort.send('Processed: $message');
  });
}

دالة compute() (Flutter)

إذا كنت تعمل في Flutter، فإن دالة compute() (من package:flutter/foundation.dart) توفر واجهة أبسط حتى من Isolate.run(). كانت متاحة قبل وجود Isolate.run() وتبقى مستخدمة على نطاق واسع في تطبيقات Flutter.

دالة compute() في Flutter

import 'package:flutter/foundation.dart';

// يجب أن تكون دالة من المستوى الأعلى
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// يجب أن تكون دالة من المستوى الأعلى بمعامل واحد
Map<String, dynamic> processData(Map<String, dynamic> input) {
  final n = input['n'] as int;
  final result = fibonacci(n);
  return {'input': n, 'result': result};
}

// في ودجة Flutter:
Future<void> calculateFibonacci() async {
  // compute() تشغل processData في عزلة منفصلة
  final result = await compute(
    processData,
    {'n': 40},
  );
  print('Fibonacci(${result['input']}) = ${result['result']}');
}
ملاحظة: في Dart 2.19+، Isolate.run() هي أساساً نفس compute() في Flutter لكنها متاحة في Dart الخالص (بدون Flutter SDK). إذا كنت تكتب تطبيق Flutter، كلاهما يعمل بشكل جيد. لمشاريع Dart الخالصة، استخدم Isolate.run().

متى تستخدم العزلات مقابل async/await

الاختيار بين async/await والعزلات هو قرار معماري حاسم. إليك دليل واضح:

استخدم async/await عندما:

  • تنتظر طلبات الشبكة (HTTP، WebSocket)
  • تقرأ/تكتب ملفات (عمليات مقيدة بالإدخال/الإخراج)
  • تنتظر إدخال المستخدم أو أحداث المؤقت
  • أي عملية يتم فيها العمل بواسطة نظام التشغيل أو خدمة خارجية

استخدم العزلات عندما:

  • تحليل ملفات JSON كبيرة (> 1 ميجابايت)
  • معالجة الصور (تغيير الحجم، الفلتر، الضغط)
  • العمليات الحسابية المعقدة
  • تشفير/فك تشفير البيانات
  • البحث/الترتيب على مجموعات بيانات كبيرة (> 10,000 عنصر)
  • أي حساب يستغرق أكثر من ~16 مللي ثانية (إطار واحد عند 60 FPS)

المقارنة: async/await مقابل Isolate

import 'dart:isolate';
import 'dart:io';

// صحيح: استخدم async/await للإدخال/الإخراج
Future<String> fetchData() async {
  // نظام التشغيل يتعامل مع طلب الشبكة؛ خيط Dart حر
  final client = HttpClient();
  final request = await client.getUrl(Uri.parse('https://api.example.com/data'));
  final response = await request.close();
  return await response.transform(utf8.decoder).join();
}

// صحيح: استخدم العزلة لتحليل CPU الثقيل
Future<List<Map<String, dynamic>>> fetchAndParse() async {
  // الخطوة 1: جلب البيانات (إدخال/إخراج -- استخدم async/await)
  final jsonString = await fetchData();

  // الخطوة 2: تحليل البيانات (CPU -- استخدم العزلة)
  final parsed = await Isolate.run(
    () => parseJsonList(jsonString),
  );

  return parsed;
}

// خطأ: استخدام العزلة للإدخال/الإخراج (عبء غير ضروري)
// خطأ: استخدام async/await لحساب ثقيل (يحجب حلقة الأحداث)
نصيحة: قاعدة عامة جيدة لـ Flutter: إذا كانت العملية تستغرق أقل من 16 مللي ثانية (إطار واحد عند 60 FPS)، احتفظ بها في العزلة الرئيسية. إذا استغرقت أكثر، انقلها إلى عزلة خلفية للحفاظ على سلاسة الرسوم المتحركة والتمرير. استخدم Stopwatch لقياس أداء كودك إذا لم تكن متأكداً.

مثال عملي: خط أنابيب معالجة الصور

إليك مثال واقعي لاستخدام العزلات لمعالجة الصور المكثفة لوحدة المعالجة المركزية. هذا النمط شائع في تطبيقات Flutter التي تتعامل مع الصور.

معالجة الصور بالعزلات

import 'dart:isolate';
import 'dart:typed_data';

// يمثل بيانات البكسل للصورة
class ImageData {
  final int width;
  final int height;
  final Uint8List pixels; // بايتات RGBA

  ImageData(this.width, this.height, this.pixels);
}

// دالة من المستوى الأعلى: تطبيق فلتر التدرج الرمادي
Uint8List applyGrayscale(Uint8List pixels) {
  final result = Uint8List(pixels.length);
  for (int i = 0; i < pixels.length; i += 4) {
    final r = pixels[i];
    final g = pixels[i + 1];
    final b = pixels[i + 2];
    final a = pixels[i + 3];

    // صيغة السطوع (ترجيح إدراكي)
    final gray = (0.299 * r + 0.587 * g + 0.114 * b).round();
    result[i] = gray;     // R
    result[i + 1] = gray; // G
    result[i + 2] = gray; // B
    result[i + 3] = a;    // A (بدون تغيير)
  }
  return result;
}

// دالة من المستوى الأعلى: ضبط السطوع
Uint8List adjustBrightness(List<dynamic> args) {
  final pixels = args[0] as Uint8List;
  final factor = args[1] as double; // -1.0 إلى 1.0

  final result = Uint8List(pixels.length);
  final adjustment = (factor * 255).round();

  for (int i = 0; i < pixels.length; i += 4) {
    result[i] = (pixels[i] + adjustment).clamp(0, 255);
    result[i + 1] = (pixels[i + 1] + adjustment).clamp(0, 255);
    result[i + 2] = (pixels[i + 2] + adjustment).clamp(0, 255);
    result[i + 3] = pixels[i + 3]; // Alpha بدون تغيير
  }
  return result;
}

Future<void> main() async {
  // محاكاة صورة 1920x1080 (حوالي 8 ميجابايت من بيانات البكسل)
  final width = 1920;
  final height = 1080;
  final pixels = Uint8List(width * height * 4);

  // ملء ببيانات عينة
  for (int i = 0; i < pixels.length; i += 4) {
    pixels[i] = 128;     // R
    pixels[i + 1] = 64;  // G
    pixels[i + 2] = 192; // B
    pixels[i + 3] = 255; // A
  }

  print('Processing ${width}x${height} image...');

  // تشغيل فلتر التدرج الرمادي على عزلة خلفية
  final grayscaled = await Isolate.run(
    () => applyGrayscale(pixels),
  );

  // تشغيل ضبط السطوع على عزلة خلفية
  final brightened = await Isolate.run(
    () => adjustBrightness([grayscaled, 0.2]),
  );

  print('Done! Processed ${brightened.length} bytes');
  print('Sample pixel: R=${brightened[0]} G=${brightened[1]} B=${brightened[2]}');
}

مثال عملي: الحساب المتوازي

يمكنك إنشاء عدة عزلات لتشغيل الحسابات بالتوازي، مستفيداً من أنوية CPU المتعددة.

تشغيل عزلات متعددة بالتوازي

import 'dart:isolate';

// دالة من المستوى الأعلى: التحقق من عدد أولي
bool isPrime(int n) {
  if (n < 2) return false;
  if (n < 4) return true;
  if (n % 2 == 0 || n % 3 == 0) return false;
  for (int i = 5; i * i <= n; i += 6) {
    if (n % i == 0 || n % (i + 2) == 0) return false;
  }
  return true;
}

// دالة من المستوى الأعلى: عد الأعداد الأولية في نطاق
int countPrimesInRange(List<int> range) {
  final start = range[0];
  final end = range[1];
  int count = 0;
  for (int i = start; i <= end; i++) {
    if (isPrime(i)) count++;
  }
  return count;
}

Future<void> main() async {
  const maxNumber = 10000000;
  const numIsolates = 4;
  const rangeSize = maxNumber ~/ numIsolates;

  print('Counting primes up to $maxNumber using $numIsolates isolates...');

  final stopwatch = Stopwatch()..start();

  // إنشاء نطاقات لكل عزلة
  final futures = <Future<int>>[];
  for (int i = 0; i < numIsolates; i++) {
    final start = i * rangeSize + 1;
    final end = (i == numIsolates - 1) ? maxNumber : (i + 1) * rangeSize;
    futures.add(Isolate.run(() => countPrimesInRange([start, end])));
  }

  // انتظار اكتمال جميع العزلات
  final results = await Future.wait(futures);
  final totalPrimes = results.reduce((a, b) => a + b);

  stopwatch.stop();
  print('Found $totalPrimes primes in ${stopwatch.elapsedMilliseconds}ms');
}
تحذير: إنشاء عزلة له تكلفة -- كل عزلة تحتاج كومة ذاكرة خاصة ووقت بدء (عادة 5-50 مللي ثانية). لا تنشئ عزلات لحسابات بسيطة. أيضاً، تجنب إنشاء عزلات كثيرة دفعة واحدة؛ قاعدة جيدة هي استخدام Platform.numberOfProcessors عزلات كحد أقصى للعمل المقيد بالمعالج. إنشاء 100 عزلة على جهاز بـ 4 أنوية لن يجعل الأمور أسرع 100 مرة -- سيضيف عبء تبديل السياق.

معالجة الأخطاء في العزلات

الأخطاء المرمية داخل عزلة لا تنتشر تلقائياً إلى العزلة الرئيسية. مع Isolate.run()، يتم التقاط الأخطاء وإعادة رميها كـ RemoteError في العزلة المستدعية. مع Isolate.spawn()، تحتاج معالجة الأخطاء بشكل صريح.

معالجة الأخطاء مع Isolate.run()

import 'dart:isolate';

int riskyComputation(int input) {
  if (input < 0) {
    throw ArgumentError('Input must be non-negative: $input');
  }
  return input * input;
}

Future<void> main() async {
  try {
    // هذا سيرمي RemoteError
    final result = await Isolate.run(() => riskyComputation(-5));
    print('Result: $result');
  } on RemoteError catch (e) {
    print('Error from isolate: $e');
  }

  // الحالة الناجحة
  try {
    final result = await Isolate.run(() => riskyComputation(7));
    print('Result: $result'); // Result: 49
  } on RemoteError catch (e) {
    print('Error from isolate: $e');
  }
}

الملخص

يوفر نموذج العزلات في Dart توازياً حقيقياً مع ضمان سلامة الذاكرة. استخدم Isolate.run() (Dart 2.19+) لمهام النقل والإرجاع البسيطة، و Isolate.spawn() مع SendPort/ReceivePort لتواصل العمال المستمر، و compute() في Flutter كغلاف مريح. احتفظ بالعزلات للعمل المكثف لوحدة المعالجة المركزية الذي يحجب حلقة الأحداث لأكثر من 16 مللي ثانية، واستخدم async/await للعمليات المقيدة بالإدخال/الإخراج.

نصيحة: عند الشك، قس الأداء أولاً. استخدم Stopwatch لقياس مدة حسابك. إذا تجاوز 16 مللي ثانية ويعمل أثناء تفاعل المستخدم، انقله إلى عزلة. التحسين المبكر بالعزلات يضيف تعقيداً -- استخدمها فقط عندما تواجه مشكلة أداء حقيقية.