كيفية استخدام Thread.sleep بفعالية دون حظر سلاسل التنفيذ في بيئة JVM

دقائق القراءة: 14

مقدمة: فهم تحديات Thread.sleep في بيئة JVM

توفر لغات بيئة التشغيل الافتراضية لجافا (JVM) مثل جافا وسكالا قدرات قوية لتشغيل التعليمات البرمجية المتزامنة باستخدام فئة Thread. ومع ذلك، فإن التعامل مع سلاسل التنفيذ (Threads) يُعرف بتعقيده الكبير وقابليته العالية للأخطاء. لذا، فإن امتلاك فهم راسخ لكيفية عملها يُعد أمرًا بالغ الأهمية لتطوير تطبيقات مستقرة وفعالة.

لنبدأ بتعريف Thread.sleep وفقًا لوثائق Javadoc الرسمية:

تتسبب في جعل سلسلة التنفيذ الجارية حاليًا في وضع السكون (إيقاف مؤقت للتنفيذ) لعدد محدد من المللي ثانية.

ما هي تداعيات عبارة **إيقاف مؤقت للتنفيذ**، والتي تُعرف أيضًا باسم **الحظر (Blocking)**؟ وماذا يعني ذلك بالضبط؟ هل هو أمر سيء؟ وإذا كان كذلك، فهل يمكننا تحقيق **نوم غير حظر (non-blocking sleep)**؟

ماذا سنتناول في هذا المقال؟

يغطي هذا المقال مجموعة واسعة من المفاهيم، ونأمل أن تكتسب من خلاله معرفة قيمة حول:

  • ماذا يحدث على مستوى نظام التشغيل عند وضع سلسلة التنفيذ في وضع السكون؟
  • المشكلة الأساسية مع استخدام Thread.sleep.
  • مبادرة Project Loom وسلاسل التنفيذ الافتراضية (Virtual Threads).
  • البرمجة الوظيفية (Functional Programming) والتصميم.
  • مكتبة ZIO Scala للبرمجة المتزامنة.

كل هذا سيتم شرحه بالتفصيل أدناه. ولكن أولاً، دعنا نبدأ بمقتطف بسيط بلغة سكالا سنقوم بتعديله طوال المقال لتحقيق هدفنا:

println( "a" )
Thread .sleep( 1000 )
println( "b" )

الأمر بسيط للغاية: يقوم بطباعة الحرف “a”، ثم بعد 10 ثوانٍ يطبع الحرف “b”.

دعنا نركز على Thread.sleep ونحاول فهم كيفية تحقيقها لوضع السكون. بمجرد أن نفهم الكيفية، سنتمكن من رؤية المشكلة وتحديدها بشكل أكثر وضوحًا.

كيف يعمل وضع السكون على مستوى نظام التشغيل؟

إليك ما يحدث عندما تستدعي Thread.sleep في الكواليس:

  • يتم استدعاء واجهة برمجة تطبيقات سلاسل التنفيذ (Thread API) لنظام التشغيل الأساسي.
  • نظرًا لأن بيئة JVM تستخدم تعيينًا واحدًا لواحد بين سلاسل تنفيذ جافا وسلاسل تنفيذ النواة (kernel threads)، فإنها تطلب من نظام التشغيل التخلي عن “حقوق” سلسلة التنفيذ في استخدام وحدة المعالجة المركزية (CPU) للمدة المحددة.
  • عندما تنقضي المدة، يقوم مُجدول نظام التشغيل (OS scheduler) بإيقاظ سلسلة التنفيذ عبر مقاطعة (interrupt) (وهذا فعال)، ويخصص لها شريحة زمنية من وحدة المعالجة المركزية للسماح لها باستئناف التشغيل.

النقطة الحاسمة هنا هي أن سلسلة التنفيذ النائمة تُسحب بالكامل من الخدمة ولا يمكن إعادة استخدامها أثناء فترة السكون.

قيود سلاسل التنفيذ التقليدية

