برنامه نویسی

غواصی عمیق به بسته همگام سازی GO: Mutex ، RWMutex و مشکلات برای گول زدن

1. سلام ، بیایید قفل صحبت کنیم!

اگر کمی کد Go را به خود جلب کرده اید ، احتمالاً گورواین ها را خسته کرده اید و با کانال هایی مانند یک حرفه ای رقصیده اید. همزمانی سس مخفی Go است ، و sync بسته سرآشپز قابل اعتماد شما است-به خصوص قفل های آن: Mutex وت RWMutexبشر قفل ها قهرمانان ناخوشایند هستند که کد همزمان شما را از تبدیل شدن به یک گودال پر هرج و مرج جلوگیری می کنند. آنها را به درستی بدست آورید ، و برنامه شما هولناک است. آشفتگی ، و این بن بست شهر یا عملکردی است.

من بیش از یک دهه است که برنامه نویسی کرده ام و قفل ها بیکن من را نجات داده اند – و مرا سوزانده اند – بیشتر از آنکه بتوانم حساب کنم. بنابراین ، بیایید باز کنیم Mutex وت RWMutex: چگونه آنها کار می کنند ، جایی که می درخشند ، و تله هایی که در انتظار شما هستند. از نمونه های دنیای واقعی ، استعاره هایی که در واقع معنی دارند و نکاتی برای افزایش بازی همزمانی خود انتظار دارید. در پایان ، شما قفل هایی مانند Jedi را به دست می آورید و حتی ممکن است کمی در بررسی کد بعدی خود خم شوید. آماده؟ بیایید شیرجه بزنیم!


2. sync بسته: ابزار همزمانی شما

قبل از اینکه روی قفل ها بیرون بیاییم ، بیایید صحنه را تنظیم کنیم. در sync بسته بندی Confurrency Go's Confurrency Swiss Army Conferrency ، پر از ابزاری برای نگه داشتن گوروتین ها است. به عنوان کنترل ترافیک هوایی برای کد خود فکر کنید – بدون آن ، آن دسته از گوروس های سریع به یکدیگر سقوط می کنند.

2.1 چه چیزی در داخل است؟

  • Mutex: یک گوروتین در یک زمان ، بدون استثنا.
  • RWMutex: چندین خواننده یا یک نویسنده – انعطاف پذیر و فانتزی.
  • WaitGroup: منتظر بسته شدن Goroutine Posse هستید.
  • اضافی: Onceبا Cond– کول اما طاقچه.

ما در حال بزرگنمایی هستیم Mutex وت RWMutex– نان و کره قفل ، و سخت ترین برای تسلط.

2.2 MUTEX در مقابل RWMUTEX: TL ؛ DR

  • مایه: کافی شاپ یک صندلی. یک گوروتین به طور همزمان می چرخد ​​- برای چیزهای ساده.
  • RWMUTEX: یک کتابخانه. تن خوانندگان می توانند مرور کنند ، اما فقط یک نوسازی کننده (نویسنده) در یک زمان. ایده آل برای بار کاری سنگین.
نشان مایه RWMUTEX
همزمان می خواند نه بله
همزمان می نویسد نه نه
بهترین برای همه منظوره با ارزش خواندن

2.3 چگونه کار می کنند؟

در زیر کاپوت ، قفل ها از سمیفورهای سیستم عامل و گزینه های اتمی استفاده می کنند (مانند CAS-کنترل و جابجایی). بوها Mutex مانند پرچم “گرفته شده” است: اگر تنظیم شود ، دیگران صبر می کنند. RWMutex تعداد خوانندگان و قفل نویسنده را به هم می زند. سنبله های مشاجره؟ سیستم عامل قدم می زند ، در صف گوروتین ها – با هزینه. مبانی پایین-ممکن است دست به دست هم داده شود.


3. MUTEX: قفل بدون مزخرف

