البرمجة غير المتزامنة في Go: دليل شامل للمبتدئين
تم إنشاء Go (Golang) مع مراعاة احتياجات التطوير الحديث، حيث يلعب الأداء والتوازي دورًا رئيسيًا. على عكس العديد من اللغات التي يتم فيها تنفيذ عدم التزامن من خلال callbacks أو promises، تقدم Go آليات مدمجة وأنيقة: Goroutines و Channels. سيعرّفك هذا الدليل على أساسيات البرمجة غير المتزامنة في Go ويوضح لك كيفية كتابة كود متزامن فعال.
ما هي Goroutines وكيف تختلف عن الخيوط (Threads)؟
Goroutine هي خيط تنفيذ خفيف الوزن يُدار بواسطة runtime الخاص بـ Go. على عكس خيوط نظام التشغيل (OS threads)، تستهلك Goroutines موارد أقل بكثير: تبدأ مكدسها (stack) ببضعة كيلوبايتات ويمكن أن تنمو وتتقلص ديناميكيًا. هذا يسمح بتشغيل ملايين Goroutines في وقت واحد دون تحميل كبير على الذاكرة.
تشغيل Goroutine بسيط جدًا —只需在函数调用前添加关键字 go:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("مرحبًا من Goroutine!")
}
func main() {
go sayHello() // تشغيل Goroutine
time.Sleep(100 * time.Millisecond) // ننتظر حتى يتسنى لـ Goroutine التنفيذ
fmt.Println("تم إنهاء الدالة الرئيسية")
}
من المهم ملاحظة: إذا انتهت الدالة الرئيسية main()، فسيتم إيقاف جميع Goroutines قيد التشغيل قسرًا. للمزامنة، نستخدم أدوات خاصة.
المزامنة باستخدام WaitGroup
أبسط طريقة لانتظار انتهاء Goroutines هي استخدام sync.WaitGroup. يعمل كعداد: نزيده عند تشغيل Goroutine وننقصه عند الانتهاء. يتم حظر الدالة الرئيسية حتى يصبح العداد صفرًا.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // إنقاص العداد عند الانتهاء
fmt.Printf("بدأ العامل %d العمل", id)
time.Sleep(time.Second)
fmt.Printf("أنهى العامل %d العمل", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // زيادة العداد قبل التشغيل
go worker(i, &wg)
}
wg.Wait() // انتظار انتهاء جميع Goroutines
fmt.Println("جميع العمال أنهوا عملهم")
}
لاحظ: يتم استدعاء wg.Add(1) قبل تشغيل Goroutine، و wg.Done() داخلها (عادةً عبر defer). هذا يضمن إنقاص العداد بشكل صحيح حتى في حالة حدوث panic.
Channels: التواصل بين Goroutines
Channels هي "أنابيب" محددة النوع يمكن من خلالها لـ Goroutines تبادل البيانات بأمان. توفر Channels المزامنة افتراضيًا: الإرسال إلى Channel يحظر Goroutine حتى تقرأ Goroutine أخرى القيمة، والعكس صحيح.
يتم إنشاء Channels باستخدام الدالة make:
package main
import "fmt"
func main() {
// إنشاء Channel غير مخزّن (unbuffered) للأعداد الصحيحة
ch := make(chan int)
// تشغيل Goroutine ترسل البيانات
go func() {
ch <- 42 // إرسال القيمة إلى Channel
}()
// القراءة من Channel (يتم الحظر حتى تصل القيمة)
value := <-ch
fmt.Println("القيمة المستلمة:", value)
}
أنواع Channels:
- غير مخزّنة (Unbuffered) — متزامنة، يتم حظر الإرسال حتى الاستلام.
- مخزّنة (Buffered) — غير متزامنة، يتم حظر الإرسال فقط عندما يمتلئ المخزن المؤقت.
// إنشاء Channel مخزّن يتسع لـ 3 عناصر
bufferedCh := make(chan string, 3)
bufferedCh <- "A"
bufferedCh <- "B"
bufferedCh <- "C" // طالما لم يمتلئ — لا يتم حظره