تأتي سلاسل التنفيذ التقليدية مع العديد من القيود الهامة، منها:

  • الحد الأقصى لعدد سلاسل التنفيذ: هناك حد لعدد سلاسل التنفيذ التي يمكنك إنشاؤها. بعد حوالي 30 ألف سلسلة تنفيذ، قد تواجه هذا الخطأ:

    java.lang.OutOfMemoryError : unable to create new native Thread
  • تكلفة الذاكرة: يمكن أن تكون سلاسل تنفيذ JVM مكلفة من حيث استهلاك الذاكرة، حيث تأتي كل منها مع مكدس (stack) مخصص.

  • تكلفة التبديل بين السياقات: يؤدي وجود عدد كبير جدًا من سلاسل تنفيذ JVM إلى زيادة الحمل الزائد بسبب التبديل المكلف بين السياقات (context switches) وطريقة مشاركتها لموارد الأجهزة المحدودة.

الآن بعد أن فهمنا المزيد عما يحدث خلف الكواليس، دعنا نعود إلى مشكلة وضع السكون.

مشكلة Thread.sleep: تحليل معمق

دعنا نحدد المشكلة بشكل أكثر وضوحًا ونشغل مقتطفًا من التعليمات البرمجية لتوضيح المشكلة التي نواجهها. سنستخدم هذه الدالة لتوضيح الفكرة:

 def task (id: Int ): Runnable = () => {
 println( s" ${Thread.currentThread().getName()} start- $id " )
 Thread .sleep( 10000 )
 println( s" ${Thread.currentThread().getName()} end- $id " )
 }

هذه الدالة البسيطة ستقوم بما يلي:

  • طباعة **start** متبوعًا بمعرف سلسلة التنفيذ (thread id).
  • تنام لمدة 10 ثوانٍ.
  • طباعة **end** متبوعًا بمعرف سلسلة التنفيذ.

مهمتك، إذا قبلتها، هي تشغيل مهمتين (tasks) متزامنتين باستخدام سلسلة تنفيذ واحدة فقط. نريد تشغيل مهمتين بشكل متزامن، مما يعني أن البرنامج بأكمله يجب أن يستغرق 10 ثوانٍ إجمالًا. لكن لدينا سلسلة تنفيذ واحدة فقط متاحة. هل أنت مستعد لهذا التحدي؟

دعنا نلعب قليلاً بعدد المهام وسلاسل التنفيذ للحصول على فكرة دقيقة عن المشكلة.

سيناريو 1: مهمة واحدة وسلسلة تنفيذ واحدة (1 task -> 1 thread)

 new Thread (task( 1 )).start()

الناتج:

 12 : 11 : 08 INFO Thread -0 start -1
 12 : 11 : 18 INFO Thread -0 end -1

دعنا نشغل jvisualvm للتحقق مما تفعله سلسلة التنفيذ:

لقطة شاشة لـ jvisualvm تظهر حالة سلسلة تنفيذ واحدة في وضع السكون

يمكنك أن ترى أن Thread-0 في الحالة الأرجوانية، وهي حالة sleeping. عند الضغط على زر تفريغ سلاسل التنفيذ (thread dump) سيتم طباعة هذا:

 "Thread-0" # 13 prio= 5 os_prio= 31 tid= 0x00007f9a3e0e2000 nid= 0x5b03 waiting on condition [ 0x0000700004ac8000 ]
 java.lang.Thread.State: TIMED_WAITING (sleeping)
 at java.lang.Thread.sleep(Native Method)
 at example.Blog$.$anonfun$task$ 1 (Blog.scala: 7 )
 at example.Blog$$$Lambda$ 2 / 1359484306. run(Unknown Source)
 at java.lang.Thread.run(Thread.java: 748 )
 Locked ownable synchronizers: - None

من الواضح أن سلسلة التنفيذ هذه لم تعد قابلة للاستخدام حتى تنتهي من وضع السكون.

سيناريو 2: مهمتان وسلسلة تنفيذ واحدة (2 tasks -> 1 thread)

دعنا نوضح المشكلة عن طريق تشغيل مهمتين من هذا النوع مع توفر سلسلة تنفيذ واحدة فقط:

 import java.util.concurrent. Executors

 // an executor with only 1 thread available
 val oneThreadExecutor = Executors .newFixedThreadPool( 1 )

 // send 2 tasks to the executor
 ( 1 to 2 ).foreach(id => oneThreadExecutor.execute(task(id)))

نحصل على هذا الناتج:

 2020.09 .28 21 : 49 : 56 INFO pool -1 -thread -1 start -1
 2020.09 .28 21 : 50 : 07 INFO pool -1 -thread -1 end -1
 2020.09 .28 21 : 50 : 07 INFO pool -1 -thread -1 start -2
 2020.09 .28 21 : 50 : 17 INFO pool -1 -thread -1 end -2

