مشکلات معمول هنگام استفاده از گوروتین ها

توسط mrbardia72
زمان خواندن 2 دقیقه
مشکلات معمول هنگام استفاده از گوروتین ها

در این پست قصد دارم در موارد و اتفاقات رایجی  که احتمالاً هنگام استفاده از goroutines ها و نحوه برخورد با آنها را تجربه خواهید کرد را بیان کنیم.

معرفی

برای دستیابی به همزمانی در Go از goroutines استفاده می کنیم - توابع یا روش هایی که همزمان با توابع یا روش های دیگر اجرا می شوند. goroutines را می توان به عنوان threads های سبک مشاهده کرد ، اما برخلاف threads ها ، آنها توسط سیستم عامل مدیریت نمی شوند بلکه توسط زمان اجرا Golang مدیریت می شوند.

بیایید با یک مثال شروع کنیم و یک فایل  hello.go ایجاد کنیم:

func hello() {
    fmt.Println("Hello")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Waiting

بیایید با یک مثال ساده شروع کنیم:

همانطور که ممکن است متوجه شده باشید ، استفاده از زمان. هنگام نشان دادن عملکرد اساسی گوروتین ها ، بسیار رایج است.

پس چرا اینجا sleep ضروری است؟ بیایید بدون وقت بررسی کنیم. عملکرد sleep را :

func hello() {
    fmt.Println("Hello")
}
func main() {
    go hello()
     time.Sleep(1 * time.Second)   
    fmt.Println("main function")
}

هم ، اکنون خروجی goroutine وجود ندارد. چرا؟

زیرا اجرای برنامه با مقداردهی اولیه بسته اصلی و سپس فراخوانی عملکرد اصلی آغاز می شود. وقتی آن فراخوانی عملکرد بازگردد ، برنامه خارج می شود. سپس منتظر کامل شدن سایر goroutine های غیر اصلی نمی شود. این بدان معنی است که وقتی عملکرد اصلی آن را به پایان می رساند ، منتظر نمی ماند تا سایر goroutine ها به پایان برسند.

بنابراین اکنون که ضرورت waiting برای پایان یافتن سایر goroutine ها را فهمیدیم ،تنها روش کارآمد برای waiting و پایان دادن به goroutine چه روشی هست؟

روشی به نام  WaitGroups به آن  گفته می شود.

WaitGroups به ما اجازه می دهد تا زمانی که تمام goroutine داخل آن گروه انتظار اجرای خود را به پایان برسانند ، مسدود کنیم.

نمونه ای از اجرای WaitGroup:

func hello(wgrp *sync.WaitGroup) {
    fmt.Println("Hello")
    wgrp.Done()
}
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go hello(&wg)
    wg.Wait()
    fmt.Println("main function")
}

این روش بهتر و سریعتر هست  ، زیرا مجبور نیستیم مدت زمان مشخصی منتظر بمانیم.

Deadlocks

ممکن است قبلاً به چینین خطای برخورده باشید

fatal error: all goroutines are asleep - deadlock!

بن بست زمانی اتفاق می افتد که گروهی از گوروتین ها منتظر یکدیگر هستند و هیچ یک قادر به ادامه کار نیستند.

به یاد داشته باشید ، بسته اصلی نیز گوروتین است.

func hello(wgrp *sync.WaitGroup) {
    fmt.Println("Hello")
    wgrp.Done()
}
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go hello(&wg)
    wg.Wait()
    fmt.Println("main function")
}

1. wgrp.Done زمانی که اجرای goroutine ها به  اتمام رسیده

2. wg.Add  تعداد goroutine هایی را که باید منتظر آن باشیم.

مقادیر ممکن:

0 یعنی goroutine اجرا نمی شود

1 مطابق انتظار کار خواهد کرد

2 و بالاتر منجر به بن بست خواهد شد

مورد دیگری که در آن به بن بست خواهید رسید ، این است که هیچ نوع goroutine دیگری برای ارسال آنچه فرستنده ارسال می کند وجود ندارد ، زیرا این اتفاق نمی تواند در همان دامنه خاص رخ دهد:

func main() {
    c := make(chan string)
    c <- "hello"
    fmt.Println(<-c)
}

در عوض ، این کار را انجام دهید:

func main() {
    c := make(chan string)
    go func() {
        get := <-c                     
        fmt.Println("get value:", get)
    }()
    fmt.Println("push to channel c")
    c <- "hello"
}

Unexpected results

افزودن حلقه for به ترکیب:

func main() {
    var wg sync.WaitGroup
    players := []string{"aaa", "bbb", "ccc"}
    wg.Add(len(players))
for _, player := range players {
        go func() {
            fmt.Printf("printing player %s\n", player)
            wg.Done()
        }()
    }
    wg.Wait()
}
$ go run hello.go
printing player aaa
printing player aaa
printing player aaa

