9.4.19 الگو Lock-free synchronization

9.4.19 الگو Lock-free synchronization

9.4.19.1 توضیحات #

همگام‌سازی بدون قفل (lock-free synchronization) به مجموعه‌ای از تکنیک‌ها و الگوهای برنامه‌نویسی گفته می‌شود که به چندین thread یا goroutine اجازه می‌دهد به طور همزمان و ایمن به داده‌های مشترک دسترسی پیدا کنند، بدون اینکه از primitiveهایی مثل Mutex یا قفل‌های سنتی استفاده شود. هدف اصلی این الگوها افزایش بازدهی (throughput)، کاهش زمان انتظار (latency) و جلوگیری از مشکلات رایج قفل‌گذاری (مثل deadlock، priority inversion و contention) است. الگوریتم‌های lock-free تضمین می‌کنند که حتی اگر برخی از threadها متوقف شوند یا دچار کندی شوند، باقی سیستم همچنان قادر به پیشرفت خواهد بود (progress guarantee).

9.4.19.1.1 مکانیزم پایه: عملیات اتمیک و Compare-And-Swap (CAS) #

قلب تمام الگوریتم‌های lock-free، عملیات اتمیک (atomic operations) است، که توسط سخت‌افزار CPU و در Go توسط پکیج sync/atomic فراهم می‌شود. مهم‌ترین عملیات اتمیک، Compare-And-Swap (CAS) است؛ در این متد، برنامه مقدار فعلی متغیر را با مقدار مورد انتظار مقایسه می‌کند و در صورت برابری، مقدار جدید را جایگزین می‌کند—همه این مراحل به صورت اتمیک انجام می‌شوند. اگر مقدار تغییر نکرده باشد، عملیات موفق است وگرنه دوباره تلاش می‌شود (این رفتار اصطلاحاً به optimistic concurrency مشهور است).

مثال پایه‌ای از CAS در Go:

1import "sync/atomic"
2
3var counter int64
4atomic.AddInt64(&counter, 1) // جمع اتمیک بدون قفل

یا:

1if atomic.CompareAndSwapInt32(&x, old, new) {
2    // موفقیت در تعویض مقدار، ادامه بده
3}

9.4.19.1.2 الگوهای رایج Lock-Free #

  1. Lock-Free Counter:
    پیاده‌سازی شمارنده‌های افزایشی/کاهشی (مثل تعداد درخواست، session فعال و …) با متدهایی مانند atomic.AddInt64 و atomic.LoadInt64 بدون هیچ قفل یا wait.
  2. Lock-Free Stack/Queue:
    ساختارهایی مانند stack و queue را می‌توان با ترکیب pointer اتمیک و حلقه‌ی CAS پیاده‌سازی کرد؛ هر عملیاتی که نیاز به افزودن/حذف دارد، تا زمانی که مقدار قبلی با مقدار مورد انتظار برابر باشد، مقدار جدید را جایگزین می‌کند. اگر مقدار تغییر کرده باشد (به دلیل دخالت thread دیگر)، عمل دوباره تکرار می‌شود.
  3. Flagها و وضعیت‌های اتمیک:
    استفاده از فلگ‌ها برای signaling یا مدیریت وضعیت‌های بین چند goroutine (مثلاً active/inactive)، بدون race condition و با سرعت بسیار بالا.
  4. Reference Swap (atomic.Value):
    تعویض اتمیک مراجع به object یا ساختار داده کامل (مثلاً عوض کردن reference یک cache در حافظه) با atomic.Value، که خواندن و نوشتن کامل آن اتمیک است.

9.4.19.1.3 مزایا و محدودیت‌ها #

مزایا:

  • عملکرد بسیار بالا مخصوصاً در سناریوهای multi-core و تعداد بالای thread/goroutine
  • بدون deadlock و starvation: تضمین می‌کند که سیستم به خاطر انتظار برای قفل، هرگز متوقف نمی‌شود
  • مقیاس‌پذیری عالی برای داده‌های ساده و الگوریتم‌های سبک

محدودیت‌ها:

  • پیچیدگی کدنویسی و تحلیل: الگوریتم‌های lock-free نوشتن و تست سخت‌تری دارند و به دانش عمیق رفتار CPU و حافظه نیاز دارند.
  • مناسب فقط برای داده‌های primitive یا تغییرات ساده: برای داده‌های پیچیده یا ساختارهای بزرگ، مدیریت اتمیک بسیار دشوار و گاهاً غیرممکن است.
  • سازگاری معماری: روی همه CPUها و پلتفرم‌ها باید از لحاظ alignment و atomicity اطمینان حاصل کنید (در Go این موضوع مستند شده اما باید رعایت شود).

9.4.19.2 دیاگرام #

flowchart TD subgraph SharedMemory[Shared Variable مثلاً counter] V[Value: X] end G1[Goroutine 1] -- CAS (Compare and Swap) --> V G2[Goroutine 2] -- CAS --> V G3[Goroutine 3] -- CAS --> V V -- "Success: update committed" --> Done1[Continue] V -- "Fail: value changed,\nretry CAS" --> G1 V -- "Fail: value changed,\nretry CAS" --> G2 V -- "Fail: value changed,\nretry CAS" --> G3 style SharedMemory fill:#e2f0fc,stroke:#377dbf,stroke-width:2px style G1,G2,G3 fill:#fffbe7,stroke:#eac442,stroke-width:2px style Done1 fill:#d3f5e4,stroke:#11b584,stroke-width:2px