لقطة شاشة لـ jvisualvm تظهر سلسلة تنفيذ واحدة في وضع السكون، مع تنفيذ المهام بشكل تسلسلي

يمكنك رؤية اللون الأرجواني (حالة السكون) لـ pool-1-thread-1. ليس أمام المهام خيار سوى التشغيل واحدة تلو الأخرى لأن سلسلة التنفيذ تُسحب من الخدمة في كل مرة يتم فيها استخدام Thread.sleep.

سيناريو 3: مهمتان وسلسلتا تنفيذ (2 tasks -> 2 threads)

دعنا نشغل نفس التعليمات البرمجية مع توفر سلسلتي تنفيذ. نحصل على هذا:

 // an executor with 2 threads available
 val oneThreadExecutor = Executors .newFixedThreadPool( 2 )

 // send 2 tasks to the executor
 ( 1 to 2 ).foreach(id => oneThreadExecutor.execute(task(id)))

الناتج:

 2020.09 .28 22 : 42 : 04 INFO pool -1 -thread -2 start -2
 2020.09 .28 22 : 42 : 04 INFO pool -1 -thread -1 start -1
 2020.09 .28 22 : 42 : 14 INFO pool -1 -thread -1 end -1
 2020.09 .28 22 : 42 : 14 INFO pool -1 -thread -2 end -2

يمكن لكل سلسلة تنفيذ تشغيل مهمة واحدة في كل مرة. لقد حققنا أخيرًا ما أردناه، تشغيل مهمتين بشكل متزامن، وانتهى البرنامج بأكمله في 10 ثوانٍ.

لقطة شاشة لـ jvisualvm تظهر سلسلتي تنفيذ تعملان بشكل متزامن

كان ذلك سهلاً لأننا استخدمنا سلسلتي تنفيذ (pool-1-thread-1 و pool-1-thread-2)، لكننا نريد أن نفعل الشيء نفسه بسلسلة تنفيذ واحدة فقط. دعنا نحدد المشكلة ثم نجد حلاً.

المشكلة: Thread.sleep يسبب الحظر.

لقد فهمنا الآن أننا لا نستطيع استخدام Thread.sleep – فهي تحظر سلسلة التنفيذ الحالية وتجعلها غير قابلة للاستخدام لأي عمل آخر. وهذا يمنعنا من تشغيل مهمتين بشكل متزامن. لحسن الحظ، هناك حلول، والتي سنناقشها لاحقًا.

الحل الأول: ترقية بيئة JVM باستخدام Project Loom

لقد ذكرت سابقًا أن سلاسل تنفيذ JVM تتطابق واحدًا لواحد مع سلاسل تنفيذ نظام التشغيل. وهذا الخطأ التصميمي القاتل يقودنا إلى هنا. يهدف Project Loom إلى تصحيح ذلك عن طريق إضافة سلاسل تنفيذ افتراضية (Virtual Threads).

إليك التعليمات البرمجية الخاصة بنا المعاد كتابتها باستخدام سلاسل التنفيذ الافتراضية من Loom:

 Thread .startVirtualThread(() -> {
 System .out.println( "a" )
 Thread .sleep( 1000 )
 System .out.println( "b" )
 });

الشيء المذهل هو أن Thread.sleep لن تحظر بعد الآن! إنها غير متزامنة تمامًا. وفوق ذلك، فإن سلاسل التنفيذ الافتراضية رخيصة جدًا. يمكنك إنشاء مئات الآلاف منها دون حمل زائد أو قيود. لقد تم حل جميع مشاكلنا الآن – حسنًا، باستثناء حقيقة أن Project Loom لن يكون متاحًا حتى JDK 17 على الأقل (حسب الجدول الزمني الحالي في سبتمبر 2021).

حسنًا، دعنا نعود ونحاول حل مشكلة وضع السكون بما توفره بيئة JVM حاليًا.

فهم جوهري: يمكن التعبير عن وضع السكون بجدولة مهمة في المستقبل

إذا أخبرت مديرك أنك مشغول وستستأنف عملك بعد 10 دقائق، فإن مديرك لا يعرف أنك على وشك أخذ قيلولة. كل ما يراه هو أنك بدأت عملك في الصباح ثم توقفت لمدة 10 دقائق ثم استأنفت.

هذا:

start
sleep( 10 )
end

يعادل من الخارج هذا:

start
resumeIn( 10 s, end)

