نحوه مدیریت همزمانی با گوروتین ها و کانال ها در Go
همزمانی توانایی یک برنامه برای انجام چندین کار به طور همزمان است. این یک جنبه حیاتی در ساخت سیستم های مقیاس پذیر و پاسخگو است.
مدل همزمانی Go مبتنی بر مفهوم گوروتینها، رشتههای سبک وزن است که میتوانند چندین عملکرد را به طور همزمان اجرا کنند، و کانالها، یک مکانیسم ارتباطی داخلی برای تبادل اطلاعات ایمن و کارآمد بین گوروتینها.
ویژگی های همزمانی Go به توسعه دهندگان امکان می دهد برنامه هایی بنویسند که می توانند:
رسیدگی به چندین درخواست به طور همزمان، بهبود پاسخگویی و توان عملیاتی.
از پردازنده های چند هسته ای به طور موثر استفاده کنید و منابع سیستم را به حداکثر برسانید.
کدی همزمان بنویسید که ایمن، کارآمد و نگهداری آسان باشد.
مدل همزمانی Go برای به حداقل رساندن سربار، کاهش تأخیر و جلوگیری از خطاهای رایج همزمانی مانند شرایط مسابقه و بن بست طراحی شده است.
با Go، توسعهدهندگان میتوانند به راحتی سیستمهایی با کارایی بالا، مقیاسپذیر و همزمان بسازند، که آن را به گزینهای ایدهآل برای ساخت سیستمها، شبکهها و زیرساختهای ابری مدرن تبدیل میکند.
فهرست مطالب
نحوه خواندن داده ها از یک کانال
نحوه پیاده سازی کانال ها با Goroutine
چگونه یک کانال بافر ایجاد کنیم
نحوه مدیریت چندین عملیات ارتباطی با انتخاب کانال
چگونه می توان فرآیندهای طولانی مدت را در یک کانال زمان بندی کرد
بیایید سناریویی را برای نشان دادن همزمانی در نظر بگیریم:
مطالعه موردی: عابر بانک
یک بانک شلوغ با دو عابر بانک، ماریا و دیوید را تصور کنید. مشتریان برای انجام معاملات مختلف مانند سپرده، برداشت و نقل و انتقال به بانک می آیند. هدف خدمات رسانی سریع و کارآمد به مشتریان است.
پردازش متوالی (بدون همزمانی)
ماریا و دیوید به صورت متوالی و یکی یکی کار می کنند. وقتی مشتری می آید، ماریا به مشتری کمک می کند و دیوید منتظر می ماند تا ماریا تمام شود و به مشتری بعدی کمک می کند. این منجر به زمان انتظار طولانی برای مشتریان می شود.
همزمانی
ماریا و دیوید به طور همزمان کار می کنند و به طور همزمان به مشتریان خدمات ارائه می دهند. هنگامی که مشتری می آید، ماریا در انجام یک معامله به مشتری کمک می کند و دیوید به طور همزمان به مشتری دیگری در معامله متفاوت کمک می کند. آنها با هم کار می کنند و منابعی مانند پایگاه داده بانک و منابع نقدی را به اشتراک می گذارند تا همزمان به چندین مشتری خدمات رسانی کنند.
در این سناریو، همزمانی ماریا و دیوید را قادر میسازد تا به طور موثر با هم کار کنند، به طور همزمان به چندین مشتری خدمت کنند و تجربه کلی مشتری را بهبود بخشند. همین مفهوم در مورد برنامه نویسی رایانه نیز صدق می کند، جایی که همزمانی چندین کار را قادر می سازد تا به طور همزمان اجرا شوند و پاسخگویی، کارایی و عملکرد را بهبود می بخشند.
گوروتین ها و کانال ها چیست؟
Goroutine یک رشته سبک وزن است که توسط زمان اجرا Go مدیریت می شود. این تابعی است که در زمان اجرا Go اجرا می شود. این به رسیدگی به الزامات جریان همزمان و ناهمگام کمک می کند.
گوروتین ها به شما این امکان را می دهند که رشته های دیگر اجرا را همزمان در برنامه خود راه اندازی و اجرا کنید.
از کانال ها برای برقراری ارتباط بین گوروتین ها استفاده می شود. این یک مجرای تایپ شده است که از طریق آن می توانید مقادیر را با اپراتور کانال ارسال و دریافت کنید: <-
.
نحوه اجرای گوروتین
برای استفاده و پیاده سازی یک goroutine
، از کلمه کلیدی go
قبل از یک تابع استفاده می شود.
package main import ( "fmt" "math/rand" "time" ) func pause() { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) } func sendMsg(msg string) { pause() fmt.Println(msg) } func main() { sendMsg("hello") // sync go sendMsg("test1") // async go sendMsg("test2") // async go sendMsg("test3") // async sendMsg("main") // sync time.Sleep(2 * time.Second) }
از مثال بالا،
تابع sendMsg
به صورت همزمان و ناهمزمان فراخوانی می شود.
وقتی تابع sendMsg
بدون کلمه کلیدی go
فراخوانی شود، تابع sendMsg
به صورت همزمان فراخوانی می شود.
هنگامی که تابع sendMsg
با کلمه کلیدی go
فراخوانی شود، تابع sendMsg
به صورت ناهمزمان فراخوانی می شود.
گوروتین چگونه کار می کند؟
هنگامی که تابع sendMsg
با کلمه کلیدی go
فراخوانی می شود، تابع main
قبل از ادامه اجرای آن به خط بعدی کد منتظر نمی ماند تا عملکرد sendMsg
به پایان برسد و بلافاصله پس از فراخوانی تابع sendMsg
برمی گردد.
در غیر این صورت، تابع به صورت همزمان فراخوانی می شود و تابع main
قبل از اینکه به خط بعدی کد ادامه دهد، منتظر می ماند تا عملکرد sendMsg
به پایان برسد.
ترتیب خروجی هنگام اجرای مثال بالا با ترتیب کد متفاوت است زیرا این سه مورد goroutine
همه به طور همزمان اجرا می شوند و از آنجایی که توابع برای مدتی متوقف می شوند، ترتیبی که آنها بیدار می شوند متفاوت است و خروجی می شود.
time.Sleep(2 * time.Second)
یک روش سریع و ساده است که برای اجرای تابع اصلی به مدت 2 ثانیه استفاده می شود تا به goroutine
اجازه دهد قبل از خروج تابع اصلی، اجرا را به پایان برساند. در غیر این صورت، تابع اصلی بلافاصله پس از فراخوانی goroutine
خارج می شود و goroutine
زمان کافی برای اتمام اجرای خطاهای منتج از آن را نخواهد داشت.
WaitGroups چیست؟
برخلاف time.Sleep(2 * time.Second)
که در مثال بالا استفاده شد، WaitGroups
استانداردتر هستند تا منتظر بمانند تا مجموعه ای از گوروتین ها اجرا شوند. این یک راه ساده برای همگام سازی چندین گوروتین است.
یک گوروتین همچنین می تواند با توابع ناشناس اعلام شود
package main import ( "fmt" "sync" "time" "math/rand" ) func pause() { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) } func sendMsg(msg string, wg *sync.WaitGroup) { defer wg.Done() pause() fmt.Println(msg) } func main() { var wg sync.WaitGroup wg.Add(3) go func(msg string) { defer wg.Done() pause() fmt.Println(msg) }("test1") go sendMsg("test2", &wg) go sendMsg("test3", &wg) wg.Wait() }
از مثال بالا، sync.WaitGroup
برای منتظر ماندن برای پایان اجرای سه goroutine
قبل از خروج تابع اصلی استفاده می شود. این سه goroutine
و عملکرد اصلی را همگام می کند.
sync.WaitGroup (wg)
گوروتین ها را مدیریت می کند و تعداد گوروتین های در حال اجرا را پیگیری می کند.
روش sync.WaitGroup.Add (wg.Add)
برای اضافه کردن تعداد گوروتین ها به عنوان آرگومان های در حال اجرا استفاده می شود.
روش sync.WaitGroup.Done (wg.Done)
برای کاهش تعداد گوروتین های در حال اجرا استفاده می شود.
متد sync.WaitGroup.Wait (wg.Wait)
برای منتظر ماندن برای پایان اجرای همه گوروتین ها قبل از خروج تابع اصلی استفاده می شود.
کانال ها چیست؟
از کانال ها برای برقراری ارتباط بین گوروتین ها استفاده می شود. این یک مجرای تایپ شده است که از طریق آن می توانید پیام ها را با اپراتور کانال ارسال و دریافت کنید، <-
.
در سادهترین شکل، یک گوروتین پیامها را در کانال مینویسد و گوروتین دیگر همان پیامها را خارج از کانال میخواند.
کانال ها با استفاده از روش make
و کلمه کلیدی chan
همراه با نوع آن ایجاد می شوند. از کانال ها برای انتقال پیام هایی استفاده می شود که با کدام نوع اعلام شده است.
مثال:
package main func main(){ msgChan := make(chan string) }
مثال بالا یک کانال msgChan
از نوع string
ایجاد می کند.
نحوه نوشتن داده در کانال
برای نوشتن داده در یک کانال، ابتدا نام ( msgChan
) کانال و سپس اپراتور <-
و پیام را مشخص کنید. این فرستنده در نظر گرفته می شود.
msgChan <- "hello world"
نحوه خواندن داده ها از یک کانال
برای خواندن داده ها از یک کانال، به سادگی عملگر ( <-
) را به جلوی نام کانال ( msgChan
) ببرید و می توانید آن را به یک متغیر اختصاص دهید. این گیرنده در نظر گرفته می شود.
msg := <- msgChan
نحوه پیاده سازی کانال ها با Goroutine
package main import ( "fmt" "math/rand" "time" ) func main() { msgChan := make(chan string) go func() { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) msgChan <- "hello" // Write data to the channel msgChan <- "world" // Write data to the channel }() msg1 := <- msgChan msg2 := <- msgChan fmt.Println(msg1, msg2) }
مثال بالا نحوه نوشتن و خواندن داده ها از یک کانال را نشان می دهد. کانال msgChan
ایجاد می شود و از کلمه کلیدی go
برای ایجاد یک گوروتین استفاده می شود که داده ها را در کانال می نویسد. از متغیرهای msg1
و msg2
برای خواندن دادهها از کانال استفاده میشود.
کانال ها به عنوان صف first-in-first-out
عمل می کنند. پس ، هنگامی که یک گوروتین داده ها را در کانال می نویسد، گوروتین دیگر داده ها را از کانال به همان ترتیبی که نوشته شده است می خواند.
بافر کانال چیست؟
کانال ها را می توان buffered
یا unbuffered
. مثالهای قبلی شامل استفاده از کانالهای بافر نشده است.
کانال بافر نشده چیست؟
یک کانال بافر نشده باعث می شود که فرستنده بلافاصله پس از ارسال پیام به کانال مسدود شود تا زمانی که گیرنده پیام را دریافت کند.
کانال بافر چیست؟
یک کانال بافر به فرستنده این امکان را می دهد که پیام ها را بدون مسدود کردن به کانال ارسال کند تا زمانی که بافر پر شود. پس ، فرستنده تنها زمانی مسدود میشود که بافر پر شود و منتظر میماند تا گوروتین دیگری از کانال بخواند و مطمئن شود که اندازه فضا قبل از رفع انسداد در دسترس است.
چگونه یک کانال بافر ایجاد کنیم
هنگام ایجاد یک کانال بافر، از تابع make
استفاده کنید و پارامتر دوم را برای نشان دادن اندازه بافر مشخص کنید.
msgBufChan := make(chan string, 2)
مثال بالا یک کانال بافر msgBufChan
از نوع string
با اندازه بافر 2 ایجاد می کند. این بدان معنی است که کانال می تواند تا دو پیام را قبل از مسدود شدن نگه دارد.
package main import ( "time" ) func main() { size := 3 msgBufChan := make(chan int, size) // reader (receiver) go func() { for { _ = <- msgBufChan time.Sleep(time.Second) } }() //writer (sender) writer := func() { for i := 0; i <=> 10; i++ { msgBufChan <- i println(i) } } writer() }
مثال بالا یک کانال بافر msgBufChan
از نوع int
با اندازه بافر 3 ایجاد می کند.
تابع writer
داده ها را در کانال می نویسد و تابع reader
داده ها را از کانال می خواند.
هنگامی که برنامه اجرا می شود، خواهید دید که عدد 0 through to 3
بلافاصله چاپ می شود و اعداد باقیمانده 5 through to 10
به آرامی حدود یک در ثانیه چاپ می شوند ( time.Sleep(time.Second
).
این اثر کانال بافر شده را نشان می دهد که اندازه آن را قبل از مسدود شدن مشخص می کند.
مسیرهای کانال چیست؟
هنگام استفاده از کانال ها به عنوان پارامترهای عملکرد، به طور پیش فرض، می توانید پیام هایی را در داخل تابع ارسال و دریافت کنید. برای ایجاد ایمنی بیشتر در زمان کامپایل، پارامترهای تابع کانال را می توان با یک جهت تعریف کرد. یعنی می توان آنها را فقط خواندنی یا فقط نوشتاری تعریف کرد.
مثال:
package main import ( "fmt" "time" ) func writer(channel chan<- string, msg string) { channel <- msg } func reader(channel <-chan string) { msg := <- channel fmt.Println(msg) } func main() { msgChan := make(chan string, 1) go reader(msgChan) for i :- 0; i < 10; i++ { writer(msgChan, fmt.Sprintf("msg %d", i)) } time.Sleep(time.Second * 5) }
مثال بالا نحوه تعریف کانال با جهت را نشان می دهد.
تابع writer
با یک کانال فقط نوشتن و
تابع reader
با یک کانال فقط خواندنی تعریف می شود.
کانال msgChan
با اندازه بافر 1 ایجاد می شود. تابع writer
داده ها را در کانال می نویسد و تابع reader
داده ها را از کانال می خواند.
نحوه مدیریت چندین عملیات ارتباطی با انتخاب کانال
دستور select
به یک گوروتین اجازه می دهد تا در چند عملیات ارتباطی منتظر بماند. select
تا زمانی که یکی از کیس هایش اجرا شود مسدود می کند، سپس آن کیس را اجرا می کند. اگر چندین مورد آماده باشند، یکی را به صورت تصادفی انتخاب می کند.
دستور select
و case
برای ساده کردن مدیریت و خوانایی wait
در چندین کانال استفاده می شود.
مثال
package main import ( "fmt" "time" "math/rand" ) func pause() { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) } func test1(c chan<- string) { for { pause() c <- "hello" } } func test2(c chan<- string) { for { pause() c <- "world" } } func main() { rand.Seed(time.Now().Unix()) c1 := make(chan string) c2 := make(chan string) go test1(c1) go test2(c2) for { select { case msg1 := <- c1: fmt.Println(msg1) case msg2 := <- c2: fmt.Println(msg2) } } }
مثال بالا نحوه استفاده از دستور select
برای انتظار در چندین کانال نشان می دهد. توابع test1
و test2
داده ها را به ترتیب در کانال های c1
و c2
می نویسند. تابع main
داده ها را از کانال های c1
و c2
با استفاده از دستور select
می خواند.
دستور select تا زمانی که یکی از کانال ها آماده ارسال یا دریافت داده ها نباشد مسدود می شود. اگر هر دو کانال آماده باشند، دستور select یکی را به صورت تصادفی انتخاب می کند.
چگونه می توان فرآیندهای طولانی مدت را در یک کانال زمان بندی کرد
تابع time.After
برای ایجاد کانالی که پیامی را پس از مدت زمان مشخصی ارسال می کند استفاده می شود. این را می توان برای اجرای یک بازه زمانی برای یک کانال استفاده کرد.
میتوان آن را در یک عبارت select
برای کمک به مدیریت موقعیتهایی که دریافت پیام از هر یک از کانالهای تحت نظارت طول میکشد، مشخص کرد.
همچنین در هنگام کار با منابع خارجی استفاده از timeout
را در نظر بگیرید زیرا هرگز نمی توانید زمان پاسخگویی را تضمین کنید و پس ممکن است لازم باشد پس از گذشت زمان از پیش تعیین شده به طور فعال اقدام کنید.
اجرای timeout
با دستور select
بسیار ساده است.
مثال:
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) go func(channel chan string) { time.Sleep(1 * time.Second) channel <- "hello world" }(c1) select { case msg2 := <-c1: fmt.Println(msg2) case <-time.After(2 * time.Second): //Timeout after 2 second fmt.Println("timeout") } }
مثال بالا نحوه استفاده از time.After
را برای ایجاد کانالی نشان می دهد که پس از مدت زمان مشخصی پیامی را ارسال می کند.
تابع main
با استفاده از دستور select
داده ها را از کانال c1
می خواند.
دستور select
تا زمانی که یکی از کانال ها برای ارسال یا دریافت داده آماده شود مسدود می شود.
اگر کانال c1
آماده باشد، تابع main
پیام را چاپ می کند.
اگر کانال c1
بعد از 2 ثانیه آماده نباشد، تابع main
یک پیام مهلت زمانی را چاپ می کند.
نحوه بستن کانال
بستن یک کانال برای نشان دادن اینکه هیچ مقدار دیگری در کانال ارسال نخواهد شد استفاده می شود. برای سیگنال دادن به گیرنده استفاده می شود که کانال بسته شده است و مقادیر بیشتری ارسال نمی شود.
برای کمک به مشکلات همگامسازی، کانالهای Go میتوانند به صراحت بسته شوند. پیادهسازی پیشفرض زمانی که تمام مقادیر ارسال شدند، کانال را میبندد.
بستن یک کانال با فراخوانی تابع close
داخلی انجام می شود
close(channel)
مثال:
package main import ( "fmt" "bytes" ) func process(work <-chan string, fin chan<- string) { var b bytes.Buffer for { if msg, notClosed := <-work; notClosed { fmt.Printf("%s received...\n", msg) } else { fmt.Println("Channel closed") fin <- b.String() return } } } func main() { work := make(chan string, 3) fin := make(chan string) go process(work, fin) word := "hello world" for i := 0; i < len(word); i++ { letter := string(word[i]) work <- letter fmt.Printf("%s sent ...\n", letters) } close(work) fmt.Printf("result: %s\n", <-fin) }
مثال بالا نحوه بستن یک کانال را نشان می دهد. کانال work
با اندازه بافر 3 ایجاد می شود. تابع process
داده ها را از کانال work
می خواند و داده ها را در کانال fin
می نویسد. تابع main
داده ها را در کانال work
می نویسد و کانال work
را می بندد. اگر کانال work
بسته نشود، process
پیام را چاپ می کند. اگر کانال work
بسته باشد، تابع process
پیامی را چاپ می کند و داده ها را در کانال fin
می نویسد.
نحوه تکرار روی پیام های کانال
کانال ها را می توان با استفاده از کلمه کلیدی range
، مشابه arrays, slice, and/or maps
تکرار کرد. این به شما امکان می دهد تا به سرعت و به راحتی پیام های داخل یک کانال را تکرار کنید.
مثال:
package main import ( "fmt" ) func main() { c := make(chan string, 3) go func() { c <- "hello" c <- "world" c <- "goroutine" close(c) // Closing the channel is very important before proceeding to the iteration hence deadlock error }() for msg := range c { fmt.Println(msg) } }
مثال بالا نحوه تکرار در کانال را با استفاده از کلمه کلیدی range
نشان می دهد. کانال c
با اندازه بافر 3 ایجاد می شود. کلمه کلیدی go
برای ایجاد یک گوروتین استفاده می شود که داده ها را در کانال c
می نویسد. تابع main
در کانال c
با استفاده از کلمه کلیدی range
تکرار می شود و پیام را چاپ می کند.
نتیجه
در این مقاله یاد گرفتیم که چگونه همزمان با گوروتین ها و کانال ها را در Go مدیریت کنیم. ما یاد گرفتیم که چگونه گوروتین ایجاد کنیم و چگونه از WaitGroups
و کانال ها برای برقراری ارتباط بین گوروتین ها استفاده کنیم.
همچنین نحوه استفاده از بافرهای کانال، مسیرهای کانال، select
کانال، مهلت زمانی کانال، بسته شدن کانال و محدوده کانال را یاد گرفتیم.
گوروتین ها و کانال ها ویژگی های قدرتمندی در Go هستند که به رفع نیازهای جریان همزمان و ناهمگام کمک می کنند.
مثل همیشه امیدوارم از مقاله لذت برده باشید و چیز جدیدی یاد گرفته باشید. در صورت تمایل، می توانید من را در لینکدین یا توییتر نیز دنبال کنید.
ارسال نظر