هذا مقتطف من التدوينة؛ المنشور الكامل متاح هنا: https://victoriametrics.com/blog/go-sync-pool/
هذا المنشور جزء من سلسلة حول التعامل مع التزامن في Go:
في كود مصدر VictoriaMetrics، نستخدم sync.Pool كثيرًا، وهو بصراحة مناسب تمامًا لكيفية تعاملنا مع الكائنات المؤقتة، وخاصة مخازن البايت المؤقتة أو الشرائح.
يشيع استخدامه في المكتبة القياسية. على سبيل المثال، في الحزمة encoding/json:
package json var encodeStatePool sync.Pool // An encodeState encodes JSON into a bytes.Buffer. type encodeState struct { bytes.Buffer // accumulated output ptrLevel uint ptrSeen map[any]struct{} }
في هذه الحالة، يتم استخدام sync.Pool لإعادة استخدام كائنات *encodeState، التي تتعامل مع عملية تشفير JSON في بايت.Buffer.
بدلاً من مجرد رمي هذه الكائنات بعد كل استخدام، الأمر الذي من شأنه أن يمنح جامع البيانات المهملة المزيد من العمل، نقوم بتخزينها في تجمع (sync.Pool). في المرة القادمة التي نحتاج فيها إلى شيء مماثل، نلتقطه من حوض السباحة بدلاً من صنع شيء جديد من الصفر.
ستجد أيضًا العديد من مثيلات sync.Pool في حزمة net/http، والتي يتم استخدامها لتحسين عمليات الإدخال/الإخراج:
package http var ( bufioReaderPool sync.Pool bufioWriter2kPool sync.Pool bufioWriter4kPool sync.Pool )
عندما يقرأ الخادم نصوص الطلب أو يكتب الاستجابات، يمكنه بسرعة سحب القارئ أو الكاتب المخصص مسبقًا من هذه التجمعات، وتخطي التخصيصات الإضافية. علاوة على ذلك، تم إعداد مجموعتي الكاتب، *bufioWriter2kPool و*bufioWriter4kPool، للتعامل مع احتياجات الكتابة المختلفة.
func bufioWriterPool(size int) *sync.Pool { switch size { case 2حسنًا، هذا يكفي للمقدمة.
اليوم، نحن نتعمق في موضوع المزامنة.Pool، التعريف، كيفية استخدامه، ما الذي يحدث تحت الغطاء، وكل شيء آخر قد ترغب في معرفته.
بالمناسبة، إذا كنت تريد شيئًا عمليًا أكثر، فهناك مقالة جيدة من خبراء Go لدينا توضح كيفية استخدامنا للمزامنة.Pool in VictoriaMetrics: تقنيات تحسين الأداء في قواعد بيانات السلاسل الزمنية: sync.Pool للعمليات المرتبطة بوحدة المعالجة المركزية
ما هو sync.Pool؟
بكل بساطة، sync.Pool in Go هو مكان يمكنك من خلاله الاحتفاظ بالكائنات المؤقتة لإعادة استخدامها لاحقًا.
ولكن هذا هو الأمر، فأنت لا تتحكم في عدد الأشياء التي تبقى في حوض السباحة، وأي شيء تضعه هناك يمكن إزالته في أي وقت، دون أي تحذير وستعرف السبب عند قراءة القسم الأخير.
]النقطة الجيدة هي أن المجمع مصمم ليكون آمنًا للخيوط، لذلك يمكن للعديد من goroutines الاستفادة منه في وقت واحد. ليست مفاجأة كبيرة، مع الأخذ في الاعتبار أنها جزء من حزمة المزامنة.
"ولكن لماذا نهتم بإعادة استخدام الأشياء؟"
عندما يكون لديك الكثير من goroutines قيد التشغيل مرة واحدة، فغالبًا ما تحتاج إلى كائنات مماثلة. تخيل تشغيل go f() عدة مرات بشكل متزامن.
إذا قام كل goroutine بإنشاء كائناته الخاصة، فيمكن أن يزيد استخدام الذاكرة بسرعة وهذا يضع ضغطًا على أداة تجميع البيانات المهملة لأنه يتعين عليها تنظيف كل هذه الكائنات بمجرد عدم الحاجة إليها.
ينشئ هذا الموقف دورة يؤدي فيها التزامن العالي إلى زيادة استخدام الذاكرة، مما يؤدي بعد ذلك إلى إبطاء أداة تجميع مجمعي البيانات المهملة. تم تصميم sync.Pool للمساعدة في كسر هذه الحلقة المفرغة.
type Object struct { Data []byte } var pool sync.Pool = sync.Pool{ New: func() any { return &Object{ Data: make([]byte, 0, 1024), } }, }لإنشاء تجمع، يمكنك توفير وظيفة New() التي تُرجع كائنًا جديدًا عندما يكون التجمع فارغًا. هذه الوظيفة اختيارية، إذا لم تقم بتوفيرها، فإن المجمع يُرجع صفرًا فقط إذا كان فارغًا.
في المقتطف أعلاه، الهدف هو إعادة استخدام مثيل بنية الكائن، وتحديدًا الشريحة الموجودة بداخله.
تساعد إعادة استخدام الشريحة على تقليل النمو غير الضروري.
على سبيل المثال، إذا زاد حجم الشريحة إلى 8192 بايت أثناء الاستخدام، فيمكنك إعادة تعيين طولها إلى الصفر قبل إعادتها إلى المجموعة. لا تزال سعة المصفوفة الأساسية تبلغ 8192، لذا في المرة القادمة التي تحتاج إليها، ستكون تلك الـ 8192 بايت جاهزة لإعادة الاستخدام.
func (o *Object) Reset() { o.Data = o.Data[:0] } func main() { testObject := pool.Get().(*Object) // do something with testObject testObject.Reset() pool.Put(testObject) }التدفق واضح جدًا: تحصل على كائن من حمام السباحة، وتستخدمه، وتعيد تعيينه، ثم تعيده إلى حمام السباحة. يمكن إجراء إعادة ضبط الكائن إما قبل إعادته إلى مكانه مرة أخرى أو مباشرة بعد الحصول عليه من حوض السباحة، ولكنه ليس إلزاميًا، إنه ممارسة شائعة.
إذا لم تكن من محبي استخدام تأكيدات النوع Pool.Get().(*Object)، فهناك طريقتان لتجنب ذلك:
func getObjectFromPool() *Object { obj := pool.Get().(*Object) return obj }
type Pool[T any] struct { sync.Pool } func (p *Pool[T]) Get() T { return p.Pool.Get().(T) } func (p *Pool[T]) Put(x T) { p.Pool.Put(x) } func NewPool[T any](newF func() T) *Pool[T] { return &Pool[T]{ Pool: sync.Pool{ New: func() interface{} { return newF() }, }, } }
يمنحك الغلاف العام طريقة أكثر أمانًا للكتابة للعمل مع التجمع، وتجنب تأكيدات الكتابة.
لاحظ فقط أنه يضيف القليل من النفقات العامة بسبب الطبقة الإضافية من المراوغة. في معظم الحالات، يكون هذا الحمل ضئيلًا للغاية، ولكن إذا كنت تعمل في بيئة شديدة الحساسية لوحدة المعالجة المركزية، فمن الجيد تشغيل معايير لمعرفة ما إذا كان الأمر يستحق ذلك.
ولكن مهلا، هناك المزيد.
إذا لاحظت من العديد من الأمثلة السابقة، بما في ذلك تلك الموجودة في المكتبة القياسية، فإن ما نقوم بتخزينه في التجمع ليس الكائن نفسه ولكن مؤشر للكائن.
دعني أشرح السبب بمثال:
var pool = sync.Pool{ New: func() any { return []byte{} }, } func main() { bytes := pool.Get().([]byte) // do something with bytes _ = bytes pool.Put(bytes) }
نحن نستخدم مجموعة من [] بايت. بشكل عام (وإن لم يكن دائمًا)، عندما تقوم بتمرير قيمة إلى واجهة ما، قد يتسبب ذلك في وضع القيمة في الكومة. يحدث هذا هنا أيضًا، ليس فقط مع الشرائح ولكن مع أي شيء تمرره إلى Pool.Put() والذي لا يمثل مؤشرًا.
إذا قمت بالتحقق باستخدام تحليل الهروب:
// escape analysis $ go build -gcflags=-m bytes escapes to heap
الآن، لا أقول أن وحدات البايت المتغيرة تنتقل إلى الكومة، بل أود أن أقول "قيمة البايتات تهرب إلى الكومة من خلال الواجهة".
لمعرفة سبب حدوث ذلك، سنحتاج إلى البحث في كيفية عمل تحليل الهروب (وهو ما قد نفعله في مقال آخر). ومع ذلك، إذا قمنا بتمرير مؤشر إلى Pool.Put()، فلن يكون هناك تخصيص إضافي:
var pool = sync.Pool{ New: func() any { return new([]byte) }, } func main() { bytes := pool.Get().(*[]byte) // do something with bytes _ = bytes pool.Put(bytes) }
قم بتشغيل تحليل الهروب مرة أخرى، وسترى أنه لم يعد هناك إمكانية للهروب إلى الكومة. إذا كنت تريد معرفة المزيد، فهناك مثال في كود مصدر Go.
قبل أن نتطرق إلى كيفية عمل sync.Pool فعليًا، من المفيد التعرف على أساسيات نموذج جدولة PMG الخاص بـ Go، وهذا حقًا هو العمود الفقري لسبب كفاءة sync.Pool.
هناك مقالة جيدة تشرح نموذج PMG مع بعض العناصر المرئية: نماذج PMG في Go
إذا كنت تشعر بالكسل اليوم وتبحث عن ملخص مبسط، فأنا أساندك:
يرمز PMG إلى P (المعالجات المنطقية p)، وM (mخيوط الآلة)، وG (goroutines). النقطة الأساسية هي أن كل معالج منطقي (P) يمكن أن يعمل عليه مؤشر ترابط جهاز واحد فقط (M) في أي وقت. ولكي يتم تشغيل goroutine (G)، يجب ربطه بخيط (M).
يتلخص هذا في نقطتين رئيسيتين:
ولكن الأمر هو المزامنة. إن التجمع في Go ليس مجرد تجمع كبير واحد، فهو يتكون في الواقع من عدة تجمعات "محلية"، مع ربط كل منها بسياق معالج معين، أو P، وقت تشغيل Go هو الإدارة في أي وقت.
عندما يحتاج goroutine الذي يعمل على معالج (P) إلى كائن من المجموعة، فسوف يتحقق أولاً من تجمع P-local الخاص به قبل البحث في أي مكان آخر.
المنشور الكامل متاح هنا: https://victoriametrics.com/blog/go-sync-pool/
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3