ما فعلناه أعلاه هو **جدولة** المهمة لتنتهي في 10 ثوانٍ. هذا كل شيء، لم نعد بحاجة إلى النوم. نحتاج فقط إلى أن نكون قادرين على جدولة الأشياء في المستقبل بدلاً من ذلك. لقد قللنا مشكلة واحدة بمشكلة أخرى، وهي مشكلة أسهل ولها حل أبسط.

مشكلة الجدولة والحلول المتاحة

لحسن حظنا، جدولة المهام أمر بسيط للغاية. علينا فقط تبديل مُنفّذ المهام (executor) على النحو التالي:

val oneThreadScheduleExecutor = Executors.newScheduledThreadPool( 1 )

يمكننا الآن استخدام الدالة schedule بدلاً من execute:

oneThreadScheduleExecutor.schedule (task( 1 ), 10 , TimeUnit . SECONDS )

حسنًا، هذا ليس بالضبط ما نريده. نريد تقسيم طباعة “start” و “end” بفاصل زمني قدره 10 ثوانٍ، لذا دعنا نغير دالة task الخاصة بنا على النحو التالي:

 def nonBlockingTask (id: Int ): Runnable = () => {
 println( s" ${Thread.currentThread().getName()} start- $id " )
 val endTask: Runnable = () => {
 println( s" ${Thread.currentThread().getName()} end- $id " )
 }
 //instead of Thread.sleep for 10s, we schedule it in the future, no more blocking!
 oneThreadScheduleExecutor.schedule(endTask, 10 , TimeUnit . SECONDS )
 }

الناتج:

 2020.09 .28 23 : 35 : 45 INFO pool -1 -thread -1 start -1
 2020.09 .28 23 : 35 : 45 INFO pool -1 -thread -1 start -2
 2020.09 .28 23 : 35 : 56 INFO pool -1 -thread -1 end -1
 2020.09 .28 23 : 35 : 56 INFO pool -1 -thread -1 end -2

نعم! لقد فعلناها! سلسلة تنفيذ واحدة ومهمتان متزامنتان “تنامان” لمدة 10 ثوانٍ لكل منهما. هذا رائع، لكن لا يمكنك حقًا كتابة التعليمات البرمجية بهذه الطريقة. ماذا لو أردت مهمة أخرى في المنتصف على النحو التالي:

 00 : 00 : 00 start
 00 : 00 : 10 middle
 00 : 00 : 20 end

ستحتاج إلى تغيير تنفيذ nonBlockingTask وإضافة استدعاء آخر لـ schedule هناك. وهذا سيصبح فوضويًا جدًا بسرعة كبيرة.

كيفية استخدام البرمجة الوظيفية (Functional Programming) لكتابة لغة خاصة بالمجال (DSL) مع وضع سكون غير حظر

البرمجة الوظيفية في سكالا ممتعة، وكتابة لغة خاصة بالمجال (DSL) باستخدام مبادئ البرمجة الوظيفية أمر سهل للغاية. دعنا نبدأ من النهاية. نود أن يبدو برنامجنا النهائي شيئًا كهذا:

 def nonBlockingFunctionalTask (id: Int ) = {
 Print (id, "start" )
 andThen Print (id, "middle" ).sleep( 1000 )
 andThen Print (id, "end" ).sleep( 1000 )
 }

ستحقق هذه اللغة المصغرة نفس السلوك تمامًا مثل حلنا السابق ولكن دون الكشف عن جميع التفاصيل الداخلية المزعجة للمُنفّذ المجدول (scheduled executor) وسلاسل التنفيذ.

النموذج (The Model)

