Concurrency یک مفهوم اساسی در Go (Golang) است که اجرای چندین کار را به طور همزمان امکان پذیر می کند و برنامه های ما را کارآمد، پاسخگو و قادر به استفاده موثر از پردازنده های چند هسته ای می کند. یکی از جنبه های کلیدی برنامه نویسی همزمان در Go، استفاده از پترن های Concurrency است. در این مقاله، با گوروتین ها و کانال ها آشنا خواهیم شد و سپس پترن های مختلف Go Concurrency را بررسی خواهیم کرد، چرا آنها بسیار مهم هستند، و نمونه هایی در دنیای واقعی از استفاده از آنها ارائه خواهیم دید.
همزمانی یا Concurrency در قلب بسیاری از برنامه های مدرن قرار دارد، به ویژه برنامه هایی که نیاز به توان عملیاتی و کارایی بالایی دارند. Go، یک زبان برنامه نویسی تایپ ایستا که توسط گوگل توسعه یافته است، به دلیل رویکرد ساده و کارآمد خود برای همزمانی محبوبیت پیدا کرده است. مرکز این رویکرد، زمانبندی همزمان Go است، بخش پیچیدهای از زمان اجرا که گوروتینها، رشتههای سبک Go را مدیریت میکند.
Concurrency در Go چیست؟
قبل از اینکه به خود زمانبندی بپردازیم، درک واحدهای اساسی Concurrency در Go بسیار مهم است: گوروتین ها (Goroutines) و کانال ها (Channels). گوروتین ها توابع یا متد هایی هستند که همزمان با سایر گوروتین ها اجرا می شوند. آنها سبک وزن هستند و هزینه کمی بیشتر از تخصیص یک پشته جدید دارند. از سوی دیگر، کانال ها مجراهایی هستند که از طریق آن ها گوروتین ها ارتباط برقرار می کنند و اجرای خود را بدون لاک ها یا متغیرهای شرطی همگام می کنند.
چرا Concurrency مهم است؟
چرا باید به Concurrency اهمیت دهید؟ تصور کنید در حال ساخت یک وب سرور هستید که چندین درخواست را به طور همزمان مدیریت می کند. بدون Concurrency، سرور شما یک درخواست را در یک زمان پردازش میکند که منجر به کاهش زمان پاسخ و کاربران ناراضی میشود. Concurrency به برنامه شما اجازه می دهد تا چندین کار را همزمان انجام دهد و آن را سریعتر و کارآمدتر کند.
Goroutine چیست : قدرت گوروتین ها
گوروتین ها (Goroutines) رشته های سبک وزنی هستند که توسط زمان اجرا (runtime) گو مدیریت می شوند. آنها به قدری سبک هستند که می توانید به راحتی هزاران عدد از آنها را بدون افزایش مصرف حافظه خود ایجاد کنید. برای شروع یک گوروتین، کافیست یک فراخوانی تابع را با کلمه کلیدی go پیشوند کنید.
مثال :
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello, World!")
}
func main() {
go sayHello()
time.Sleep(1 * time.Second)
}
در این مثال، sayHello
به عنوان یک گوروتین اجرا می شود. تابع main یک ثانیه میخوابد تا مطمئن شود که گوروتین زمان کافی برای چاپ "!Hello, World"
قبل از خروج برنامه دارد.
Channels چیست : کانال ها در Go
حالا بیایید در مورد کانال ها صحبت کنیم. کانالهای در Go راهی را برای گوروتینها فراهم میکنند تا با یکدیگر ارتباط برقرار کنند و اجرای آنها را همگامسازی کنند. کانال ها را به عنوان مجراها یا لوله هایی در نظر بگیرید که داده ها از طریق آنها بین گوروتین ها جریان می یابد.
مثال:
package main
import (
"fmt"
)
func main() {
messages := make(chan string)
go func() {
messages <- "ping"
}()
msg := <-messages
fmt.Println(msg)
}
در این مثال، یک کانال جدید از نوع string ایجاد می کنیم و یک گوروتین را شروع می کنیم که "ping
" را به کانال ارسال می کند. تابع main پیام را از کانال دریافت می کند و آن را چاپ می کند. ساده است، مگه نه؟!
کانال های بافر شده
کانال ها همچنین می توانند بافر شوند، به این معنی که می توانند تعداد ثابتی از مقادیر را قبل از مسدود کردن نگه دارند. این می تواند برای کنترل جریان داده بین گوروتین ها مفید باشد.
package main
import (
"fmt"
)
func main() {
messages := make(chan string, 2)
messages <- "buffered"
messages <- "channel"
fmt.Println(<-messages)
fmt.Println(<-messages)
}
در این مثال، پیام های کانال می توانند تا دو مقدار را نگه دارند. دوتا messages
بدون بلاک به کانال میفرستیم و بعد دریافت میکنیم.
عبارت: Multiplexing Channels را انتخاب کنید
دستور Go’s select به یک گوروتین اجازه می دهد تا در چند عملیات ارتباطی منتظر بماند. این متد Go برای مدیریت چندین کانال است، مشابه نحوه عملکرد select در سیستمهای شبه یونیکس برای I/O.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received", msg1)
case msg2 := <-ch2:
fmt.Println("received", msg2)
}
}
}
در این مثال، گزینه waits
را در ch1
و ch2
انتخاب کنید و پیام دریافتی از هر کانال را چاپ کنید.
پترن های Concurrency چیست؟
پترن های Concurrency راه حل های ثابت شده ای برای مشکلات رایجی هستند که در برنامه نویسی همزمان با آن مواجه می شوند. آنها به ما کمک می کنند تا با ارائه رویکردهای ساختاریافته برای مدیریت گوروتین ها، همگام سازی دسترسی به داده ها و تسهیل ارتباط بین وظایف همزمان، کدهای همزمان قوی و کارآمد بنویسیم.
پترن های همزمان چندین مزیت دارند:
استفاده کارآمد از منابع: استفاده کارآمد از منابع سیستم، از جمله هسته های CPU و حافظه را امکان پذیر می کنند.
پاسخگویی: Concurrency تضمین می کند که برنامه ها در حین انجام وظایف پس زمینه به ورودی های کاربر پاسخگو می مانند.
اشکالات کاهش یافته: الگوها به جلوگیری از مشکلات رایج Concurrency مانند شرایط مسابقه و بن بست کمک می کنند و قابلیت اطمینان کد را افزایش می دهند.
اکنون، بیایید به برخی از پترن های ضروری همزمانی Go بپردازیم.
پترن Worker Pool
پترن Worker Pool چیست؟
پترن worker pool شامل ایجاد گروهی از گوروتین های ورکر برای پردازش همزمان وظایف، محدود کردن تعداد عملیات همزمان است. این الگو زمانی ارزشمند است که تعداد زیادی کار برای اجرا دارید.
کد نمونه:
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Start worker goroutines
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(workerID, jobs, results)
}(i)
}
// Enqueue jobs
for i := 1; i <= numJobs; i++ {
jobs <- i
}
close(jobs)
// Wait for all workers to finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}
چند نمونه از استفاده از Worker Pool Pattern در برنامه های دنیای واقعی :
رسیدگی به درخواست های HTTP ورودی در یک وب سرور.
پردازش تصاویر به صورت همزمان
پترن Pipeline
پترن Pipeline چیست؟
پترن Pipeline مجموعه ای از مراحل پردازش را تشکیل می دهد که هر مرحله به طور همزمان اجرا می شود. داده ها از طریق این مراحل به صورت متوالی جریان می یابد و امکان تبدیل و پردازش کارآمد داده ها را فراهم می کند.
کد نمونه:
package main
import (
"fmt"
)
func main() {
// Create the initial channel with some data
data := []int{1, 2, 3, 4, 5}
input := make(chan int, len(data))
for _, d := range data {
input <- d
}
close(input)
// First stage of the pipeline: Doubles the input values
doubleOutput := make(chan int)
go func() {
defer close(doubleOutput)
for num := range input {
doubleOutput <- num * 2
}
}()
// Second stage of the pipeline: Squares the doubled values
squareOutput := make(chan int)
go func() {
defer close(squareOutput)
for num := range doubleOutput {
squareOutput <- num * num
}
}()
// Third stage of the pipeline: Prints the squared values
for result := range squareOutput {
fmt.Println(result)
}
}
چند نمونه از استفاده از پترن Pipeline در برنامه های کاربردی دنیای واقعی :
پردازش داده در Pipeline ETL (Extract, Transform, Load).
Pipeline پردازش تصویر در برنامه های چند رسانه ای
پترن Fan-out/Fan-in
پترن Fan-out/Fan-in چیست؟
پترن fan-out/fan-in
شامل توزیع تکالیف به چندین گوروتین ورکر (fan-out) و سپس تجمیع نتایج آنها (fan-in) است. برای موازی کردن وظایف و ترکیب نتایج آنها مفید است.
کد نمونه:
package main
import (
"fmt"
"sync"
)
func main() {
data := []int{1, 2, 3, 4, 5}
input := make(chan int, len(data))
for _, d := range data {
input <- d
}
close(input)
// Fan-out: Launch multiple worker goroutines
numWorkers := 3
results := make(chan int, len(data))
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for num := range input {
// Simulate some processing
result := num * 2
results <- result
}
}()
}
// Fan-in: Aggregate results from workers
go func() {
wg.Wait()
close(results)
}()
// Process aggregated results
for result := range results {
fmt.Println(result)
}
}
چند نمونه از استفاده از پترن Fan-out/Fan-in در برنامه های کاربردی دنیای واقعی:
وب سایت های متعدد را به طور همزمان scraping می کند و نتایج را ادغام می کند.
جمع آوری داده ها از چندین حسگر در برنامه های IoT.
جدای از پترن های ذکر شده در بالا، چندین پترن Concurrency دیگر وجود دارد که ارزش بررسی دارند:
پترن Mutex: حفاظت از منابع مشترک با استفاده از mutexes (sync.Mutex) برای اطمینان از دسترسی انحصاری.
پترن Semaphore: کنترل دسترسی به منابع با محدود کردن تعداد گوروتین های مجاز در یک زمان.
پترن Barrier: همگام سازی چندین گوروتین در نقاط خاصی از اجرای آنها.
پترن WaitGroup : منتظر اتمام مجموعه ای از گوروتین ها قبل از ادامه هستید.
پترن های Concurrency راه حل های ساختاری برای مشکلات رایج برنامه نویسی همزمان هستند. آنها استفاده از منابع، پاسخگویی و قابلیت اطمینان کد را افزایش می دهند. Worker Pool، Pipeline و Fan-out/Fan-in برخی از پترن های اصلی همزمانی در Go هستند. Mutex، Semaphore، Barrier و WaitGroup پترن های اضافی با موارد استفاده خاص هستند.
نتیجه
Concurrency در Go با برنامهها و کانالهایش، یک مدل قدرتمند و ساده است که نوشتن برنامههای همزمان را آسان میکند. چه در حال ساخت یک وب سرور با کارایی بالا یا یک پردازشگر داده بلادرنگ باشید، تسلط بر این ابزارها برنامه های Go شما را سریعتر و کارآمدتر می کند. در مباحث عمیق شوید، تست کنید و به زودی خواهید دید که چرا مدل همزمانی Go بسیار مورد توجه است.