إدارة Goroutine
الـGoroutines خفيفة الوزن، ولكنها ليست مجانية: إذا كانوا يستهلكون الذاكرة على الأقل من أجل الدفعة الخاصة بهم وجدولة وحدة المعالجة المركزية. على الرغم من أن هذه التكاليف صغيرة لاستخدام الـGoroutines، إلا أن إنشاؤها بكميات كبيرة دون دورة حياة مُتحكم بها يمكن أن يؤدي إلى مشاكل أداء خطيرة. قد تؤدي الـGoroutines ذات الدورة الحياة غير المدارة أيضًا إلى مشاكل أخرى، مثل منع الكائنات غير المستخدمة من الاندثار واحتجاز الموارد غير المستخدمة.
لذلك، لا تفقد الـGoroutines في كودك. استخدم go.uber.org/goleak لاختبار تسربات الـGoroutine داخل حزمة قد تنتج الـGoroutines.
بشكل عام، يجب على كل جوروتين:
- أن يكون لديه وقت توقف قابل للتنبؤ؛ أو
- أن يكون لديه طريقة للإشارة إلى الجوروتين بأنه يجب عليه التوقف.
في كلتا الحالتين، يجب أن يوجد طريقة لكود للانتظار والانتظار حتى يكتمل الـGoroutine.
على سبيل المثال:
غير مُستحسن:
go func() {
for {
flush()
time.Sleep(delay)
}
}()
// ليس هناك طريقة لإيقاف هذا الـGoroutine. سيستمر في التشغيل بشكل لا نهائي حتى يخرج التطبيق.
مُستحسن:
var (
stop = make(chan struct{}) // إشارة للـGoroutine بالتوقف.
done = make(chan struct{}) // إشارة بأن الـGoroutine قد انتهت.
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// ... الكود الآخر
close(stop) // تشير إلى أن الـGoroutine يجب أن يتوقف.
<-done // وانتظر ليُكتمل
// يمكن أن يتم انتظار هذا الـGoroutine باستخدام close(stop)، ويمكننا الانتظار لكي يُكتمل <-done.
انتظار انتهاء الـGoroutines
عندما يتم تزويد الوظيفة بـGoroutine يتم إنشاؤها من قبل النظام، يجب وجود وسيلة للانتظار حتى يتم إنهاء الـGoroutine. هناك طريقتان شائعتان لتحقيق ذلك:
- استخدام
sync.WaitGroup
. استخدم هذا الأسلوب عندما يتعين الانتظار على العديد من الـGoroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
wg.Wait()
- إضافة
chan struct{}
آخر، وإغلاقه بعد اكتمال الـGoroutine. استخدم هذا الأسلوب إذا كان هناك جوروتين واحد فقط.
done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// لانتظار اكتمال الـGoroutine:
<-done
لا يجب على دالة init()
إنشاء الـGoroutines. كما يُرجى الرجوع إلى الابتعاد عن استخدام init().
إذا كانت الحزمة تتطلب جوروتين خلفية، يجب أن تكشف عن كائن مسؤول عن إدارة دورة حياة الـGoroutine. يجب أن يقدم هذا الكائن الإجراء (Close
, Stop
, Shutdown
, وما إلى ذلك) للإشارة إلى إنهاء الـGoroutine الخلفية وانتظار خروجها.
النهج غير المُستحسن:
func init() {
go doWork()
}
func doWork() {
for {
// ...
}
}
// عندما يقوم المستخدم بتصدير هذه الحزمة، سيتم إنشاء جوروتين خلفية بشكل لا قاعد له. الأساتذة لا يمكنه التحكم في الجوروتين أو إيقافه.
النهج المُستحسن:
type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
// ...
}
go w.doWork()
}
func (w *Worker) doWork() {
defer close(w.done)
for {
// ...
select {
case <-w.stop:
return
}
}
}
// يُخبر الإغلاق العامل للتوقف
// وينتظر حتى يكتمل.
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}
أنشئ العامل فقط عندما يطلبه المستخدم، وقدم طريقة لإيقاف العامل بحيث يمكن للمستخدم تحرير الموارد المستخدمة بواسطة العامل.
يرجى ملاحظة أنه إذا كان العامل يدير العديد من الـGoroutines، يجب استخدام WaitGroup.