لماذا تفوقت Go على الجميع في التزامن؟
\n\nتخيل طاهياً رئيسياً يعد عشرة أطباق في وقت واحد. إنه لا يركض بين المواقد بسرعة جنونية - بل يقوم بالتبديل بكفاءة بين المهام: بينما يُطهى اللحم، يقطع السلطة؛ وبينما يغلي الماء، يُجهز الطبق. في عالم البرمجة، يُسمى هذا التزامن - القدرة على إدارة مهام متعددة دون انتظار اكتمال كل منها.
\n\nلقد وُلدت لغة Go (Golang) خصيصاً لحل مشاكل التزامن. لقد سئم مبتكروها من Google من تعقيدات تعدد الخيوط في C++ و Java. لقد ابتكروا الغوروتينات - خيوط خفيفة الوزن تُشغل بسطر واحد من الكود. في هذه المقالة، سنشرح كيفية عمل التزامن في Go، ونقارنه بخيوط نظام التشغيل ونموذج async/await من Python، والأهم من ذلك، سنعرض أمثلة كود حقيقية يمكنك تطبيقها اليوم.
\n\nالغوروتينات مقابل خيوط نظام التشغيل: معركة الخيالة الخفيفة
\n\nعندما يسمع المطور "تعدد الخيوط"، فإنه عادةً ما يتصور خيوط نظام التشغيل. لكن في Go، الأمر مختلف تماماً. دعنا نحلل الاختلافات الرئيسية.
\n\nما هو خيط نظام التشغيل (Thread)؟
\nالخيط هو كائن ثقيل يديره نواة نظام التشغيل. كل خيط له مكدس خاص به (عادةً 1-8 ميجابايت)، ويتطلب استدعاءات نظام لإنشائه والتبديل بينه. إنشاء 10,000 خيط يعني قتل الذاكرة والمعالج. في Go، هناك فلسفة مختلفة.
\n\nالغوروتين (Goroutine) - الخيط الخفيف في Go
\nالغوروتين هو دالة تُنفذ بشكل متزامن. يتم تشغيله باستخدام الكلمة المفتاحية go. الميزات الرئيسية:
- \n
- حجم المكدس: يبدأ من 2-4 كيلوبايت وينمو/ينكمش ديناميكياً. \n
- الإنشاء: يتم إنشاء آلاف الغوروتينات في أجزاء من الثانية. \n
- المجدول: بيئة تشغيل Go نفسها توزع الغوروتينات على خيوط نظام التشغيل (عادةً ما يكون عددها مساوياً لعدد أنوية المعالج). \n
- الصياغة: فقط أضف
goقبل استدعاء الدالة. \n
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc sayHello(name string) {\n for i := 0; i < 3; i++ {\n fmt.Println("مرحباً،", name)\n time.Sleep(100 * time.Millisecond)\n }\n}\n\nfunc main() {\n // تشغيل غوروتينين\n go sayHello("Go")\n go sayHello("Goroutine")\n\n // إعطاء وقت للغوروتينات للتنفيذ\n time.Sleep(1 * time.Second)\n fmt.Println("الدالة الرئيسية اكتملت")\n}\n\nالنتيجة: سيتم خلط الرسائل من الغوروتينين. Go يتبدل بينهما تلقائياً دون حظر بعضهما البعض. جرب تشغيل هذا الكود - سترى كيف تتم طباعة "مرحباً، Go" و "مرحباً، Goroutine" بالتناوب.
\n\nالقنوات: الأداة الرئيسية للمزامنة
\n\nالغوروتينات رائعة، لكن كيف تتواصل؟ في Go، لا توجد ذاكرة مشتركة افتراضياً. بدلاً من ذلك، يُستخدم المبدأ: "لا تتواصل عن طريق مشاركة الذاكرة؛ شارك الذاكرة عن طريق التواصل". لهذا الغرض، تُستخدم القنوات (channels).
\n\nالقناة هي "أنبوب" محدد النوع، تمر من خلاله الغوروتينات البيانات وترسلها وتستقبلها. هذا آمن وبسيط وأنيق.
\n\nمثال: معالجة متوازية للأرقام
\npackage main\n\nimport "fmt"\n\nfunc squareWorker(numbers <-chan int, results chan<- int) {\n for n := range numbers {\n results <- n * n\n }\n close(results)\n}\n\nfunc main() {\n numbers := make(chan int, 5)\n results := make(chan int, 5)\n\n // تشغيل العامل\n go squareWorker(numbers, results)\n\n // إرسال البيانات\n numbers <- 2\n numbers <- 4\n numbers <- 6\n close(numbers)\n\n // قراءة النتائج\n for res := range results {\n fmt.Println("المربع:", res)\n }\n}\n\nفي هذا المثال، نستخدم قنوات مخزنة مؤقتاً (الوسيطة الثانية هي حجم المخزن المؤقت). العامل الغوروتين يقرأ من القناة numbers، ويقوم بالتربيع، ويرسل النتيجة إلى القناة results. تقوم الدالة الرئيسية بإرسال الأرقام ثم قراءة النتائج.