Go: أساسيات التزامن في Go
تُستخدم القنوات للتواصل بين الغوروتينات، ويُستخدم حزمة
syncلحماية الحالة المشتركة — توفر لغة Go مجموعة أدوات كاملة للبرمجة المتزامنة.
عندما تحتاج عدة «غوروتينات» إلى القراءة من متغير واحد أو الكتابة إليه، يصبح من الضروري استخدام عناصر التزامن الأساسية لضمان السلامة. في هذا الدرس، ستتعرف على جميع الأدوات الأساسية واستراتيجيات الاختيار الخاصة بحزمة sync في لغة Go.
1. ستتعلم
sync.Mutexموتكسsync.RWMutexموتكس للقراءة والكتابة (عمليات القراءة غير حصرية)sync.Once: تنفيذ مرة واحدةsync.Map: خريطة آمنة من حيث التزامنsync/atomicالعمليات الذرية- كاشف العرق (علامة
-race) sync.Poolتجمع الكائنات المؤقت- استراتيجيات اختيار عناصر التزامن الأساسية
2. قصة حقيقية لمهندس متخصص في أنظمة التزامن العالي
(1) المشكلة: تقوم 100 «غوروتين» بالكتابة إلى العداد في وقت واحد، مما يؤدي إلى تلف البيانات تمامًا
تشارلي هو مهندس «الخلفية» في منصة للتجارة الإلكترونية، ويحتاج إلى حساب عدد الطلبات في الثانية:
«كانت 100 غوروتين تعالج الطلبات في وقت واحد، وكانت كل واحدة منها تزيد العداد بمقدار 1 بعد إتمام طلب. في اليوم الأول بعد الإطلاق، كانت البيانات غير دقيقة تمامًا — فقد أظهرت لوحة التحكم 1,000 طلب في الثانية، لكن لم يكن هناك سوى 300 طلب في قاعدة البيانات. سألني مديري: «أين ذهب المال الخاص بتلك الطلبات الـ700؟»»
فتح الملف البرمجي:
// Bad code: concurrent write to shared variable, no lock
var counter int
func processOrder(orderID string) {
// ... process order
counter++ // Non-atomic operation! Equivalent to:
// temp = counter
// temp = temp + 1
// counter = temp <- 3 goroutines executing this step simultaneously = chaos
}
counter++ ليست عملية ذرية — فهي تنقسم إلى ثلاث خطوات: القراءة، والزيادة بمقدار واحد، والكتابة. إذا قامت جروتينتان في الوقت نفسه بقراءة counter=10، وزيادتها بمقدار واحد، ثم إعادة كتابتها، فستكون النتيجة counter=11 بدلاً من 12 — وهذا مثال كلاسيكي على تنافس البيانات.
(2) الحل بلغة Go: Mutex
// counter.go
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
القيمة int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
c.القيمة++ // Protected by the lock; only one goroutine can execute at a time
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.القيمة
}
func main() {
var counter SafeCounter
var wg sync.WaitGroup
// 100 goroutines incrementing simultaneously
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Final القيمة: %d (expected 100)\n", counter.Value())
}
الناتج:
Final value: 100 (expected 100)
(3) العوائد: المقيدة مقابل غير المقيدة
| الحالة | النتيجة | تضارب البيانات |
|---|---|---|
| التزامن بدون قفل | 95/100/97 (عشوائي) | ✅ نعم (تم الكشف عن تفرع سباق) |
| حماية الموتكس | 100 | ❌ لا شيء |
| عملية ذرية | 100 | ❌ لا شيء |
go run -race main.go اكتشاف حالات التنافس على البيانات. إذا لم يكن الوصول المتزامن إلى المتغيرات المشتركة محميًّا بأقفال، فسيصدر الخيار -race تحذيرًا أثناء وقت التشغيل. يُنصح دائمًا بتمكين -race في CI/CD.
3. sync.Mutex Mutex
(1) أساسيات الموتكس
package main
import (
"fmt"
"sync"
)
type Account struct {
mu sync.Mutex
balance float64
}
func (a *Account) Deposit(amount float64) {
a.mu.Lock()
a.balance += amount
a.mu.Unlock()
}
func (a *Account) Withdraw(amount float64) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance < amount {
return fmt.Errorf("insufficient balance: have %.2f, need %.2f", a.balance, amount)
}
a.balance -= amount
return nil
}
func (a *Account) Balance() float64 {
a.mu.Lock()
defer a.mu.Unlock()
return a.balance
}
func main() {
acc := Account{balance: 1000}
var wg sync.WaitGroup
// 10 concurrent deposits
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
acc.Deposit(100)
}()
}
wg.Wait()
fmt.Printf("Balance: %.2f\n", acc.Balance())
}
▶ مثال: تأجيل نمط فتح القفل (موصى به)
// Recommended: Lock then immediately defer Unlock
func (a *Account) SafeMethod() {
a.mu.Lock()
defer a.mu.Unlock()
// All operations are within the lock
a.balance += 100
a.balance -= 50
// Even if there is a panic in between, defer ensures Unlock
}
// Not recommended: manual Unlock
func (a *Account) UnsafeMethod() {
a.mu.Lock()
a.balance += 100
a.mu.Unlock() // If there is a return or panic in between, Unlock won't be called → deadlock
}
Lock، يجب استدعاء Unlock. استخدم دائمًا defer a.mu.Unlock() بدلاً من استدعاء Unlock يدويًّا في النهاية — فإذا كانت هناك مسارات عودة متعددة، فإن نسيان استدعاء Unlock في أي منها سيؤدي إلى حدوث حالة تعطل.
4. sync.RWMutex: قفل القراءة والكتابة
(1) مبادئ أقفال القراءة والكتابة
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}
// Read operation: RLock (concurrent reads, non-exclusive)
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
// Write operation: Lock (exclusive, blocks all reads and writes)
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
cache := NewCache()
var wg sync.WaitGroup
// Multiple concurrent reads (RLock is non-exclusive)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
if v, ok := cache.Get("key"); ok {
fmt.Printf("Reader %d: %s\n", id, v)
}
time.Sleep(10 * time.Millisecond)
}
}(i)
}
// One writer
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond)
cache.Set("key", "value")
fmt.Println("Writer: write complete")
}()
wg.Wait()
}
▶ مثال: مقارنة الأداء بين Mutex و RWMutex
package main
import (
"sync"
"testing"
)
type DataStore struct {
mu sync.RWMutex
value int
}
func BenchmarkMutexRead(b *testing.B) {
store := DataStore{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
store.mu.Lock()
_ = store.value
store.mu.Unlock()
}
})
}
func BenchmarkRWMutexRead(b *testing.B) {
store := DataStore{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
store.mu.RLock()
_ = store.value
store.mu.RUnlock()
}
})
}
// go test -bench=. -benchmem
(3) Mutex مقابل RWMutex
| ميزة | Mutex | RWMutex |
|---|---|---|
| القراءة والقراءة | الاستبعاد المتبادل | عدم الاستبعاد المتبادل |
| القراءة والكتابة | الاستبعاد المتبادل | الاستبعاد المتبادل |
| الكتابة والكتابة | الاستبعاد المتبادل | الاستبعاد المتبادل |
| حالات الاستخدام | نسبة القراءة إلى الكتابة 1:1 | تتجاوز عمليات القراءة عمليات الكتابة بكثير (مثل ذاكرة التخزين المؤقت) |
| أداء القراءة | بطيء (متسلسل) | سريع (قراءات متزامنة) |
5. sync.Once: التنفيذ مرة واحدة
(1) «Once Basics»
package main
import (
"fmt"
"sync"
)
var (
config map[string]string
configOnce sync.Once
)
func loadConfig() {
configOnce.Do(func() {
fmt.Println("Initializing config...")
config = map[string]string{
"host": "localhost",
"port": "8080",
}
})
}
func main() {
var wg sync.WaitGroup
// 100 goroutines call simultaneously, but init executes only once
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
loadConfig()
}()
}
wg.Wait()
fmt.Printf("config: %v\n", config)
}
الناتج:
Initializing config...
config: map[host:localhost port:8080]
(2) sync.Once مقابل الإشارات اليدوية
| الطريقة | آمنة للاستخدام المتعدد | حجم الكود |
|---|---|---|
if !initialized { ... } |
❌ تضارب في الوصول إلى البيانات | بسيط |
sync.Once |
✅ ضمانات Go | الحد الأدنى |
init() دالة |
✅ يتم تنفيذها عند تحميل الحزمة | بسيطة |
sync.Once أن الدالة الموجودة داخل Do تُنفَّذ مرة واحدة فقط، حتى لو تم استدعاؤها في وقت واحد من قِبل 1,000 goroutine. ويستخدم التنفيذ الداخلي لهذه الدالة عمليات ذرية وموتكسات، مما يجعلها عالية الكفاءة.
6. sync.Map: خريطة آمنة من حيث التزامن
(1) أساسيات sync.Map
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// Concurrent writes
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(fmt.Sprintf("key-%d", n), n*100)
}(i)
}
wg.Wait()
// Read
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s = %d\n", key, value)
return true
})
// LoadOrStore: returns if exists, stores if not
actual, loaded := m.LoadOrStore("key-0", 999)
fmt.Printf("LoadOrStore: actual=%d, loaded=%v\n", actual, loaded)
}
▶ مثال: sync.Map مقابل map+Mutex
package main
import (
"sync"
"testing"
)
// map + Mutex
type MutexMap struct {
mu sync.Mutex
item map[string]int
}
func (m *MutexMap) Store(key string, القيمة int) {
m.mu.Lock()
m.item[key] = القيمة
m.mu.Unlock()
}
func (m *MutexMap) Load(key string) (int, bool) {
m.mu.Lock()
v, ok := m.item[key]
m.mu.Unlock()
return v, ok
}
func BenchmarkMutexMap(b *testing.B) {
m := &MutexMap{item: make(map[string]int)}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Store("key", 1)
m.Load("key")
}
})
}
func BenchmarkSyncMap(b *testing.B) {
var m sync.Map
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Store("key", 1)
m.Load("key")
}
})
}
// go test -bench=. -benchmem
(3) حالات استخدام sync.Map
| السيناريو | التوصية | السبب |
|---|---|---|
| كتابة المفتاح مرة واحدة | sync.Map | تحسين القراءة (عمليات كتابة قليلة، عمليات قراءة كثيرة) |
| تحديثات متكررة للمفاتيح | map+Mutex | عمليات الكتابة في sync.Map أبطأ قليلاً |
| توسيع مجموعة المفاتيح | sync.Map | تجنب قفل الخريطة بأكملها |
| سيناريو بسيط | الخريطة + Mutex | أكثر سهولة في الاستخدام |
| متطلبات الأداء العالي | معيار قياس محدد | يتم تحديدها من خلال الاختبارات الفعلية |
7. sync/atomic: العمليات الذرية
(1) أساسيات الذرة
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64
// Atomic increment
newVal := atomic.AddInt64(&counter, 1)
fmt.Printf("AddInt64: %d\n", newVal)
// Atomic read
val := atomic.LoadInt64(&counter)
fmt.Printf("LoadInt64: %d\n", val)
// Atomic write
atomic.StoreInt64(&counter, 100)
// CAS (Compare And Swap)
swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
fmt.Printf("CAS: swapped=%v, val=%d\n", swapped, atomic.LoadInt64(&counter))
}
▶ مثال: عدادات الذرات مقابل الموتكسات
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type AtomicCounter struct {
value int64
}
func (c *AtomicCounter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
type MutexCounter struct {
mu sync.Mutex
value int
}
func (c *MutexCounter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *MutexCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
n := 100000
// Atomic counter
atomicStart := time.Now()
var ac AtomicCounter
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ac.Increment()
}()
}
wg.Wait()
fmt.Printf("Atomic: %d (%v)\n", ac.Value(), time.Since(atomicStart))
// Mutex counter
muStart := time.Now()
var mc MutexCounter
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mc.Increment()
}()
}
wg.Wait()
fmt.Printf("Mutex: %d (%v)\n", mc.Value(), time.Since(muStart))
}
(3) الوظائف الذرية الشائعة
| العملية | الوظيفة | الغرض |
|---|---|---|
| جمع/طرح | AddInt64، AddUint32 |
العداد |
| قراءة | LoadInt64، LoadPointer |
قراءة آمنة |
| كتابة | StoreInt64، StorePointer |
كتابة آمنة |
| CAS | CompareAndSwapInt64 |
قفل متفائل |
| المبادلة | SwapInt64 |
المبادلة الذرية |
8. كاشف السباق
▶ مثال: الكشف عن تضارب الوصول إلى البيانات
// race_example.go
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // Data race!
}()
}
wg.Wait()
fmt.Println(counter)
}
$ go run -race race_example.go
==================
WARNING: DATA RACE
Read at 0x00c0000... by goroutine X
main.main.func1()
race_example.go:14 +0x...
Previous write at 0x00c0000... by goroutine Y
main.main.func1()
race_example.go:14 +0x...
==================
9. sync.Pool — تجمع الكائنات
▶ مثال: تجمع الكائنات
package main
import (
"fmt"
"sync"
)
type User struct {
Name string
Age int
}
var userPool = sync.Pool{
New: func() interface{} {
return &User{}
},
}
func main() {
// Get from pool (avoids allocating a new object each time)
u := userPool.Get().(*User)
u.Name = "Alice"
u.Age = 28
fmt.Printf("Using: %+v\n", u)
// Put back into the pool after use
userPool.Put(u)
// Next Get reuses the previous object
u2 := userPool.Get().(*User)
fmt.Printf("Reusing: %+v\n", u2)
}
10. مثال كامل: عداد عالي التزامن
// concurrent_counter.go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// ---------- 3 counter variants ----------
type Counter interface {
Increment()
Value() int64
}
// AtomicCounter
type AtomicCounter struct {
القيمة int64
}
func (c *AtomicCounter) Increment() {
atomic.AddInt64(&c.القيمة, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.القيمة)
}
// MutexCounter
type MutexCounter struct {
mu sync.Mutex
القيمة int64
}
func (c *MutexCounter) Increment() {
c.mu.Lock()
c.القيمة++
c.mu.Unlock()
}
func (c *MutexCounter) Value() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.القيمة
}
// ChannelCounter
type ChannelCounter struct {
ch chan struct{}
القيمة int64
done chan struct{}
}
func NewChannelCounter() *ChannelCounter {
c := &ChannelCounter{
ch: make(chan struct{}, 100),
done: make(chan struct{}),
}
go func() {
for range c.ch {
c.القيمة++
}
close(c.done)
}()
return c
}
func (c *ChannelCounter) Increment() {
c.ch <- struct{}{}
}
func (c *ChannelCounter) Value() int64 {
return atomic.LoadInt64(&c.القيمة)
}
func (c *ChannelCounter) Close() {
close(c.ch)
<-c.done
}
// ---------- Benchmark ----------
func benchmarkCounter(c Counter, goroutines, increments int) time.Duration {
var wg sync.WaitGroup
start := time.Now()
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
c.Increment()
}
}()
}
wg.Wait()
return time.Since(start)
}
func main() {
const (
goroutines = 100
increments = 10000
)
fmt.Printf("Benchmark: %d goroutines, each executing %d Increments\n\n", goroutines, increments)
// 1. Atomic counter
ac := &AtomicCounter{}
atomicTime := time.Now()
atomicDone := make(chan struct{})
go func() {
defer close(atomicDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
ac.Increment()
}
}()
}
wg.Wait()
}()
<-atomicDone
fmt.Printf("AtomicCounter: %d (expected %d) — %v\n",
ac.Value(), goroutines*increments, time.Since(atomicTime))
// 2. Mutex counter
mc := &MutexCounter{}
mutexTime := time.Now()
mutexDone := make(chan struct{})
go func() {
defer close(mutexDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
mc.Increment()
}
}()
}
wg.Wait()
}()
<-mutexDone
fmt.Printf("MutexCounter: %d (expected %d) — %v\n",
mc.Value(), goroutines*increments, time.Since(mutexTime))
// 3. Channel counter
cc := NewChannelCounter()
channelTime := time.Now()
channelDone := make(chan struct{})
go func() {
defer close(channelDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
cc.Increment()
}
}()
}
wg.Wait()
cc.Close()
}()
<-channelDone
fmt.Printf("ChannelCounter: %d (expected %d) — %v\n",
cc.Value(), goroutines*increments, time.Since(channelTime))
fmt.Println("\nConclusion: Atomic > Mutex > Channel (performance from high to low)")
}
sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant G3 as Goroutine 3
participant M as Mutex
G1->>M: Lock()
Note over G1,M: G1 holds the lock
G2->>M: Lock() blocked
G3->>M: Lock() blocked
G1->>M: Unlock()
M-->>G2: wake up
M-->>G3: still blocked
G2->>M: Lock() success
Note over G2,M: G2 holds the lock
G2->>M: Unlock()
M-->>G3: wake up
G3->>M: Lock() success
*Counter).
❓ أسئلة شائعة
sync.Once؟sync.Once.Do(f) أن يتم تنفيذ f مرة واحدة فقط، حتى لو استدعته 1,000 goroutine في وقت واحد. الاستخدامات النموذجية: تكوينات التحميل المتأخر، ونمط singleton، والموارد التي تحتاج إلى التهيئة مرة واحدة فقط. ويتم تنفيذه داخليًا باستخدام العمليات الذرية وموتكس.sync.Map مناسبًا؟atomic مناسبًا؟atomic أسرع بـ 10 إلى 100 مرة من الموتكس، ولكنه لا يمكن استخدامه إلا مع الأنواع الأساسية مثل int وuint وpointer. بالنسبة للمنطق المعقد، استخدم الموتكس بدلاً من ذلك.go run -race main.go أو go test -race ./.... تكتشف هذه الأداة حالات التضارب في الوصول إلى البيانات أثناء وقت التشغيل — فإذا قامت اثنتان من goroutines بالوصول في وقت واحد إلى نفس المتغير وكانت إحدى العمليتين على الأقل عملية كتابة، فسيتم إصدار تحذير. يُنصح بإبقائها مفعّلة في بيئة CI/CD.sync.Pool؟sync.Mutex في لغة Go هو قفل غير قابل لإعادة الدخول — إذا استدعت نفس goroutine Lock مرتين متتاليتين، فسيؤدي ذلك إلى حدوث تعطل متبادل. إذا كنت بحاجة إلى الحصول على القفل مرة أخرى داخل نفس goroutine، فقم بإعادة هيكلة الكود لاستخدام sync.Mutex مع الاستدعاءات التكرارية، أو استخدم defer لضمان تحرير القفل بشكل صحيح.Lock فقط لعمليات الكتابة وRLock فقط لعمليات القراءة؛ ولا تخلط بينهما.📖 ملخص
- موتكس:
Lock/Unlock، مناسب لنسبة قراءة وكتابة 1:1 - قفل RWMutex للقراءة والكتابة:
RLockللقراءة المتزامنة،Lockللكتابة الحصرية - sync.Once:
Do(f)يضمن أن الدالة تُنفَّذ مرة واحدة فقط - sync.Map: خريطة آمنة من حيث التزامن، مخصصة لسيناريوهات القراءة المكثفة والكتابة المحدودة
- sync/atomic: عدادات خالية من القفل، قفل متفائل CAS
- كاشف التنافس على الوصول: يكتشف العلامة
-raceحالات التنافس على الوصول إلى البيانات - sync.Pool: مجموعة من الكائنات المؤقتة التي تقلل من العبء على أداة جمع القمامة
- الاختيار: atomic > Mutex > RWMutex > Channel (حسب ترتيب الأداء من الأعلى إلى الأسفل)
📝 تمارين
-
المشكلة الأساسية (الصعوبة ⭐): استخدم موتكس لحماية عمليات القراءة والكتابة المتزامنة على
map[string]int. قم بتشغيل 10 غوروتينات كاتبة لكتابة البيانات و10 غوروتينات قارئة لقراءة البيانات، وتأكد من عدم وجود تضارب في التنافس (اجتياز اختبار-race). -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قارن أداء أربعة أنواع من العدادات (الذري، وMutex، وRWMutex، والقناة) من خلال قياس الوقت المستغرق والتحقق من صحة 100 goroutine تقوم بتنفيذ 10,000 عملية زيادة. استخدم
-raceللتحقق من عدم وجود تضارب في الوصول إلى البيانات. -
التحدي (الصعوبة ⭐⭐⭐): تنفيذ ذاكرة تخزين مؤقتة ذات تزامن عالٍ تتضمن عمليات قراءة كثيرة وعمليات كتابة قليلة. المتطلبات: (1) استخدام RWMutex للحماية؛ (2) دعم عمليات Get/Set/Delete/Range؛ (3) دعم انتهاء صلاحية TTL (
time.Now().After(expireAt))؛ (4) حظر جميع عمليات القراءة أثناء عمليات الكتابة، مع السماح بعمليات القراءة المتزامنة؛ (5) اجتياز اختبار-race.