دعنا نحدد أنواع بياناتنا:

 object Task {
 sealed trait Task { self => def andThen (other: Task ) = AndThen (self,other)
 def sleep (millis: Long ) = Sleep (self,millis)
 }
 case class AndThen ( t1: Task , t2: Task ) extends Task
 case class Print ( id: Int , value: String ) extends Task
 case class Sleep ( t1: Task , millis: Long ) extends Task

في البرمجة الوظيفية، تحمل أنواع البيانات البيانات فقط ولا تحمل أي سلوك. لذا فإن هذه التعليمات البرمجية بأكملها “لا تفعل شيئًا” – إنها فقط تلتقط بنية اللغة والمعلومات التي نريدها. نحتاج إلى دالتين:

  • sleep لجعل المهمة تنام.
  • andThen لربط المهام ببعضها.

لاحظ أن تنفيذها لا يفعل شيئًا. إنه فقط يغلفها في الفئة الصحيحة وهذا كل ما في الأمر.

دعنا نستخدم دالة nonBlockingFunctionalTask الخاصة بنا:

 import Task ._

 //create 2 tasks, this does not run them, no threads involved here
 ( 1 to 2 ).toList.map(nonBlockingFunctionalTask)

إنه وصف للمشكلة. لا يفعل شيئًا، إنه فقط يبني قائمة بمهمتين، كل واحدة تصف ما يجب فعله. إذا طبعنا النتيجة في REPL نحصل على هذا:

res3: List [ Task ] = List (
 //first task
 AndThen (
 AndThen (
 Print ( 1 ,start),
 Sleep ( Print ( 1 ,middle), 10000 )),
 Sleep ( Print ( 1 ,end), 10000 )),
 //second task
 AndThen (
 AndThen (
 Print ( 2 ,start),
 Sleep ( Print ( 2 ,middle), 10000 )),
 Sleep ( Print ( 2 ,end), 10000 ))
 )

دعنا نكتب interpreter الذي سيحول هذه الشجرة إلى شجرة تقوم بتشغيل المهام بالفعل.

المُفسّر (The Interpreter)

في البرمجة الوظيفية، تسمى الدالة التي تحول الوصف إلى برنامج قابل للتنفيذ interpreter. تأخذ وصف البرنامج، النموذج، وتفسره إلى شكل قابل للتنفيذ. هنا ستقوم بتنفيذ وجدولة المهام مباشرة.

نحتاج أولاً إلى Stack يسمح لنا بترميز التبعيات بين المهام. فكر أن start >>= middle >>= end سيتم دفع كل منها إلى المكدس ثم إخراجها بترتيب التنفيذ. سيكون هذا واضحًا في التنفيذ.

والآن المُفسّر (لا تقلق إذا لم تفهم هذه التعليمات البرمجية، إنها معقدة بعض الشيء، هناك حل أبسط قادم):

 def interpret (task: Task , executor: ScheduledExecutorService ): Unit = {
 def loop (current: Task , stack: Stack [ Task ]): Unit = current match {
 case AndThen (t1, t2) => loop(t1,stack.push(t2))
 case Print (id, value) => stack.pop match {
 case Some ((t2, b)) => executor.execute(() => {
 println( s" ${Thread.currentThread().getName()} $value - $id " )
 })
 loop(t2,b)
 case None => executor.execute(() => {
 println( s" ${Thread.currentThread().getName()} $value - $id " )
 })
 case Sleep (t1,millis) => val r: Runnable = () =>{loop(t1,stack)}
 executor.schedule(r, millis, TimeUnit . MILLISECONDS )
 }
 loop(task, Nil )
 }

والناتج هو ما نريده:

 2020.09 .29 00 : 06 : 39 INFO pool -1 -thread -1 start -1
 2020.09 .29 00 : 06 : 39 INFO pool -1 -thread -1 start -2
 2020.09 .29 00 : 06 : 50 INFO pool -1 -thread -1 middle -1
 2020.09 .29 00 : 06 : 50 INFO pool -1 -thread -1 middle -2
 2020.09 .29 00 : 07 : 00 INFO pool -1 -thread -1 end -1
 2020.09 .29 00 : 07 : 00 INFO pool -1 -thread -1 end -2

سلسلة تنفيذ واحدة تشغل مهمتين متزامنتين في وضع السكون. هذا قدر كبير من التعليمات البرمجية والكثير من العمل. كالعادة، يجب أن تسأل نفسك دائمًا ما إذا كانت هناك مكتبة تحل هذه المشكلة بالفعل. اتضح أن هناك مكتبة: ZIO.

وضع السكون غير الحظر في مكتبة ZIO

**[ZIO](https://zio.dev/)** هي مكتبة وظيفية للبرمجة غير المتزامنة والمتزامنة. تعمل بطريقة مشابهة للغة الخاصة بالمجال (DSL) الصغيرة الخاصة بنا، لأنها توفر لك بعض الأنواع التي يمكنك مزجها ومطابقتها لوصف برنامجك لا أكثر. ثم توفر لنا مُفسّرًا يتيح لك تشغيل برنامج ZIO. كما قلت، نمط المُفسّر هذا منتشر في عالم البرمجة الوظيفية. بمجرد أن تفهمه، ينفتح لك عالم جديد.

ZIO.sleep – نسخة أفضل من Thread.sleep

توفر لنا ZIO دالة ZIO.sleep، وهي نسخة غير حظر من Thread.sleep. إليك دالتنا مكتوبة باستخدام ZIO:

 import zio._
 import zio.console._
 import zio.duration._

 object ZIOApp extends zio . App {
 def zioTask (id: Int ) = for {
 _ <- putStrLn( s" ${Thread.currentThread().getName()} start- $id " )
 _ <- ZIO .sleep( 10. seconds)
 _ <- putStrLn( s" ${Thread.currentThread().getName()} end- $id " )
 } yield ()
 }

إنها مشابهة بشكل لافت للنظر للمقتطف الأول:

 def task (id: Int ): Runnable = () => {
 println( s" ${Thread.currentThread().getName()} start- $id " )
 Thread .sleep( 10000 )
 println( s" ${Thread.currentThread().getName()} end- $id " )
 }

الفرق الواضح هو صيغة for التي تسمح لنا بربط العبارات بنوع ZIO. إنها تشبه إلى حد كبير دالة andThen من لغتنا المصغرة السابقة. كما كان الحال من قبل مع لغتنا المصغرة، هذا البرنامج هو مجرد وصف. إنه بيانات بحتة، ولا يفعل شيئًا. لكي يفعل شيئًا نحتاج إلى المُفسّر.

مُفسّر ZIO

لتفسير برنامج ZIO، عليك فقط توسيع واجهة ZIO.App ووضعها في طريقة run، وسيتولى ZIO مهمة تشغيلها، هكذا:

 object ZIOApp extends zio . App {
 override def run (args: List [ String ]) = {
 ZIO //start 2 ZIO tasks in parallel
 .foreachPar(( 1 to 2 ))(zioTask)
 //complete program when done
 .as( ExitCode .success)
 }
 }

ونحصل على هذا الناتج – تكتمل المهام بشكل صحيح في 10 ثوانٍ:

 2020.09 .29 00 : 45 : 12 INFO zio- default - async -3 -1594199808 start -2
 2020.09 .29 00 : 45 : 12 INFO zio- default - async -2 -1594199808 start -1
 2020.09 .29 00 : 45 : 33 INFO zio- default - async -7 -1594199808 end -1
 2020.09 .29 00 : 45 : 33 INFO zio- default - async -8 -1594199808 end -2

النقاط الرئيسية المستفادة

  • تتطابق كل سلسلة تنفيذ في JVM مع سلسلة تنفيذ في نظام التشغيل، بطريقة واحد لواحد. وهذا هو جذر الكثير من المشاكل.
  • استخدام Thread.sleep أمر سيء! فهو يحظر سلسلة التنفيذ الحالية ويجعلها غير قابلة للاستخدام لأي عمل آخر.
  • سيعالج Project Loom (الذي سيكون متاحًا في JDK 17) الكثير من المشكلات من خلال سلاسل التنفيذ الافتراضية.
  • يمكنك استخدام ScheduledExecutorService لتحقيق وضع سكون غير حظر.
  • يمكنك استخدام البرمجة الوظيفية لنمذجة لغة يكون فيها وضع السكون غير حظر.
  • توفر مكتبة ZIO وضع سكون غير حظر جاهزًا للاستخدام (out of the box).

الخلاصة التقنية

في عالم البرمجة المتزامنة، يُعد فهم الآثار الجانبية للدوال مثل Thread.sleep أمرًا حيويًا. لقد أوضح هذا المقال كيف أن الاعتماد المباشر على Thread.sleep يؤدي إلى حظر سلاسل التنفيذ، مما يقلل من كفاءة استخدام الموارد ويحد من إمكانيات التزامن الحقيقي. بينما يقدم Project Loom حلاً جذريًا في المستقبل القريب من خلال سلاسل التنفيذ الافتراضية، فإن الحلول الحالية المتمثلة في استخدام ScheduledExecutorService أو تبني مكتبات البرمجة الوظيفية مثل ZIO توفر بدائل قوية وفعالة. إن تبني هذه الأساليب لا يقتصر على تجنب الحظر فحسب، بل يفتح الباب أمام تصميم تطبيقات أكثر مرونة وقابلية للتوسع، مما يعكس فهمًا أعمق لمبادئ البرمجة المتزامنة الحديثة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *