متن خبر

نحوه مدیریت همزمانی با گوروتین ها و کانال ها در Go

نحوه مدیریت همزمانی با گوروتین ها و کانال ها در Go

شناسهٔ خبر: 470682 -




همزمانی توانایی یک برنامه برای انجام چندین کار به طور همزمان است. این یک جنبه حیاتی در ساخت سیستم های مقیاس پذیر و پاسخگو است.

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

ویژگی های همزمانی Go به توسعه دهندگان امکان می دهد برنامه هایی بنویسند که می توانند:

رسیدگی به چندین درخواست به طور همزمان، بهبود پاسخگویی و توان عملیاتی.

از پردازنده های چند هسته ای به طور موثر استفاده کنید و منابع سیستم را به حداکثر برسانید.

کدی همزمان بنویسید که ایمن، کارآمد و نگهداری آسان باشد.

مدل همزمانی Go برای به حداقل رساندن سربار، کاهش تأخیر و جلوگیری از خطاهای رایج همزمانی مانند شرایط مسابقه و بن بست طراحی شده است.

با Go، توسعه‌دهندگان می‌توانند به راحتی سیستم‌هایی با کارایی بالا، مقیاس‌پذیر و همزمان بسازند، که آن را به گزینه‌ای ایده‌آل برای ساخت سیستم‌ها، شبکه‌ها و زیرساخت‌های ابری مدرن تبدیل می‌کند.

فهرست مطالب

مطالعه موردی: عابر بانک

پردازش متوالی

همزمانی

گوروتین ها و کانال ها چیست؟

گوروتین چیست؟

نحوه اجرای گوروتین

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

WaitGroups چیست؟

کانال ها چیست؟

نحوه نوشتن داده در کانال

نحوه خواندن داده ها از یک کانال

نحوه پیاده سازی کانال ها با 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 هستند که به رفع نیازهای جریان همزمان و ناهمگام کمک می کنند.

مثل همیشه امیدوارم از مقاله لذت برده باشید و چیز جدیدی یاد گرفته باشید. در صورت تمایل، می توانید من را در لینکدین یا توییتر نیز دنبال کنید.

خبرکاو

ارسال نظر

دیدگاه‌ها بسته شده‌اند.


تبليغات ايهنا تبليغات ايهنا

تمامی حقوق مادی و معنوی این سایت متعلق به خبرکاو است و استفاده از مطالب با ذکر منبع بلامانع است