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
و تشخیص شرایط مسابقه با استفاده از این تکنیک ها می توانید ایمنی برنامه های همزمان خود را افزایش دهید.