آیا قرار نیست هر بار تکرار نام های مختلف چاپ شود؟خوب .. چنین است ، اما goroutines ایجاد شده در داخل حلقه for لزوماً به طور پی در پی اجرا نمی شوند.هر goroutine به طور تصادفی شروع می شود. راه حل کاملاً ساده است ، ما فقط مورد فعلی تکرار را منتقل می کنیم:

func main() {

    var wg sync.WaitGroup
    
    players := []string{"James", "Kyrie", "Kevin"}
    
    wg.Add(len(players))
    
for _, player := range players {

        go func(baller string) {
            fmt.Printf("printing player %s\n", baller)
            wg.Done()
            
        }(player)
    }
    
    wg.Wait()
}
$ go run hello.go
printing player James
printing player Kevin
printing player Kyrie

این بهتر است

Race conditions

اکنون میخواهیم  کمی پیچیده تر کنیم: تصور کنید که شما یک برنامه بانکی دارید ، جایی که مشتری می تواند پول را واریز و برداشت کند. تا زمانی که برنامه تک رشته ای و همزمان باشد ، مشکلی پیش نخواهد آمد ، اما اگر برنامه شما صدها یا هزاران goroutines داشته باشد چه اتفاقی می افتد؟

این سناریو را در نظر بگیرید:

مانده مشتری 100 دلار است و 50 دلار به حساب خود واریز می کند. یک goroutines معاملات را می بیند ، مانده فعلی 100 دلار را می خواند و 50 دلار به موجودی اضافه می کند. اما صبر کنید ، دقیقاً در همان زمان مبلغ 80 دلار به حساب مشتری برای پرداخت قبض وی در بار محلی اعمال شده است. دومین حالت موجودی 100 دلار را در آن زمان می خواند ، 80 دلار از حساب کم می کند و مانده حساب را به روز می کند. سپس مشتری مانده حساب خود را بررسی می کند و می بیند که این مبلغ به جای 70 دلار فقط 20 دلار است ، زیرا goroutine دوم هنگام پردازش معامله ، مقدار موجودی را رونویسی می کند. برای حل این مسئله ، می توانیم از Mutex استفاده کنیم. موتکس؟ Mutex روشی است که به عنوان مکانیزم قفل گذاری برای اطمینان از دستیابی فقط یک Goroutine به بخش مهم کد در هر زمان مورد استفاده قرار می گیرد.

کد زیر را درنظر بگیرید:

var (
    mutex   sync.Mutex
    balance int
)
func init() {
    balance = 100
}
func deposit(val int, wg *sync.WaitGroup) {
    mutex.Lock()
    balance += val
    mutex.Unlock()
    wg.Done()
}
func withdraw(val int, wg *sync.WaitGroup) {
    mutex.Lock()
    balance -= val
    mutex.Unlock()
    wg.Done()
}
func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    go deposit(20, &wg)
    go withdraw(80, &wg)
    go deposit(40, &wg)
    wg.Wait()
    fmt.Printf("Balance is: %d\n", balance)
}

$ go run mutex.go
Balance is: 80

راه دیگری برای حل آن وجود دارد ، این بار با استفاده از کانال ها.

کانال ها لوله هایی هستند که گوروتین های همزمان را به هم متصل می کنند. می توانید مقادیر را از یک goroutine به کانالها ارسال کرده و آن مقادیر را به goroutine دیگر دریافت کنید.در این مثال ، ما از یک کانال بافر استفاده می کنیم. این کانال بافر برای اطمینان از اینکه فقط یک goroutine می تواند به بخش مهم کد دسترسی پیدا کند ، استفاده می شود

var (
    balance int
)
func init() {
    balance = 100
}
func deposit(val int, wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    balance += val
    <-ch
    wg.Done()
}
func withdraw(val int, wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    balance -= val
    <-ch
    wg.Done()
}
func main() {
    var wg sync.WaitGroup
    ch := make(chan bool, 1)
    wg.Add(3)
    go deposit(20, &wg, ch)
    go withdraw(80, &wg, ch)
    go deposit(40, &wg, ch)
    wg.Wait()
    fmt.Printf("Balance is: %d\n", balance)
}
$ go run buf.go 
Balance is: 80

ما یک کانال بافر با ظرفیت 1 ایجاد کرده ایم ، زیرا می خواهیم موجودی را فقط یک بار در هر عملیات تغییر دهیم ، و این به Goroutines واریز / برداشت می رسد.

بنابراین کدام یک را انتخاب کنیم؟

معمولاً در مواقعی که Goroutines نیاز به برقراری ارتباط با یکدیگر دارند از کانالها استفاده کنید و درصورتی که فقط یک Goroutine باید به بخش مهم کد دسترسی داشته باشد از Mutexes استفاده کنید. در این حالت ، بهترین روش استفاده از Mutex است.