Mutex آیا تندرست همزمان Go است: یکی در ، هر کس دیگری منتظر است. این ساده ، قابل اعتماد و یک نجات دهنده است – تا زمانی که به آن سفر کنید.

3.1 mutex در عمل

در اینجا یک کلاسیک وجود دارد: یک پیشخوان با Goroutines Gone Wild ، که توسط Mutex:

package main

import (
    "fmt"
    "sync"
)

var (
    mu      sync.Mutex
    counter int
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++ // Safe now!
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // 1000, every time
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بدون muبا counter++ می تواند یک پله Racecar باشد. با آن ، قایقرانی صاف.

3.2 جایی که می درخشد

  • نگهبان حافظه نهان: یک حافظه نهان در حافظه را در API قفل کرده است-کپی کپی شده ، سرد می شود.
  • همگام سازی: نوشتن لاجگر ترافیک بالا به یک پرونده؟ Mutex آن را عاقل نگه داشت.

3.3 قدم خود را تماشا کنید

  • فاجعه بن بست: فراموش کن Unlock()، و شما نان تست می کنید:
func oops() {
    mu.Lock()
    counter++ // No Unlock—main goroutine hangs forever
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ثابت کردن: defer mu.Unlock()بشر آن را تنظیم کنید و فراموش کنید.

  • وحشت بازگشتی: Mutex از لانه سازی متنفر است:
func recursive() {
    mu.Lock()
    defer mu.Unlock()
    recursive() // Panic: deadlock
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ثابت کردن: نکن از کانال ها یا Refactor استفاده کنید.

نکات طرفدار:

  • همیشه defer بازه های شما
  • دویدن go run -race‘این یک پیاده روی مسابقه شما است.

3.4 دست انداز سرعت

چرخه های CPU هزینه قفل. قفل بیش از حد یک بار توان یک پروژه را مخزن کرد – دامنه و رونق ، 30 ٪ سریعتر. قفل محکم ، اما مختصر.


4. RWMUTEX: قفل هوشمند برای روزهای خواندن سنگین

اگر Mutex تندر “یک بار” است ، RWMutex دروازه بان زرنگ و دانا است: به جمعیتی از خوانندگان اجازه می دهد اما در را برای یک نویسنده تنها قفل می کند. این یک Rockstar همزمان برای تنظیمات سنگین است-اما این اتفاق می افتد. بیایید آن را تجزیه کنیم.

4.1 rwmutex در عمل

RWMutex دو حالت دارد: قفل را بخوانید (RLock/RUnlock) برای خواندن موازی ، و نوشتن قفل (Lock/Unlock) برای دسترسی انحصاری. این مثال پیکربندی مشترک را بررسی کنید:

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    rwmu   sync.RWMutex
    config = map[string]string{"version": "1.0"}
)

func readConfig(id int) {
    rwmu.RLock()
    defer rwmu.RUnlock()
    fmt.Printf("Goroutine %d read: %s\n", id, config["version"])
    time.Sleep(100 * time.Millisecond) // Fake some work
}

func updateConfig(version string) {
    rwmu.Lock()
    defer rwmu.Unlock()
    config["version"] = version
    fmt.Println("Updated to:", version)
    time.Sleep(200 * time.Millisecond)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ { // Readers galore
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            readConfig(id)
        }(i)
    }
    wg.Add(1)
    go func() { // One writer
        defer wg.Done()
        time.Sleep(50 * time.Millisecond)
        updateConfig("2.0")
    }()
    wg.Wait()
    fmt.Println("Final:", config["version"])
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

چه اتفاقی می افتد: خوانندگان در کنار هم جمع می شوند ، سپس نویسنده پس از اتمام کار می کند. خروجی؟ خواندن موازی ، نوشتن را تمیز کنید:

Goroutine 0 read: 1.0
Goroutine 1 read: 1.0
Goroutine 2 read: 1.0
Goroutine 3 read: 1.0
Goroutine 4 read: 1.0
Updated to: 2.0
Final: 2.0
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

چرا سنگ می زند: خواندن همزمان = سرعت.

4.2 برنده در دنیای واقعی

  • داشبورد آمار: تعداد بازدید از URL در نقشه 99 ٪ خوانده شده ، نادر است. RWMutex بگذارید پرواز را بخواند ، افزایش توان 50 ٪ بیش از حد Mutexبشر
  • روتر پویا: یک دروازه میکروسرویس با یک جدول مسیریابی ساخته شده است. جستجوی فرکانس بالا؟ بدون عرق به روزرسانی های نادر؟ اداره شده QPS از 100k به 150k افزایش یافت.

4.3 تله برای جلوگیری از

RWMutex قدرتمند اما دزدکی است. اینجا جایی است که من گیر کرده ام – و چگونه می توانم کنار بگذارم:

  • بن بست به نوشتن: سعی کرد قفل خواندن را به قفل نوشتن ارتقا دهید:
func badUpdate() {
    rwmu.RLock()
    if config["version"] == "1.0" {
        rwmu.Lock() // Nope—deadlock
        config["version"] = "2.0"
        rwmu.Unlock()
    }
    rwmu.RUnlock()
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ثابت کردن: آن را تقسیم کنید:

func goodUpdate() {
    rwmu.RLock()
    needUpdate := config["version"] == "1.0"
    rwmu.RUnlock()
    if needUpdate {
        rwmu.Lock()
        defer rwmu.Unlock()
        if config["version"] == "1.0" { // Double-check
            config["version"] = "2.0"
        }
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

داستان: اشکال زدایی این در یک سرویس زنده یک کابوس بود – runtime.Stack() قهرمانان من بودند

  • باز کردن قفل: یک قفل نوشتن را آویزان کرد:
func oopsWrite() {
    rwmu.Lock()
    if someError() {
        panic("boom") // No unlock—reads stuck
    }
    rwmu.Unlock()
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ثابت کردن: defer rwmu.Unlock()بشر ضد وحشت

  • مجازات بیش از حد: استفاده شده RWMutex با 40 ٪ می نویسد – عملکرد بدتر از Mutexبشر
    قاعده: اگر 30 ٪ برتر را می نویسد ، Mutex ممکن است دوست شما باشد. آن را مشخص کنید!

4.4 عملکرد رو در رو

معیار سریع را اجرا کرد:

package main

import (
    "sync"
    "testing"
)

var (
    mu    sync.Mutex
    rwmu  sync.RWMutex
    value int
)

func BenchmarkMutex(b *testing.B) {
    for i := 0; i < b.N; i++ {
        mu.Lock()
        value++
        mu.Unlock()
    }
}

func BenchmarkRWMutexRead(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rwmu.RLock()
        _ = value
        rwmu.RUnlock()
    }
}

func BenchmarkRWMutexWrite(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rwmu.Lock()
        value++
        rwmu.Unlock()
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

نتایج (NS/OP):

  • Mutex: ~ 20ns
  • RWMutex Read: ~ 10ns (50 ٪ سریعتر!)
  • RWMutex Write: 25ns ~ (کمی کندتر)

حکم: RWMutex آن را برای خواندن می کشد ، اما می نویسد مالیات اندکی پرداخت می کند. در یک سیستم خواندن 90 ٪ ، تأخیر از 50ms به 20ms کاهش یافته است. بالا می نویسد؟ به Mutexبشر


5. Lock Smarts: بهترین شیوه ها و زخم های نبرد

شما Mutex وت RWMutex زیر کمربند خود – اجازه دهید مهارت های شما را تیز کنیم. قفل ها مانند سس داغ هستند: کمی طعم آن را اضافه می کند ، بیش از حد غذا را خراب می کند. در اینجا یک دهه خرد و خرد شده به اصول ، نکات حرفه ای و لحظات “اوه” برای شما یک قهرمان همزمان است.

5.1 دستورات قفل

  • محکم نگه دارید: طاق را قفل کنید ، نه بانک. فقط بیت های بحرانی را بپیچید.
  • بدون لانه سازی: قفل های تو در تو طعمه بن بست هستند – مانند دو کودک نوپا که با یک اسباب بازی می جنگند. یک قفل مقدار زیادی.
  • ابله: go vet قفل های بدون زوج را می گیرد. go run -race نژادها را خراب می کند. از آنها استفاده کنید.

مانترا: قفل ها ابزارهای جراحی هستند – مناسب و نادر.

5.2 حرکات حرفه ای که پرداخت می کنند

  • دامنه قفل باریک: پیر من بیش از حد قفل شده است:
var (
    mu    sync.Mutex
    tasks []string
)

func process() {
    mu.Lock()
    if len(tasks) > 0 {
        task := tasks[0]
        tasks = tasks[1:]
        mu.Unlock()
        fmt.Println(task)
    } else {
        mu.Unlock()
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

جدید من:

func processOptimized() {
    var task string
    mu.Lock()
    if len(tasks) > 0 {
        task = tasks[0]
        tasks = tasks[1:]
    }
    mu.Unlock()
    if task != "" {
        fmt.Println(task)
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پیروز شدن: 40 ٪ افزایش توان. قفل کمتر ، بیشتر برنده شوید.

  • آن را ببندید: قفل + داده = bffs:
type SafeCounter struct {
    mu sync.Mutex
    n  int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

func (c *SafeCounter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

چرا: پاک کننده ، ایمن تر. این کار را برای حافظه نهان استفاده کرد – Bugs از بین رفت.

  • کانال های روی قفل: مبادله این:
func produce() {
    mu.Lock()
    queue = append(queue, 1)
    mu.Unlock()
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای این:

ch := make(chan int, 10)
go func() { ch <- 1 }()
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

کی: کانال ها جریان داده ها را حاکم می کنند. قفل چیزهای استاتیک نگهبان.

5.3 داستان جنگ

  • قفل چربی شکست خورد: یک لاجگر کامل قفل شده است – صد نفر از Ops/Sec. قفل های بسته بندی شده براساس دسته – هزار هزار نفر. ثابت کردن: دامنه مهم است.
  • BUST RWMUTEX: 50 ٪ می نویسد کشته شده RWMutexبشر مبادله شده به Mutex، تأخیر 20 ٪ سقوط کرد. ثابت کردن: نسبت های خود را مشخص کنید.
  • کمین بن بست: دو قفل ، دو گوروتین ، کل یخ. runtime.Stack() من را وثیقه داد ثابت کردن: یک قفل یا زمان.

امتیاز: sync/atomic می تواند قفل ها را برای پیشخوان های ساده – لکه دار و نرم و صاف پرش کند.


6. بسته بندی: آن را قفل کنید ، سطح بالا را بالا ببرید

Mutex آیا سپر قابل اعتماد شماست – ساده ، جامد. RWMutex نینجا با سرعت خواندن-tricky اما کلاچ است. قفل ها هرج و مرج را در معرض خطر قرار می دهند ، اما آنها رایگان نیستند: دامنه درست است ، یا قیمت آن را پرداخت می کنند. قانون من: قفل ها ابزاری هستند ، نه نوار مجرای. تست ، ترفند ، پیروزی.

همزمانی Go در حال تحول است –sync ممکن است خیالی تر شود ، اما کانال ها و ترفندهای بدون قفل آینده هستند. برای آب بیشتر به اسناد GO یا “همزمانی در GO” بروید. من؟ من می گویم لاغر در کانال هایی که در آن می توانید – قفل کمتری ، تکان دهنده تر.

یک داستان ترسناک قفل یا ترفند نرم و صاف دارید؟ آن را در زیر رها کنید – من همه گوش ها را برای یک گپ همزمان!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا