برنامه نویسی

Sync.Mutex در GO: یک مطالعه

پست قبلی این بار ، من کاوش کردم sync.Mutex برای تعمیق درک من در مورد چگونگی کار Mutex در GO.


نمای کلی

با استفاده از sync.Mutex از نژادهای داده ای که می تواند باعث ایجاد مشکلات شود ، جلوگیری می کند ، مانند به روزرسانی ها که به درستی یا رفتارهای غیر منتظره اعمال نمی شوند. به طور خاص ، با قرار دادن Lock وت Unlock تماس های مربوط به دسترسی به منابع مشترک ، می توانید از ناسازگاری بین چندین گوروتین جلوگیری کنید.

شما همچنین می توانید از -race گزینه ای برای تشخیص مسابقات داده در برنامه خود. استفاده از این گزینه در حین توسعه یا آزمایش به گرفتن اشکالات در مراحل اولیه کمک می کند.


درباره sync.mutex

https://pkg.go.dev/sync#mutex

شروع از Go 1.18 ، TryLock اضافه شد ، بنابراین اکنون Mutex سه روش دارد:

  • func (m *Mutex) Lock()
  • func (m *Mutex) TryLock() bool
  • func (m *Mutex) Unlock()

به طور معمول ، Lock وت Unlock کافی هستند TryLock بلافاصله برمی گردد false اگر نتواند قفل را بدست آورد ، اما همانطور که در اسناد رسمی GO بیان شد ، مورد استفاده آن نادر است. اگر طراحی شما مناسب است ، فقط Lock وت Unlock بیشتر سناریوها را اداره می کند.


مثال با استفاده از mutex

در زیر نمونه ای از نمایش “نمایشگر پیشرفت مانند Docker” در حالی که آزمایش برای مسابقات داده های بالقوه را نشان می دهد ، آورده شده است. این بار ، می توانید از طریق آرگومان خط فرمان استفاده کنید که آیا از mutex استفاده کنید یا خیر.

هنگامی که از mutex استفاده می شود ، متغیر مشترک به درستی تعداد مشخص شده ها را به درستی افزایش می دهد. با این حال ، اگر MUTEX را غیرفعال کنید ، این روند ممکن است با تعداد زیر شماره مورد نظر به دلیل درگیری های گوروتین به پایان برسد (افزایش استدلال خط فرمان اول اغلب مشاهده این امر را آسان تر می کند).

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    // set concurrency count
    if len(os.Args) < 3 {
        fmt.Println("Usage: go run main.go [number] [useMutex]")
        return
    }
    maxLoopCount, err := strconv.Atoi(os.Args[1])
    if err != nil || maxLoopCount <= 0 {
        fmt.Println("Invalid number. Please provide a positive integer.")
        return
    }

    useMutex, err := strconv.ParseBool(os.Args[2])
    if err != nil {
        fmt.Println("Invalid string. Please provide a true or false.")
        return
    }

    // A mutex to prevent conflicts during progress updates.
    var mu sync.Mutex

    // WaitGroup for waiting until each goroutine has completed
    var wg sync.WaitGroup

    loopCount := 0
    for i := 0; i < maxLoopCount; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
            if useMutex {
                mu.Lock()
                loopCount++
                mu.Unlock()
            } else {
                loopCount++
            }
        }()
    }

    // A channel to signal that all goroutines have finished
    done := make(chan struct{})
    go func() {
        wg.Wait()
        done <- struct{}{}
    }()

    // Periodically draw the progress status on the screen
    ticker := time.NewTicker(10 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            // Clear the screen and redraw
            printProgress(maxLoopCount, loopCount, &mu)
        case <-done:
            // Display the final state and exit
            printProgress(maxLoopCount, loopCount, &mu)
            return
        }
    }
}

// printProgress is a function that displays the progress of each indicator
// Prevent concurrent access to progress data using a mutex
func printProgress(maxLoopCount int, loopCount int, mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()

    // Clear the screen using ANSI escape sequences
    // \033[H : Move the cursor to the home position
    // \033[2J : Clear the screen
    fmt.Print("\033[H\033[2J")
    fmt.Printf("max loop count: %d\n", maxLoopCount)
    fmt.Printf("current loop count: %d\n", loopCount)

    // Set the total width of the progress bar to 50 characters
    width := 50
    progress := int((float64(loopCount) / float64(maxLoopCount)) * 100)

    // Number of "*" characters based on progress percentage
    stars := progress * width / 100
    spaces := width - stars
    fmt.Printf("%d.[%s%s] %d%%\n", 1, repeat("*", stars), repeat(" ", spaces), progress)
}

// repeat: Returns concatenated string by repeating string "s" "count" times
func repeat(s string, count int) string {
    result := ""
    for i := 0; i < count; i++ {
        result += s
    }
    return result
}
حالت تمام صفحه را وارد کنید

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

نمایش برنامه

همانطور که نشان داده شده است ، آیا شما از mutex استفاده می کنید یا نه می تواند به طور قابل توجهی بر نتیجه تأثیر بگذارد. اگر چندین گوروتین بدون هماهنگ سازی به داده های مشترک بنویسند ، احتمالاً نژادهای داده به احتمال زیاد رخ می دهد.


گزینه مسابقه

ردیاب مسابقه داده