9.4.19.3 نمونه کد #

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6	"sync/atomic"
 7)
 8
 9func main() {
10	var counter int64
11	var wg sync.WaitGroup
12
13	numGoroutines := 10
14	incrementsPerGoroutine := 1000
15
16	wg.Add(numGoroutines)
17	for i := 0; i < numGoroutines; i++ {
18		go func() {
19			defer wg.Done()
20			for j := 0; j < incrementsPerGoroutine; j++ {
21				atomic.AddInt64(&counter, 1) // افزایش اتمیک بدون قفل
22			}
23		}()
24	}
25
26	wg.Wait()
27	fmt.Println("Final counter value:", counter)
28}
1$ go run main.go
2Final counter value: 10000

در این مثال یک شمارنده lock-free (بدون قفل) با استفاده از پکیج sync/atomic در زبان Go پیاده‌سازی شده است. هدف این است که چندین goroutine بتوانند همزمان و بدون نیاز به Mutex یا قفل سنتی، یک متغیر مشترک را افزایش دهند و در نهایت مقدار دقیق، بدون race condition و کاملاً صحیح به دست آید.

در ابتدای برنامه یک متغیر از نوع int64 به نام counter تعریف می‌شود که قرار است توسط goroutineها به طور مشترک افزایش یابد. یک sync.WaitGroup نیز به کار گرفته شده تا اطمینان حاصل شود همه goroutineها تا پایان اجرای خود منتظر بمانند و برنامه قبل از تکمیل همه عملیات‌ها خاتمه پیدا نکند.

در حلقه اصلی، ۱۰ goroutine ایجاد می‌شود که هر کدام ۱۰۰۰ بار مقدار شمارنده را افزایش می‌دهند. برای این کار به جای استفاده از قفل، از تابع atomic.AddInt64 استفاده می‌شود. این تابع تضمین می‌کند که عملیات افزایش مقدار شمارنده کاملاً اتمیک است؛ یعنی در هر لحظه فقط یک goroutine می‌تواند مقدار متغیر را تغییر دهد و هیچ دو goroutineی به طور همزمان مقدار ناسازگار یا نادرست دریافت نمی‌کنند.

در پایان برنامه با استفاده از wg.Wait() اطمینان حاصل می‌شود که همه goroutineها کار خود را به پایان رسانده‌اند، سپس مقدار نهایی شمارنده چاپ می‌شود. با توجه به تعداد goroutineها و تعداد دفعات افزایش، انتظار داریم مقدار نهایی برابر با ۱۰,۰۰۰ باشد که نشان‌دهنده عملکرد صحیح و بدون خطا (race) الگوریتم است.

این مثال یکی از ساده‌ترین و کاربردی‌ترین نمونه‌های lock-free synchronization است که می‌تواند در شمارنده‌های آماری، جمع‌آوری لاگ، فلگ‌های اشتراکی و سناریوهای نیازمند سرعت بالا و رقابت زیاد به کار رود—بدون نگرانی از deadlock، overhead قفل یا کاهش performance.

9.4.19.4 کاربردها #

  • شمارنده‌های آماری با بازده بالا:
    ثبت تعداد درخواست‌های دریافتی سرور، تعداد پیام‌های ارسال یا دریافت‌شده، تعداد خطاها یا موفقیت‌ها، یا هر نوع شمارش سریع و موازی که نباید موجب گلوگاه (bottleneck) در performance شود.
  • ثبت رخدادهای سریع (event counting):
    به‌طور مثال، شمارش لحظه‌ای کلیک‌ها یا رویدادهای کاربر در وب‌سرورهای real-time یا برنامه‌های مانیتورینگ.
  • پیاده‌سازی flagهای همگام‌سازی:
    استفاده از متغیرهای اتمیک برای مدیریت وضعیت بین goroutineها (مثلاً علامت پایان، فعال/غیرفعال شدن یک job، آماده‌بودن یک سرویس، یا لغو شدن یک تسک).
  • ساخت cache یا پیکربندی داینامیک:
    با استفاده از atomic.Value می‌توانید یک reference به ساختار داده یا تنظیمات (مثل map یا struct تنظیمات) را به صورت لحظه‌ای و اتمیک عوض کنید، بدون نیاز به قفل کردن کل داده برای خواندن و نوشتن.
  • ساخت primitiveهای همگام‌سازی سفارشی:
    پیاده‌سازی الگوریتم‌هایی مانند spinlock، lock-free queue و stack، و semaphoreهای سبک که نیاز به performance بسیار بالا دارند و استفاده از Mutex می‌تواند گلوگاه ایجاد کند.
  • مدیریت ساده state در concurrent logger یا metrics collector:
    در لایه‌های جمع‌آوری لاگ یا متریک که چندین goroutine همزمان روی یک متغیر می‌نویسند و می‌خوانند.