القناة ميزة مدمجة مهمة في Go. وهي إحدى الميزات التي تجعل Go فريدة. وإلى جانب ميزة فريدة أخرى، goroutine، تجعل القناة البرمجة المتزامنة مريحة وممتعة وتخفض صعوبات البرمجة المتزامنة.
تعمل القناة أساساً كأسلوب مزامنة تزامن. سيسرد هذا المقال جميع المفاهيم والصيغ والقواعد المتعلقة بالقنوات. ولفهم القنوات بشكل أفضل، يُوصف أيضاً باختصار البنية الداخلية للقنوات وبعض تفاصيل التنفيذ لدى مصرِّف/وقت تشغيل Go القياسي.
قد تكون المعلومات في هذا المقال صعبة بعض الشيء على المبرمجين الجدد على Go. وقد تحتاج بعض أجزاء هذا المقال إلى قراءتها عدة مرات لفهمها فهماً كاملاً.
مقدمة عن القنوات
من التوصيات (التي قدّمها Rob Pike) في البرمجة المتزامنة: لا تجعل (الحسابات) تتواصل عبر مشاركة الذاكرة، بل (اجعلها) تتشارك الذاكرة عبر التواصل (عبر القنوات). (يمكننا اعتبار كل حساب بمثابة goroutine في برمجة Go.)
التواصل عبر مشاركة الذاكرة ومشاركة الذاكرة عبر التواصل أسلوبان للبرمجة في البرمجة المتزامنة. عندما تتواصل الـgoroutines عبر مشاركة الذاكرة، نستخدم أساليب مزامنة التزامن التقليدية، مثل أقفال mutex، لحماية الذاكرة المشتركة ومنع تسابقات البيانات. ويمكننا استخدام القنوات لتحقيق مشاركة الذاكرة عبر التواصل.
توفّر Go أسلوباً فريداً لمزامنة التزامن هو القناة. تجعل القنوات الـgoroutines تتشارك الذاكرة عبر التواصل. يمكننا اعتبار القناة طابور FIFO (الوارد أولاً يُخدم أولاً) داخلياً في البرنامج. ترسل بعض الـgoroutines قيماً إلى الطابور (القناة) وتستقبل goroutines أخرى قيماً منه.
إلى جانب نقل القيم (عبر القنوات)، قد تنتقل أيضاً ملكية بعض القيم بين الـgoroutines. عندما ترسل goroutine قيمة إلى قناة، يمكننا اعتبار أن الـgoroutine تتخلى عن ملكية بعض القيم (التي يمكن الوصول إليها عبر القيمة المُرسَلة). وعندما تستقبل goroutine قيمة من قناة، يمكننا اعتبار أنها تكتسب ملكية بعض القيم (التي يمكن الوصول إليها عبر القيمة المُستقبَلة).
بالطبع، قد لا تنتقل أي ملكية أيضاً مع اتصالات القنوات.
غالباً ما يُشار إلى القيم (التي تنتقل ملكيتها) عبر القيمة المنقولة (لكن لا يُشترط ذلك). يُرجى ملاحظة أننا حين نتحدث هنا عن الملكية فإننا نعني الملكية من منظور منطقي. يمكن للقنوات في Go أن تساعد المبرمجين على كتابة شيفرة خالية من تسابقات البيانات بسهولة، لكنها لا تستطيع منعهم من كتابة شيفرة متزامنة سيئة على مستوى الصيغة.
رغم أن Go تدعم أيضاً أساليب مزامنة التزامن التقليدية، فإن القناة وحدها مواطن من الدرجة الأولى في Go. القناة نوع من الأنواع في Go، لذا يمكننا استخدام القنوات دون استيراد أي حزم. في المقابل، تُقدَّم أساليب مزامنة التزامن التقليدية تلك في الحزمتين القياسيتين sync وsync/atomic.
بصراحة، لكل أسلوب من أساليب مزامنة التزامن أفضل سيناريوهات استخدامه. لكن القناة لها نطاق تطبيق أوسع وتنوّع أكبر في الاستخدام. ومن مشكلات القنوات أن تجربة البرمجة بها ممتعة ومبهجة لدرجة أن المبرمجين كثيراً ما يفضّلون استخدامها حتى في السيناريوهات التي لا تكون فيها الأفضل.
أنواع القنوات وقيمها
مثل المصفوفة والشريحة والخريطة، لكل نوع قناة نوع عنصر. ولا يمكن للقناة نقل إلا قيم نوع عنصرها.
يمكن أن تكون أنواع القنوات ثنائية الاتجاه أو أحادية الاتجاه. لنفترض أن T نوع اعتباطي،
chan Tيدل على نوع قناة ثنائية الاتجاه. تسمح المصرِّفات باستقبال قيم من القنوات ثنائية الاتجاه وإرسال قيم إليها معاً.chan<- Tيدل على نوع قناة للإرسال فقط. لا تسمح المصرِّفات باستقبال قيم من قنوات الإرسال فقط.<-chan Tيدل على نوع قناة للاستقبال فقط. لا تسمح المصرِّفات بإرسال قيم إلى قنوات الاستقبال فقط.
يُسمّى T نوع العنصر لهذه الأنواع من القنوات.
يمكن تحويل قيم نوع القناة ثنائية الاتجاه chan T ضمنياً إلى النوعين chan<- T (الإرسال فقط) و<-chan T (الاستقبال فقط)، لكن ليس العكس (حتى لو صراحةً). ولا يمكن تحويل قيم نوع الإرسال فقط chan<- T إلى نوع الاستقبال فقط <-chan T، والعكس صحيح. لاحظ أن علامتي <- في القيم الحرفية لأنواع القنوات مُعدِّلتان.
لكل قيمة قناة سعة، ستُشرح في القسم الذي يلي القسم التالي. تُسمّى قيمة القناة ذات السعة الصفرية قناة غير مخزّنة، وتُسمّى قيمة القناة ذات السعة غير الصفرية قناة مخزّنة.
تُمثَّل القيم الصفرية لأنواع القنوات بالمعرّف المعلن مسبقاً nil. ويجب إنشاء قيمة قناة غير nil باستخدام الدالة المدمجة make. على سبيل المثال، make(chan int, 10) تنشئ قناة نوع عنصرها int. ويحدّد المعطى الثاني في استدعاء الدالة make سعة القناة المُنشأة حديثاً. والمعطى الثاني اختياري وقيمته الافتراضية صفر.
مقارنات قيم القنوات
جميع أنواع القنوات أنواع قابلة للمقارنة.
من مقالة أجزاء القيمة، نعلم أن قيم القنوات غير nil قيم متعددة الأجزاء. وإذا أُسندت قيمة قناة إلى أخرى، تشارك القناتان الجزء (الأجزاء) الأساسي نفسه. وبعبارة أخرى، تمثّل القناتان الكائن القناة الداخلي نفسه. وتكون نتيجة مقارنتهما true.
عمليات القنوات
هناك خمس عمليات مخصّصة للقنوات. لنفترض أن القناة هي ch، تُدرج هنا صيغتها واستدعاءات الدوال الخاصة بها.
- أغلق القناة باستخدام استدعاء الدالة التالي ```go
close(ch)
حيث `close` دالة مدمجة. يجب أن يكون معطى استدعاء الدالة `close` قيمة قناة، ويجب ألا تكون القناة `ch` قناة استقبال فقط. 2. أرسل قيمة، `v`، إلى القناة باستخدام الصيغة التاليةgo
ch <- v
حيث يجب أن تكون `v` قيمة قابلة للإسناد إلى نوع عنصر القناة `ch`، ويجب ألا تكون القناة `ch` قناة استقبال فقط. لاحظ أن `<-` هنا معامل إرسال إلى قناة. 3. استقبل قيمة من القناة باستخدام الصيغة التاليةgo
<-ch
تُرجع عملية استقبال من قناة دائماً نتيجة واحدة على الأقل، وهي قيمة من نوع عنصر القناة، ويجب ألا تكون القناة `ch` قناة إرسال فقط. لاحظ أن `<-` هنا معامل استقبال من قناة. نعم، تمثيله هو نفسه تمثيل معامل الإرسال إلى قناة. وفي معظم السيناريوهات، تُعدّ عملية الاستقبال من قناة تعبيراً أحادي القيمة. لكن عندما تُستخدم عملية قناة كتعبير المصدر الوحيد في إسناد، يمكن أن تُنتج قيمة منطقية ثانية اختيارية غير محدّدة النوع وتصبح تعبيراً متعدد القيم. وتشير القيمة المنطقية غير محدّدة النوع إلى ما إذا كانت النتيجة الأولى قد أُرسلت قبل إغلاق القناة. (سنعلم أدناه أنه يمكننا استقبال عدد غير محدود من القيم من قناة مغلقة.) عمليتا استقبال من قناة تُستخدمان كقيم مصدر في إسنادات:go
v = <-ch v, sentBeforeClosed = <-ch
4. استعلم عن سعة مخزن القيم للقناة باستخدام استدعاء الدالة التالي ```go
cap(ch)
``` حيث `cap` دالة مدمجة قُدّمت في [الحاويات في Go](/article/fundamentals/container#cap-len). ونتيجة استدعاء الدالة `cap` قيمة من نوع `int`.
5. استعلم عن العدد الحالي للقيم في مخزن القيم (أو الطول) للقناة باستخدام استدعاء الدالة التالي ```go
len(ch)
``` حيث `len` دالة مدمجة قُدّمت سابقاً أيضاً. وقيمة إرجاع استدعاء الدالة `len` قيمة من نوع `int`. والطول الناتج هو عدد العناصر التي أُرسلت بنجاح إلى القناة المستعلَم عنها لكنها لم تُستقبل (لم تُؤخذ) بعد.
معظم العمليات الأساسية في Go غير متزامنة. بعبارة أخرى، ليست آمنة من التزامن. وتشمل هذه العمليات إسنادات القيم وتمرير المعطيات والتلاعب بعناصر الحاويات، إلخ. ومع ذلك، فإن جميع عمليات القنوات التي قُدّمت للتو متزامنة بالفعل، لذا لا حاجة إلى مزامنات إضافية لتنفيذها بأمان.
مثل معظم العمليات الأخرى في Go، إسنادات قيم القنوات غير متزامنة. وبالمثل، إسناد القيمة المُستقبَلة إلى قيمة أخرى غير متزامن أيضاً، رغم أن أي عملية استقبال من قناة متزامنة.
إذا كانت القناة المستعلَم عنها قناة nil، تُرجع كلتا الدالتين المدمجتين `cap` و`len` صفراً. وعمليتا الاستعلام بسيطتان لدرجة أنه لن تُقدَّم لهما شروح إضافية لاحقاً. وفي الواقع، نادراً ما تُستخدم العمليتان في الممارسة.
ستُشرح عمليات إرسال القناة واستقبالها وإغلاقها بالتفصيل في القسم التالي.
## شروح مفصّلة لعمليات القنوات
لجعل شروح عمليات القنوات بسيطة وواضحة، ستُصنَّف القنوات في ما تبقّى من هذا المقال إلى ثلاث فئات:
1. قنوات nil.
2. قنوات غير nil لكن مغلقة.
3. قنوات غير nil وغير مغلقة.
يلخّص الجدول التالي ببساطة سلوكيات جميع أنواع العمليات المطبَّقة على القنوات nil والمغلقة وغير المغلقة غير nil.
| العملية | قناة nil | قناة مغلقة | قناة غير مغلقة وغير nil |
| --- | --- | --- | --- |
| الإغلاق | panic | panic | نجاح الإغلاق <sup>(C)</sup> |
| إرسال قيمة إلى | الحجب إلى الأبد | panic | الحجب أو نجاح الإرسال <sup>(B)</sup> |
| استقبال قيمة من | الحجب إلى الأبد | عدم الحجب أبداً <sup>(D)</sup> | الحجب أو نجاح الاستقبال <sup>(A)</sup> |
بالنسبة إلى الحالات الخمس المعروضة دون أحرف علوية، فإن السلوكيات واضحة جداً.
1. إغلاق قناة nil أو قناة مغلقة بالفعل ينتج panic في الـgoroutine الحالية.
2. إرسال قيمة إلى قناة مغلقة ينتج أيضاً panic في الـgoroutine الحالية.
3. إرسال قيمة إلى قناة nil أو استقبال قيمة منها يجعل الـgoroutine الحالية تدخل حالة الحجب وتبقى فيها إلى الأبد.
وفي ما يلي شروح إضافية للحالات الأربع المعروضة بأحرف علوية (A وB وC وD).
لفهم أنواع القنوات وقيمها بشكل أفضل، ولتسهيل بعض الشروح، من المفيد جداً النظر في البنى الداخلية الخام لكائنات القنوات الداخلية.
يمكننا تصوّر أن كل قناة تتكوّن داخلياً من ثلاثة طوابير (كلها يمكن اعتبارها طوابير FIFO):
1. طابور الـgoroutines المستقبِلة (FIFO عموماً). الطابور قائمة مترابطة بلا حد للحجم. جميع الـgoroutines في هذا الطابور في حالة الحجب وتنتظر استقبال قيم من تلك القناة.
2. طابور الـgoroutines المرسِلة (FIFO عموماً). الطابور أيضاً قائمة مترابطة بلا حد للحجم. جميع الـgoroutines في هذا الطابور في حالة الحجب وتنتظر إرسال قيم إلى تلك القناة. وتُخزَّن أيضاً القيمة (أو عنوان القيمة، حسب تنفيذ المصرِّف) التي تحاول كل goroutine إرسالها في الطابور مع تلك الـgoroutine.
3. طابور مخزن القيم (FIFO قطعاً). هذا طابور دائري. وحجمه يساوي سعة القناة. وأنواع القيم المخزّنة في طابور المخزن هذا كلها نوع عنصر تلك القناة. وإذا بلغ العدد الحالي للقيم المخزّنة في طابور مخزن القيم للقناة سعة القناة، تُسمّى القناة في حالة الامتلاء. وإذا لم تُخزَّن أي قيم في طابور مخزن القيم للقناة حالياً، تُسمّى القناة في حالة الفراغ. وبالنسبة إلى قناة ذات سعة صفرية (غير مخزّنة)، فهي دائماً في حالتي الامتلاء والفراغ معاً.
تحمل كل قناة داخلياً قفل mutex يُستخدم لتجنّب تسابقات البيانات في جميع أنواع العمليات.
حالة عملية القناة A: عندما تحاول goroutine `R` استقبال قيمة من قناة غير nil وغير مغلقة، تكتسب الـgoroutine `R` أولاً القفل المرتبط بالقناة، ثم تنفّذ الخطوات التالية حتى يتحقق شرط واحد.
1. إذا لم يكن طابور مخزن القيم للقناة فارغاً، وفي هذه الحالة يجب أن يكون طابور الـgoroutines المستقبِلة للقناة فارغاً، تستقبل الـgoroutine `R` (بالسحب) قيمة من طابور مخزن القيم. وإذا لم يكن طابور الـgoroutines المرسِلة للقناة فارغاً أيضاً، تُسحب goroutine مرسِلة من طابور الـgoroutines المرسِلة وتُستأنف إلى حالة التشغيل مجدداً. وتُدفع القيمة التي تحاول الـgoroutine المرسِلة المسحوبة للتو إرسالها إلى طابور مخزن القيم للقناة. وتواصل الـgoroutine المستقبِلة `R` التشغيل. ولهذا السيناريو، تُسمّى عملية استقبال القناة عملية غير حاجبة.
2. وإلا (أي إذا كان طابور مخزن القيم للقناة فارغاً)، إذا لم يكن طابور الـgoroutines المرسِلة للقناة فارغاً، وفي هذه الحالة يجب أن تكون القناة غير مخزّنة، تسحب الـgoroutine المستقبِلة `R` goroutine مرسِلة من طابور الـgoroutines المرسِلة للقناة وتستقبل القيمة التي تحاول الـgoroutine المرسِلة المسحوبة للتو إرسالها. وتُفكّ حجب الـgoroutine المرسِلة المسحوبة للتو وتُستأنف إلى حالة التشغيل مجدداً. وتواصل الـgoroutine المستقبِلة `R` التشغيل. ولهذا السيناريو، تُسمّى عملية استقبال القناة عملية غير حاجبة.
3. إذا كان طابور مخزن القيم وطابور الـgoroutines المرسِلة للقناة فارغين معاً، تُدفع الـgoroutine `R` إلى طابور الـgoroutines المستقبِلة للقناة وتدخل حالة الحجب (وتبقى فيها). وقد تُستأنف إلى حالة التشغيل عندما ترسل goroutine أخرى قيمة إلى القناة لاحقاً. ولهذا السيناريو، تُسمّى عملية استقبال القناة عملية حاجبة.
حالة قاعدة القناة B: عندما تحاول goroutine `S` إرسال قيمة إلى قناة غير nil وغير مغلقة، تكتسب الـgoroutine `S` أولاً القفل المرتبط بالقناة، ثم تنفّذ الخطوات التالية حتى يتحقق شرط خطوة واحدة.
1. إذا لم يكن طابور الـgoroutines المستقبِلة للقناة فارغاً، وفي هذه الحالة يجب أن يكون طابور مخزن القيم للقناة فارغاً، تسحب الـgoroutine المرسِلة `S` goroutine مستقبِلة من طابور الـgoroutines المستقبِلة للقناة وترسل القيمة إلى الـgoroutine المستقبِلة المسحوبة للتو. وتُفكّ حجب الـgoroutine المستقبِلة المسحوبة للتو وتُستأنف إلى حالة التشغيل مجدداً. وتواصل الـgoroutine المرسِلة `S` التشغيل. ولهذا السيناريو، تُسمّى عملية إرسال القناة عملية غير حاجبة.
2. وإلا (أي إذا كان طابور الـgoroutines المستقبِلة فارغاً)، إذا لم يكن طابور مخزن القيم للقناة ممتلئاً، وفي هذه الحالة يجب أن يكون طابور الـgoroutines المرسِلة فارغاً أيضاً، تُدفع القيمة التي تحاول الـgoroutine المرسِلة `S` إرسالها إلى طابور مخزن القيم، وتواصل الـgoroutine المرسِلة `S` التشغيل. ولهذا السيناريو، تُسمّى عملية إرسال القناة عملية غير حاجبة.
3. إذا كان طابور الـgoroutines المستقبِلة فارغاً وطابور مخزن القيم للقناة ممتلئاً بالفعل، تُدفع الـgoroutine المرسِلة `S` إلى طابور الـgoroutines المرسِلة للقناة وتدخل حالة الحجب (وتبقى فيها). وقد تُستأنف إلى حالة التشغيل عندما تستقبل goroutine أخرى قيمة من القناة لاحقاً. ولهذا السيناريو، تُسمّى عملية إرسال القناة عملية حاجبة.
ذُكر أعلاه أنه بمجرد إغلاق قناة غير nil، سيُنتج إرسال قيمة إلى القناة panic في وقت التشغيل في الـgoroutine الحالية. لاحظ أن إرسال بيانات إلى قناة مغلقة يُعدّ عملية غير حاجبة.
حالة عملية القناة C: عندما تحاول goroutine إغلاق قناة غير nil وغير مغلقة، وبمجرد أن تكتسب الـgoroutine قفل القناة، تُنفَّذ الخطوتان التاليتان بالترتيب التالي.
1. إذا لم يكن طابور الـgoroutines المستقبِلة للقناة فارغاً، وفي هذه الحالة يجب أن يكون مخزن القيم للقناة فارغاً، تُسحب جميع الـgoroutines في طابور الـgoroutines المستقبِلة للقناة واحدة تلو الأخرى، وتستقبل كل منها قيمة صفرية من نوع عنصر القناة وتُستأنف إلى حالة التشغيل.
2. إذا لم يكن طابور الـgoroutines المرسِلة للقناة فارغاً، تُسحب جميع الـgoroutines في طابور الـgoroutines المرسِلة للقناة واحدة تلو الأخرى وتُنتج كل منها panic بسبب الإرسال إلى قناة مغلقة. وهذا هو سبب وجوب تجنّب عمليتي الإرسال والإغلاق المتزامنتين على القناة نفسها. في الواقع، عند تمكين [خيار كاشف تسابق البيانات في أمر go](https://golang.org/doc/articles/race_detector.html) (`-race`)، قد تُكتشف حالات الإرسال والإغلاق المتزامنين في وقت التشغيل ويُطرح panic في وقت التشغيل.
ملاحظة: بعد إغلاق قناة، تبقى القيم التي دُفعت بالفعل إلى مخزن قيم القناة موجودة. يُرجى قراءة الشروح التالية القريبة للحالة D للتفاصيل.
حالة عملية القناة D: بعد إغلاق قناة غير nil، لن تحجب عمليات استقبال القناة على القناة أبداً. ويمكن استقبال القيم الموجودة في مخزن قيم القناة رغم ذلك. وتبقى قيم الإرجاع المنطقية الثانية الاختيارية المصاحبة `true`. وبمجرد أخذ جميع القيم في مخزن القيم واستقبالها، ستُستقبل قيم صفرية لا نهائية من نوع عنصر القناة عبر أي من عمليات الاستقبال التالية على القناة. وكما ذُكر أعلاه، نتيجة الإرجاع الثانية الاختيارية لعملية استقبال من قناة قيمة منطقية غير محدّدة النوع تشير إلى ما إذا كانت النتيجة الأولى (القيمة المُستقبَلة) قد أُرسلت قبل إغلاق القناة. وإذا كانت نتيجة الإرجاع الثانية `false`، فيجب أن تكون نتيجة الإرجاع الأولى (القيمة المُستقبَلة) قيمة صفرية من نوع عنصر القناة.
معرفة ما هي عمليات إرسال أو استقبال القناة الحاجبة وغير الحاجبة مهمة لفهم آلية كتل تدفّق التحكم `select` التي ستُقدَّم في قسم لاحق.
في الشروح أعلاه، إذا سُحبت goroutine من طابور (سواء طابور الـgoroutines المرسِلة أو المستقبِلة) لقناة، وكانت الـgoroutine قد حُجبت بسبب دفعها إلى الطابور عند [كتلة شيفرة تدفّق التحكم `select`](#select)، فستُستأنف الـgoroutine إلى حالة التشغيل عند الخطوة 9 من [تنفيذ كتلة شيفرة تدفّق التحكم `select`](#select-implementation). وقد تُخرَج من طابور الـgoroutines المقابل لعدة قنوات مشاركة في كتلة شيفرة تدفّق التحكم `select`.
وفقاً للشروح المذكورة أعلاه، يمكننا استنتاج بعض الحقائق عن الطوابير الداخلية للقناة.
1. إذا كانت القناة مغلقة، فيجب أن يكون طابورا الـgoroutines المرسِلة والمستقبِلة فيها فارغين، لكن طابور مخزن القيم قد لا يكون فارغاً.
2. في أي وقت، إذا لم يكن مخزن القيم فارغاً، فيجب أن يكون طابور الـgoroutines المستقبِلة فارغاً.
3. في أي وقت، إذا لم يكن مخزن القيم ممتلئاً، فيجب أن يكون طابور الـgoroutines المرسِلة فارغاً.
4. إذا كانت القناة مخزّنة، ففي أي وقت يجب أن يكون أحد طابوري الـgoroutines للقناة على الأقل فارغاً (المرسِلة أو المستقبِلة أو كلاهما).
5. إذا كانت القناة غير مخزّنة، فيجب في معظم الأوقات أن يكون أحد طابورَي الـgoroutines المرسِلة والمستقبِلة فارغاً، باستثناء واحد. الاستثناء هو أن goroutine قد تُدفع إلى الطابورين معاً عند تنفيذ [كتلة شيفرة تدفّق التحكم `select`](#select).
## بعض أمثلة استخدام القنوات
الآن بعد أن قرأت القسم أعلاه، لنطّلع على بعض الأمثلة التي تستخدم القنوات لتعزيز فهمك.
مثال بسيط لطلب/استجابة. تتحدث الـgoroutines في هذا المثال إحداهما إلى الأخرى عبر قناة غير مخزّنة.
```go
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // قناة غير مخزّنة
go func(ch chan<- int, x int) {
time.Sleep(time.Second)
// <-ch // يفشل التصريف
// أرسل القيمة واحجب حتى يُستقبل الناتج.
ch <- x*x // يُرسل 9
}(c, 3)
done := make(chan struct{})
go func(ch <-chan int) {
// احجب حتى يُستقبل 9.
n := <-ch
fmt.Println(n) // 9
// ch <- 123 // يفشل التصريف
time.Sleep(time.Second)
done <- struct{}{}
}(c)
// احجب هنا حتى تستقبل القناة "done"
// قيمة.
<-done
fmt.Println("bye")
}
الناتج:
9
bye
عرض توضيحي لاستخدام قناة مخزّنة. هذا البرنامج ليس متزامناً، بل يوضّح فقط كيفية استخدام القنوات المخزّنة.
package main
import "fmt"
func main() {
c := make(chan int, 2) // قناة مخزّنة
c <- 3
c <- 5
close(c)
fmt.Println(len(c), cap(c)) // 2 2
x, ok := <-c
fmt.Println(x, ok) // 3 true
fmt.Println(len(c), cap(c)) // 1 2
x, ok = <-c
fmt.Println(x, ok) // 5 true
fmt.Println(len(c), cap(c)) // 0 2
x, ok = <-c
fmt.Println(x, ok) // 0 false
x, ok = <-c
fmt.Println(x, ok) // 0 false
fmt.Println(len(c), cap(c)) // 0 2
close(c) // panic!
// سيتسبب الإرسال أيضاً في panic إذا
// أُزيل استدعاء close أعلاه.
c <- 7
}
مباراة كرة قدم لا تنتهي أبداً.
package main
import (
"fmt"
"time"
)
func main() {
var ball = make(chan string)
kickBall := func(playerName string) {
for {
fmt.Println(<-ball, "kicked the ball.")
time.Sleep(time.Second)
ball <- playerName
}
}
go kickBall("John")
go kickBall("Alice")
go kickBall("Bob")
go kickBall("Emily")
ball <- "referee" // ضربة البداية
var c chan bool // nil
<-c // الحجب هنا إلى الأبد
}
يُرجى قراءة حالات استخدام القنوات لمزيد من أمثلة استخدام القنوات.
تُنقل قيم عناصر القناة بالنسخ
عندما تُنقل قيمة من goroutine إلى أخرى، تُنسخ القيمة مرة واحدة على الأقل. وإذا مكثت القيمة المنقولة في مخزن قيم قناة، فستحدث نسختان في عملية النقل. تحدث نسخة عندما تُنسخ القيمة من الـgoroutine المرسِلة إلى مخزن القيم، وتحدث الأخرى عندما تُنسخ القيمة من مخزن القيم إلى الـgoroutine المستقبِلة. ومثل إسنادات القيم وتمرير معطيات الدوال، عندما تُنقل قيمة، يُنسخ جزءها المباشر فقط.
بالنسبة إلى مصرِّف Go القياسي، يجب أن يكون حجم أنواع عناصر القناة أصغر من 65536. ومع ذلك، عموماً، لا ينبغي إنشاء قنوات بأنواع عناصر كبيرة الحجم، لتجنّب كلفة نسخ كبيرة جداً في عملية نقل القيم بين الـgoroutines. لذا إذا كان حجم القيمة المنقولة كبيراً جداً، فمن الأفضل استخدام نوع عنصر مؤشري بدلاً من ذلك، لتجنّب كلفة نسخ قيمة كبيرة.
عن جمع النفايات للقنوات والـgoroutines
لاحظ أن القناة مُشار إليها من جميع الـgoroutines في طابور الـgoroutines المرسِلة أو المستقبِلة للقناة، لذا إذا لم يكن أي من طابوري القناة فارغاً، فلا يمكن جمع القناة كنفايات. وفي المقابل، إذا كانت goroutine محجوبة وباقية في طابور الـgoroutines المرسِلة أو المستقبِلة لقناة، فلا يمكن جمع الـgoroutine كنفايات أيضاً، حتى لو كانت القناة مُشاراً إليها من هذه الـgoroutine وحدها. في الواقع، لا يمكن جمع goroutine كنفايات إلا بعد خروجها بالفعل.
عمليات إرسال القناة واستقبالها جمل بسيطة
عمليات إرسال القناة وعمليات استقبالها جمل بسيطة. ويمكن دائماً استخدام عملية استقبال من قناة كتعبير أحادي القيمة. ويمكن استخدام الجمل البسيطة والتعبيرات في مواضع معينة من كتل تدفّق التحكم الأساسية.
مثال تظهر فيه عمليتا إرسال القناة واستقبالها كجمل بسيطة في كتلتي تدفّق تحكم for.
package main
import (
"fmt"
"time"
)
func main() {
fibonacci := func() chan uint64 {
c := make(chan uint64)
go func() {
var x, y uint64 = 0, 1
for ; y < (1 << 63); c <- y { // هنا
x, y = y, x+y
}
close(c)
}()
return c
}
c := fibonacci()
for x, ok := <-c; ok; x, ok = <-c { // هنا
time.Sleep(time.Second)
fmt.Println(x)
}
}
for-range على القنوات
تنطبق كتلة شيفرة تدفّق التحكم for-range على القنوات. وستحاول الحلقة استقبال القيم المُرسَلة إلى قناة تكرارياً، حتى تُغلق القناة ويصبح طابور مخزن قيمها خالياً. ومع صيغة for-range على المصفوفات والشرائح والخرائط، يُسمح بعدة متغيرات تكرار. لكن بالنسبة إلى كتل for-range المطبَّقة على القنوات، يمكنك استخدام متغير تكرار واحد على الأكثر، يُستخدم لتخزين القيم المُستقبَلة.
for v := range aChannel {
// استخدم v
}
يكافئ
for {
v, ok = <-aChannel
if !ok {
break
}
// استخدم v
}
بالطبع، يجب هنا ألا تكون قيمة aChannel قناة إرسال فقط. وإذا كانت قناة nil، فستحجب الحلقة هناك إلى الأبد.
على سبيل المثال، يمكن تبسيط كتلة الحلقة for الثانية في المثال المعروض في القسم الأخير إلى
for x := range c {
time.Sleep(time.Second)
fmt.Println(x)
}
كتل شيفرة تدفّق التحكم select-case
توجد صيغة كتلة شيفرة select-case مصمّمة خصيصاً للقنوات. والصيغة تشبه كثيراً صيغة كتلة switch-case. على سبيل المثال، يمكن أن توجد عدة فروع case وفرع default واحد على الأكثر في كتلة شيفرة select-case. لكن توجد أيضاً بعض الاختلافات الواضحة بينهما.
- لا يُسمح بأي تعبيرات أو جمل بعد الكلمة المفتاحية
select(قبل{). - لا يُسمح باستخدام جمل
fallthroughفي فروعcase. - يجب أن تكون كل جملة تلي الكلمة المفتاحية
caseفي كتلة شيفرةselect-caseإما عملية استقبال من قناة أو جملة عملية إرسال إلى قناة. ويمكن أن تظهر عملية استقبال من قناة كقيمة مصدر في جملة إسناد بسيطة. ولاحقاً، ستُسمّى عملية القناة التي تلي الكلمة المفتاحيةcaseعمليةcase. - إذا وُجدت عملية
caseواحدة غير حاجبة أو أكثر، سيختار وقت تشغيل Go عشوائياً إحدى هذه العمليات غير الحاجبة لتنفيذها، ثم يواصل تنفيذ فرعcaseالمقابل. - إذا كانت جميع عمليات
caseفي كتلة شيفرةselect-caseعمليات حاجبة، سيُختار فرعdefaultللتنفيذ إذا كان فرعdefaultموجوداً. وإذا كان فرعdefaultغائباً، تُدفع الـgoroutine الحالية إلى طابور الـgoroutines المرسِلة أو المستقبِلة المقابل لكل قناة مشاركة في جميع عملياتcase، ثم تدخل حالة الحجب.
بحسب القواعد، كتلة شيفرة select-case بلا أي فروع، select{}، ستجعل الـgoroutine الحالية تبقى في حالة الحجب إلى الأبد.
سيدخل البرنامج التالي فرع default بالتأكيد.
package main
import "fmt"
func main() {
var c chan struct{} // nil
select {
case <-c: // عملية حاجبة
case c <- struct{}{}: // عملية حاجبة
default:
fmt.Println("Go here.")
}
}
مثال يوضّح كيفية استخدام محاولة الإرسال ومحاولة الاستقبال:
package main
import "fmt"
func main() {
c := make(chan string, 2)
trySend := func(v string) {
select {
case c <- v:
default: // اذهب هنا إذا امتلأت c.
}
}
tryReceive := func() string {
select {
case v := <-c: return v
default: return "-" // اذهب هنا إذا كانت c فارغة
}
}
trySend("Hello!") // نجح الإرسال
trySend("Hi!") // نجح الإرسال
// يفشل الإرسال، لكنه لن يحجب.
trySend("Bye!")
// سينجح السطران التاليان
// في الاستقبال.
fmt.Println(tryReceive()) // Hello!
fmt.Println(tryReceive()) // Hi!
// يفشل السطر التالي في الاستقبال.
fmt.Println(tryReceive()) // -
}
المثال التالي فيه احتمال 50% لحدوث panic. وعمليتا case كلتاهما غير حاجبة في هذا المثال.
package main
func main() {
c := make(chan struct{})
close(c)
select {
case c <- struct{}{}:
// يحدث panic إذا اختيرت الحالة الأولى.
case <-c:
}
}
تنفيذ آلية select
آلية select في Go ميزة مهمة وفريدة. وتُدرج هنا خطوات تنفيذ آلية select في وقت تشغيل Go الرسمي.
هناك عدة خطوات لتنفيذ كتلة select-case:
- تقييم جميع تعبيرات القنوات وتعبيرات القيم المشاركة التي قد تُرسَل في عمليات
case، من الأعلى إلى الأسفل ومن اليسار إلى اليمين. ولا حاجة إلى تقييم قيم الوجهة لعمليات الاستقبال (كقيم مصدر) في الإسنادات في هذه اللحظة. - عشوائية ترتيب الفروع للاستطلاع في الخطوة 5. ويُوضع فرع
defaultدائماً في الموضع الأخير في الترتيب الناتج. وقد تتكرر القنوات في عملياتcase. - ترتيب جميع القنوات المشاركة في عمليات
caseلتجنّب الجمود (مع goroutines أخرى) في الخطوة التالية. ولا تبقى قنوات مكرّرة في أولNقناة من الترتيب الناتج، حيثNهو عدد القنوات المشاركة في عملياتcase. وفي ما يلي، ترتيب أقفال القنوات مفهوم يشير إلى أولNقناة في الترتيب الناتج. - قفل (أي اكتساب أقفال) جميع القنوات المشاركة بترتيب أقفال القنوات الناتج في الخطوة الأخيرة.
- استطلاع كل فرع في كتلة select بالترتيب العشوائي الناتج في الخطوة 2: 1. إذا كان هذا فرع
caseوكانت عملية القناة المقابلة عملية إرسال قيمة إلى قناة مغلقة، فاكشف أقفال جميع القنوات بترتيب أقفال القنوات العكسي واجعل الـgoroutine الحالية تحدث panic. انتقل إلى الخطوة 12. - إذا كان هذا فرع
caseوكانت عملية القناة المقابلة غير حاجبة، فنفّذ عملية القناة واكشف أقفال جميع القنوات بترتيب أقفال القنوات العكسي، ثم نفّذ جسم فرعcaseالمقابل. وقد توقظ عملية القناة goroutine أخرى في حالة الحجب. انتقل إلى الخطوة 12. - إذا كان هذا فرع
default، فاكشف أقفال جميع القنوات بترتيب أقفال القنوات العكسي ونفّذ جسم فرعdefault. انتقل إلى الخطوة 12. (حتى هنا، يكون فرعdefaultغائباً وجميع عملياتcaseعمليات حاجبة.) - دفع (إدخال) الـgoroutine الحالية (مع معلومات فرع
caseالمقابل) إلى طابور الـgoroutines المستقبِلة أو المرسِلة للقناة المشاركة في كل عمليةcase. وقد تُدفع الـgoroutine الحالية إلى طوابير قناة عدة مرات، لأن القنوات المشاركة في حالات متعددة قد تكون القناة نفسها. - جعل الـgoroutine الحالية تدخل حالة الحجب وكشف أقفال جميع القنوات بترتيب أقفال القنوات العكسي.
- الانتظار في حالة الحجب حتى توقظ عمليات قنوات أخرى الـgoroutine الحالية، ...
- تُوقظ الـgoroutine الحالية بعملية قناة أخرى في goroutine أخرى. وقد تكون العملية الأخرى عملية إغلاق قناة أو عملية إرسال/استقبال قناة. وإذا كانت عملية إرسال/استقبال قناة، فلا بد من وجود عملية استقبال/إرسال
caseللقناة (في كتلةselect-caseالجاري شرحها) تتعاون معها (بنقل قيمة). وفي التعاون، تُخرَج الـgoroutine الحالية من طابور الـgoroutines المستقبِلة/المرسِلة للقناة. - قفل جميع القنوات المشاركة بترتيب أقفال القنوات.
- إخراج الـgoroutine الحالية من طابور الـgoroutines المستقبِلة أو المرسِلة للقناة المشاركة في كل عملية
case، 1. إذا أُوقظت الـgoroutine الحالية بعملية إغلاق قناة، فانتقل إلى الخطوة 5. - إذا أُوقظت الـgoroutine الحالية بعملية إرسال/استقبال قناة، فقد وُجد فرع
caseالمقابل لعملية الاستقبال/الإرسال المتعاونة بالفعل في عملية الإخراج، لذا اكتشف فقط أقفال جميع القنوات بترتيب أقفال القنوات العكسي ونفّذ فرعcaseالمقابل. - انتهى.
من التنفيذ، نعلم أن
- قد تبقى goroutine في طوابير الـgoroutines المرسِلة وطوابير الـgoroutines المستقبِلة لعدة قنوات في الوقت نفسه. بل يمكنها أن تبقى في طابور الـgoroutines المرسِلة وطابور الـgoroutines المستقبِلة للقناة نفسها في الوقت نفسه.
- عندما تُستأنف goroutine محجوبة حالياً عند كتلة شيفرة
select-caseلاحقاً، ستُزال من جميع طوابير الـgoroutines المرسِلة وطوابير الـgoroutines المستقبِلة لجميع القنوات المشاركة في عمليات القنوات التي تلي الكلمات المفتاحيةcaseفي كتلة شيفرةselect-case.
المزيد
يمكننا إيجاد المزيد من حالات استخدام القنوات في هذا المقال.
رغم أن القنوات يمكن أن تساعدنا على كتابة شيفرة متزامنة صحيحة بسهولة، فإنها، مثل أساليب مزامنة البيانات الأخرى، لن تمنعنا من كتابة شيفرة متزامنة غير سليمة.
قد لا تكون القناة دائماً أفضل حل لجميع حالات استخدام مزامنة البيانات. يُرجى قراءة هذا المقال وهذا المقال لمزيد من أساليب المزامنة في Go.