مسابقات داده ها از رایج ترین و سخت ترین انواع اشکالات اشکالات موجود در سیستم های همزمان هستند. یک مسابقه داده زمانی اتفاق می افتد که دو گوروتین به طور همزمان به همان متغیر دسترسی پیدا کنند و حداقل یکی از دسترسی ها یک نوشتن باشد.

برای کمک به تشخیص چنین اشکالات ، GO شامل یک ردیاب مسابقه داده داخلی است. برای استفاده از آن ، اضافه کنید -race پرچم به دستور Go:

به عنوان مثال ، اگر دویدید go run -race main.go 100000 false در برنامه نمونه بالا ، اگر شرایط مسابقه وجود داشته باشد ، ممکن است خروجی مانند موارد زیر را مشاهده کنید:

==================
WARNING: DATA RACE
Read at 0x00c000126048 by goroutine 417:
  main.main.func1()
      /path/to/main.go:47 +0xe8

Previous write at 0x00c000126048 by goroutine 23:
  main.main.func1()
      /path/to/main.go:47 +0xf8

Goroutine 417 (running) created at:
  main.main()
      /path/to/main.go:39 +0x440

Goroutine 23 (finished) created at:
  main.main()
      /path/to/main.go:39 +0x440
==================
حالت تمام صفحه را وارد کنید

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

شما همچنین می توانید از -race گزینه با go test یا go buildبشر اگر کد شما از بسیاری از goroutines استفاده می کند ، نگه داشتن ردیاب مسابقه در حین توسعه یا آزمایش می تواند به یافتن زودرس مسابقه داده کمک کند.


درباره Trylock

همانطور که قبلاً اشاره شد ، sync.Mutex در GO 1.18 و بعداً شامل TryLock روش اگر قفل قابل دستیابی نباشد ، TryLock بازگرداندن false بلافاصله به جای مسدود کردن ، برخلاف Lockبشر با این حال ، اسناد رسمی ذکر می کند که استفاده از موارد برای این ویژگی نادر است. همانطور که در مثال زیر نشان داده شده است ، سوء استفاده TryLock می تواند به منطق غیر ضروری پیچیده منجر شود ، بنابراین احتیاط توصیه می شود.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex

    fmt.Println("Start: Lock - Unlock")
    mu.Lock()
    mu.Unlock()
    fmt.Println("End: Lock - Unlock")
    fmt.Println("---")

    fmt.Println("Start: TryLock - Unlock")
    b := mu.TryLock()
    fmt.Printf("TryLock() result is %v\n", b)
    mu.Unlock()
    fmt.Println("End: TryLock - Unlock")
    fmt.Println("---")

    fmt.Println("Start: Lock - TryLock - Unlock")
    mu.Lock()
    b = mu.TryLock()
    fmt.Printf("TryLock() result is %v\n", b)
    mu.Unlock()
    fmt.Println("End: Lock - TryLock - Unlock")
    fmt.Println("---")

    fmt.Println("Start: TryLock - TryLock - Unlock")
    b = mu.TryLock()
    fmt.Printf("TryLock()1 result is %v\n", b)
    b = mu.TryLock()
    fmt.Printf("TryLock()2 result is %v\n", b)
    mu.Unlock()
    fmt.Println("End: TryLock - TryLock - Unlock")
    fmt.Println("---")

    fmt.Println("Start: Lock - Lock - Unlock")
    mu.Lock()
    mu.Lock() // This causes a deadlock
    mu.Unlock()
    fmt.Println("End: Lock - Lock - Unlock")

    fmt.Println("Done.")
}
حالت تمام صفحه را وارد کنید

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

اجرای این برنامه:

$ go run trylock.go
Start: Lock - Unlock
End: Lock - Unlock
---
Start: TryLock - Unlock
TryLock() result is true
End: TryLock - Unlock
---
Start: Lock - TryLock - Unlock
TryLock() result is false
End: Lock - TryLock - Unlock
---
Start: TryLock - TryLock - Unlock
TryLock()1 result is true
TryLock()2 result is false
End: TryLock - TryLock - Unlock
---
Start: Lock - Lock - Unlock
fatal error: all goroutines are asleep - deadlock!
حالت تمام صفحه را وارد کنید

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

در حالی که تماس می گیرد Lock دوباره داخل یک بخش قفل شده باعث بن بست می شود ، TryLock مسدود نمی شود و برمی گردد false در عوض ، از این رو از بن بست جلوگیری می شود. شما باید با دقت در نظر بگیرید که این رفتار در واقع مورد نیاز است.


پایان

  • برای جلوگیری از مسابقات داده از mutexes استفاده کنید

    • اگر چندین گوروتین به طور همزمان به داده های به اشتراک گذاشته شده بدون هماهنگ سازی دسترسی پیدا کنند ، نژادهای داده بیشتر محتمل هستند.
    • شرایط مسابقه همچنین می تواند در سناریوهای دیگر مانند به اشتراک گذاری داده ها از طریق کانال ها رخ دهد.
  • از -race گزینه ای برای تشخیص مسابقات داده

    • در حین توسعه و آزمایش ، به کشف زودهنگام مسائل کمک می کند.
  • TryLock بندرت مورد نیاز است قفل/قفل به طور کلی کافی است

    • در بیشتر موارد ، طراحی مناسب فقط با قفل/باز کردن قابل استفاده است.

که اصول استفاده را پوشش می دهد sync.Mutex و تشخیص شرایط مسابقه با استفاده از این تکنیک ها می توانید ایمنی برنامه های همزمان خود را افزایش دهید.

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

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

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

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