القنوات المخزنة (Buffered) مقابل غير المخزنة
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 61 من 107.
القنوات التي استخدمناها حتى الآن غير مخزنة—ليس لديها سعة للاحتفاظ بالقيم. عملية الإرسال تتوقف (blocks) حتى يقوم goroutine آخر بالاستقبال، والعكس صحيح. القنوات المخزنة تضيف مساحة تخزين داخلية، مما يسمح لعمليات الإرسال بالاكتمال دون وجود مستقبل فوري.
أنشئ قناة مخزنة (buffered channel) عن طريق تمرير سعة إلى make:
// غير مخزن مؤقتًا - السعة 0
unbuffered := make(chan int)
// مخزن مؤقتًا - السعة 3
buffered := make(chan int, 3)في القنوات ذات التخزين المؤقت (buffered channel)، تتوقف عمليات الإرسال فقط عندما يكون التخزين المؤقت ممتلئاً، وتتوقف عمليات الاستقبال فقط عندما يكون التخزين المؤقت فارغاً:
ch := make(chan string, 2)
ch <- "first" // لا يحظر - المخزن المؤقت به مساحة
ch <- "second" // لا يحظر - المخزن المؤقت به مساحة
// ch <- "third" سيحظر - المخزن المؤقت ممتلئ
fmt.Println(<-ch) // "first"
fmt.Println(<-ch) // "second"استخدم len(ch) للتحقق من عدد العناصر الموجودة حاليًا في الذاكرة المؤقتة (buffer)، و cap(ch) للسعة الإجمالية.
متى تستخدم كل منهما: توفر القنوات غير المخزنة (Unbuffered channels) مزامنة وثيقة—حيث يعرف المرسل أن المستقبل قد تلقى القيمة. أما القنوات المخزنة (Buffered channels) فتقوم بفصل توقيت المرسل والمستقبل، وهي مفيدة عندما يعمل المنتجون والمستهلكون بسرعات مختلفة. ومع ذلك، يمكن للقنوات المخزنة أن تخفي أخطاء المزامنة، لذا يفضل استخدام القنوات غير المخزنة ما لم يكن لديك سبب محدد لاستخدام التخزين المؤقت.
التحدي
سهللنقم ببناء معالج دفعات (batch processor) يوضح الفرق بين القنوات المخزنة مؤقتاً (buffered channels) وغير المخزنة مؤقتاً. ستقوم بإنشاء نظام حيث يرسل المنتج (producer) عناصر إلى المعالج، باستخدام القنوات المخزنة مؤقتاً لفصل توقيت عملهما والسماح للمنتج بالعمل مسبقاً.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
processor.go: حدد منطق معالجة الدفعات باستخدام القنوات.قم بإنشاء هيكل
Itemيحتوي على حقولID(int) وValue(string).قم بتنفيذ دالتين:
Producer(items []Item, out chan Item)- ترسل كل عنصر إلى قناة الإخراج. بعد إرسال كل عنصر، اطبع:Produced item [ID]. بعد إرسال جميع العناصر، اطبع العدد الحالي للعناصر المنتظرة في المخزن المؤقت باستخدامlen(out)بالتنسيق التالي:Buffer has [count] items. ثم أغلق القناة.Consumer(in chan Item) []string- تستقبل العناصر من قناة الإدخال وتجمعها في شريحة (slice) من السلاسل النصية المنسقة. لكل عنصر مستلم، يجب أن تكون السلسلة النصية:Consumed: [ID] - [Value]. أعد الشريحة عند إغلاق القناة.
main.go: قم بإعداد القناة المخزنة مؤقتاً وتنسيق عمل المنتج والمستهلك.اقرأ سعة المخزن المؤقت (buffer capacity)، ثم عدد العناصر، متبوعاً بمعرف (ID) وقيمة كل عنصر. أنشئ قناة مخزنة مؤقتاً بالسعة المحددة. قم بتشغيل Producer أولاً (ليس كـ goroutine) لملء المخزن المؤقت، ثم قم بتشغيل Consumer لمعالجة جميع العناصر. اطبع كل نتيجة مستهلكة في سطر منفصل.
سيتم توفير المدخلات التالية:
- السطر 1: سعة المخزن المؤقت (عدد صحيح)
- السطر 2: عدد العناصر (عدد صحيح)
- الأسطر التالية: لكل عنصر، سطران - معرف العنصر (عدد صحيح)، ثم قيمته (سلسلة نصية)
على سبيل المثال، إذا كان المدخل:
3
3
1
apple
2
banana
3
cherryيجب أن يكون مخرجك:
Produced item 1
Produced item 2
Produced item 3
Buffer has 3 items
Consumed: 1 - apple
Consumed: 2 - banana
Consumed: 3 - cherryلاحظ كيف أنه مع سعة مخزن مؤقت تبلغ 3، يمكن للمنتج إرسال جميع العناصر الثلاثة دون توقف (blocking)، ويظهر المخزن المؤقت 3 عناصر منتظرة قبل أن يبدأ المستهلك. إذا كان المخزن المؤقت أصغر من عدد العناصر، فسيتم إيقاف المنتج مؤقتاً بانتظار توفر مساحة.
ورقة مرجعية
يمكن أن تكون القنوات (Channels) في لغة Go غير مخزنة مؤقتاً (سعة 0) أو مخزنة مؤقتاً (مع تخزين داخلي).
قم بإنشاء القنوات باستخدام make:
// غير مخزنة مؤقتاً - السعة 0
unbuffered := make(chan int)
// مخزنة مؤقتاً - السعة 3
buffered := make(chan int, 3)سلوك القنوات المخزنة مؤقتاً:
- عمليات الإرسال تُحظر فقط عندما يكون المخزن المؤقت ممتلئاً
- عمليات الاستقبال تُحظر فقط عندما يكون المخزن المؤقت فارغاً
ch := make(chan string, 2)
ch <- "first" // لا يحظر - المخزن المؤقت به مساحة
ch <- "second" // لا يحظر - المخزن المؤقت به مساحة
// ch <- "third" would block - buffer is full // سيحظر - المخزن المؤقت ممتلئ
fmt.Println(<-ch) // "first"
fmt.Println(<-ch) // "second"التحقق من حالة المخزن المؤقت:
len(ch)- عدد العناصر الموجودة حالياً في المخزن المؤقتcap(ch)- السعة الإجمالية للمخزن المؤقت
متى تستخدم:
- غير مخزنة مؤقتاً: مزامنة دقيقة — يعرف المرسل أن المستقبل قد استلم القيمة
- مخزنة مؤقتاً: فصل توقيت المرسل والمستقبل عندما يعملان بسرعات مختلفة
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة سعة المخزن المؤقت
line, _ := reader.ReadString('\n')
bufferCapacity, _ := strconv.Atoi(strings.TrimSpace(line))
// قراءة عدد العناصر
line, _ = reader.ReadString('\n')
numItems, _ := strconv.Atoi(strings.TrimSpace(line))
// قراءة معرف وقيمة كل عنصر
items := make([]Item, numItems)
for i := 0; i < numItems; i++ {
line, _ = reader.ReadString('\n')
id, _ := strconv.Atoi(strings.TrimSpace(line))
line, _ = reader.ReadString('\n')
value := strings.TrimSpace(line)
items[i] = Item{ID: id, Value: value}
}
// TODO: إنشاء قناة مخزنة مؤقتاً بالسعة المحددة
// TODO: تشغيل المنتج (ليس كـ goroutine) لملء المخزن المؤقت
// TODO: تشغيل المستهلك لمعالجة جميع العناصر
// TODO: طباعة كل نتيجة مستهلكة في سطر منفصل
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool