پکیج sync/atomic در زبان Go مجموعهای از توابع سطح پایین (low-level) را برای انجام عملیات خواندن و نوشتن اتمی (atomic) روی متغیرهای حافظه فراهم میکند. منظور از عملیات اتمیک، عملیاتی است که به طور کامل و یکپارچه توسط CPU انجام میشود؛ یعنی هیچ گوروتین، thread یا پردازش دیگری نمیتواند مقدار متغیر را بین شروع و پایان یک عملیات اتمیک مشاهده یا تغییر دهد. این ویژگی برای پیادهسازی الگوهای همگامسازی سبک و بدون قفل (lock-free synchronization) و متغیرهای اشتراکی با دسترسی سریع و ایمن ضروری است.
مهمترین کاربردها و عملیات: #
- پیادهسازی شمارندههای اتمیک (atomic counters):
مثلاً افزایش شمارنده تعداد درخواست، جمع یا کم کردن بدون نیاز به Mutex. - فلگها یا وضعیتهای اشتراکی:
تنظیم و خواندن یک فلگ مشترک بین چند goroutine به شکلی که دچار race condition نشود. - ساخت primitiveهای همگامسازی سفارشی:
مثل اسپینلاک، قفل ساده، lock-free queue، semaphore سطح پایین و…
متدهای مهم atomic: #
atomic.AddInt32 / AddUint64
— جمع یا کم کردن مقدار به صورت اتمیکatomic.LoadInt32 / LoadPointer
— خواندن مقدار به شکل اتمیکatomic.StoreInt32 / StorePointer
— نوشتن مقدار به شکل اتمیکatomic.CompareAndSwapInt32
— عمل مقایسه و جایگزینی اتمیک (CAS)، قلب الگوریتمهای lock-freeatomic.Value
— یک ساختار wrapper برای نگهداری داده با خواندن و نوشتن اتمیک (ایدهآل برای ساخت cacheهای ساده یا حافظه به اشتراک گذاشته شده)
به مثال زیر توجه کنید :
1package main
2
3import (
4 "fmt"
5 "sync"
6 "sync/atomic"
7)
8
9type Cache struct {
10 mu sync.Mutex
11 data map[string]string
12}
13
14func (c *Cache) Set(key, value string) {
15 c.mu.Lock()
16 defer c.mu.Unlock()
17 c.data[key] = value
18}
19
20func (c *Cache) Get(key string) (value string, ok bool) {
21 c.mu.Lock()
22 defer c.mu.Unlock()
23 value, ok = c.data[key]
24 return
25}
26
27type AtomicCache struct {
28 mu sync.Mutex
29 data atomic.Value
30}
31
32func (c *AtomicCache) Set(key, value string) {
33 c.mu.Lock()
34 defer c.mu.Unlock()
35 c.data.Store(map[string]string{key: value})
36}
37
38func (c *AtomicCache) Get(key string) (value string, ok bool) {
39 data := c.data.Load().(map[string]string)
40 value, ok = data[key]
41 return
42}
43
44func main() {
45 cache := Cache{data: map[string]string{}}
46 cache.Set("key", "value")
47 fmt.Println(cache.Get("key")) // Output: value, true
48
49 atomicCache := AtomicCache{data: atomic.Value{}}
50 atomicCache.Set("key", "value")
51 fmt.Println(atomicCache.Get("key")) // Output: value, true
52}
در مثال فوق ما یک ساختار به نام Cache داریم که داخلش یک فیلد از نوع map داریم و قصد داریم یکسری اطلاعات را داخل کش بریزیم حال زمانیکه Set/Get می کنیم با استفاده از Mutex اون بخش از عملیات را لاک میکنیم تا جلوی عملیات نوشتن چندین گوروتین برروی یک آدرس حافظه را بگیریم. حال این عملیات رو ما با استفاده از atomic انجام دادیم و همگام سازی داده را بردیم تو سطح خیلی پایین تر در حافظه و با استفاده از atomic.Value که یک اینترفیس است این عملیات را انجام دادیم و این عملیات Set/Get حالت atomic پیدا کرده است.
آیا استفاده از atomic نیازمند mutex می باشد یا خیر؟
در این کد، mutex در متد Set برای جلوگیری از رخ دادن race condition یا دادههای نامنظم استفاده شده است. بدون mutex، چندین گوروتین ممکن است همزمان به دسترسی و تغییر دادههای map data بپردازند که موجب رفتار نامنظم و فساد داده میشود. با گرفتن mutex قبل از تغییر map data، متد Set اطمینان حاصل میکند که تنها یک گوروتین در هر زمان میتواند به دادهها دسترسی پیدا کند و تداخل دادهها را جلوگیری میکند.
استفاده از mutex در متد Get نیز مهم است، زیرا این اطمینان را به ما میدهد که در هنگام دسترسی به map data، هیچ گوروتین دیگری دارای مجوز تغییر دادهها نیست. بدون mutex، یک race condition ممکن است ایجاد شود اگر یک گوروتین دیگر در حال تغییر دادههای map باشد در حالی که یک گوروتین دیگر سعی در خواندن از آن دارد.
در پیادهسازی AtomicCache، یک atomic.Value برای ذخیره map استفاده شده است که به انجام عملیات اتمی روی آن اجازه میدهد. با این حال، حتی با استفاده از یک مقدار اتمی، همچنان نیاز به mutex وجود دارد تا فقط یک گوروتین در هر زمان به map دسترسی داشته باشد و تداخل دادهها را جلوگیری کند.
3.4.1 نکات و هشدارهای تولیدی (Production Caveats) #
memory safety: پکیج atomic از ویژگیهای سطح پایین CPU استفاده میکند و bypass کردن حافظه امن زبان Go را ممکن میسازد؛ یعنی اگر به درستی از آن استفاده نکنید، بهراحتی دچار bugهای عجیب و غیرقابل ردیابی خواهید شد. استفاده اشتباه میتواند باعث بروز race condition، memory corruption و مشکلات شدید تولیدی شود.
تراز حافظه (memory alignment): متغیرهایی که به صورت اتمیک تغییر میکنند باید به درستی روی حافظه align شوند (مثلاً در ساختار struct کنار سایر دادهها قرار نگیرند). بیتوجهی به این نکته ممکن است باعث crash برنامه در معماریهای خاص شود.
مناسب برای عملیات ساده: atomic برای primitive data types (int32, int64, pointer, …)، عملیات ساده و سناریوهایی با هماهنگی حداقلی طراحی شده است؛ اگر منطق پیچیدهتر دارید یا باید چندین متغیر را همزمان به شکل اتمیک تغییر دهید، از sync.Mutex یا سایر ابزارهای همزمانی ایمن Go استفاده کنید.
کاملاً lock-free نیست: گرچه atomic سریع و سبک است، اما فقط برای primitiveها کاملاً lock-free است. برای کار با دادههای پیچیده یا ساختارهای بزرگ، باید با احتیاط و دانش کافی عمل کنید.
atomic.Value برای دادههای ساختاری، اما فقط با خواندن و نوشتن کامل؛ عملیات mutate روی داده ذخیرهشده (مثلاً map یا slice) اتمیک نیست مگر کل value جایگزین شود.
3.4.2 برخی از کاربردهای atomic #
در زیر چندتا use case برای استفاده از پکیج atomic معرفی کردیم :
پیاده سازی همگام سازی بدون مسدودیت : پکیج atomic توابع سطح پایینی را برای انجام عملیات حافظه اتمی فراهم می کند که می تواند برای پیاده سازی الگوریتم های همگام سازی غیرمسدود مانند مقایسه و تعویض (CAS) یا بارگذاری لینک/ذخیره شرطی استفاده شود. LL/SC).
پیاده سازی ساختارهای داده با همزمانی سطح (high-concurrency) بالا : با پکیج atomic می توان برای پیاده سازی ساختارهای داده ای استفاده کرد که برای دسترسی همزمان و اصلاح توسط چندین گوروتین ایمن هستند. به عنوان مثال، می توانید از بسته اتمی برای پیاده سازی نقشه یا صف همزمان استفاده کنید.
پیاده سازی شمارنده (counter) از نوع atomic : شما با استفاده از پکیج atomic می توانید برای افزایش و کاهش شمارنده ها به صورت اتمی که می تواند برای اجرای مواردی مانند شمارش مرجع یا محدود کردن ratelimit استفاده شود.