۰
Concurrency یا همزمانی یکی از قابلیتهای قدرتمند زبان Go (یا Golang) است که به توسعهدهندگان اجازه میدهد برنامههای کارآمد و مقیاسپذیر بنویسند. دو مکانیزم متداول برای مدیریت همزمانی در Go، کانالها (Channels) و wait groups هستند. در این مقاله، به بررسی شباهتها و تفاوتهای این دو میپردازیم و نکاتی درباره زمان و نحوه استفاده مؤثر از هرکدام ارائه میدهیم.
کانالها بخش اساسی مدل همزمانی در Go هستند و اجازه میدهند goroutineها (ریز فرایندهای سبک Go) با هم ارتباط برقرار کنند و همگامسازی اجرا داشته باشند. میتوان آنها را صفهای پیام typed در نظر گرفت که موجب ایمنی در ارسال دادهها بین goroutineها میشوند.
— کانالهای بدون بافر (Unbuffered Channels)
کانالهای بدون بافر سادهترین نوع کانالها هستند. وقتی یک کانال بدون بافر ایجاد میکنید، ظرفیت آن صفر است. این یعنی هر عمل ارسال روی کانال تا وقتی که یک goroutine دیگر آماده دریافت داده باشد، بلوک میشود. همچنین هر عمل دریافت تا وقتی که یک goroutine دیگر دادهای ارسال کند، بلوک میشود.
نمونهای که رفتار کانال بدون بافر را نشان میدهد:
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func main() { 9 ch := make(chan int) // ایجاد کانال بدون بافر 10 11 go func() { 12 time.Sleep(time.Second) // شبیهسازی کار 13 ch <- 5 // ارسال مقدار به کانال 14 }() 15 16 x := <-ch // دریافت مقدار از کانال (خط بلوککننده) 17 fmt.Println(x) // خروجی: 5 18}
در این مثال، goroutine اصلی در خط x := <-ch
برقرار میماند و منتظر دریافت مقدار از کانال است. پس از دریافت مقدار، آن را چاپ میکند.
کانالهای بدون بافر تضمین میکنند که goroutine ارسالکننده و دریافتکننده همگام (سینکرون) باشند. اگر ارسالکننده زودتر ارسال کند، بلوک میشود تا دریافتکننده آماده شود و بالعکس.
— کانالهای با بافر (Buffered Channels)
کانالهای با بافر ظرفیتی بیشتر از صفر دارند. این اجازه میدهد چند مقدار در صف ذخیره شود و ارسال تا پر شدن صف بلوک نشود. کانالهای با بافر باعث قطع ارتباط همزمانی مستقیم بین ارسالکننده و دریافتکننده میشوند و به صورت غیرهمزمان قابل استفادهاند.
نمونهای که استفاده از کانال با بافر را نشان میدهد:
1package main 2 3import "fmt" 4 5func main() { 6 ch := make(chan int, 2) // ایجاد کانال با بافر ظرفیت 2 7 8 ch <- 1 // ارسال مقدار 1 9 ch <- 2 // ارسال مقدار 2 10 11 x := <-ch // دریافت اولین مقدار 12 fmt.Println(x) // خروجی: 1 13 14 y := <-ch // دریافت دومین مقدار 15 fmt.Println(y) // خروجی: 2 16}
در این مثال، مقادیر به ترتیب ارسال دریافت میشوند و به دلیل بافر، ارسال گیر گیر نمیافتد.
کانالهای با بافر زمانی مناسباند که سرعت ارسال و دریافت متفاوت باشد یا بخواهید همزمانی بین آنها را کمتر کنید.
— جهت کانال (Channel Direction)
این کانالها جریان داده را محدود میکنند، فقط اجازه ارسال یا دریافت میدهند و از نماد "<-"
برای مشخص کردن جهت استفاده میشود.
chan<- T
فقط قابلیت ارسال مقادیر از نوع T
را دارد. دسترسی به این کانال فقط برای ارسال است و نمیتوان دادهای دریافت کرد. این کانالها وقتی کاربرد دارند که بخواهید جهت داده را محدود کنید و خوانش ناخواسته را منع کنید.1func sendData(ch chan<- int) { 2 ch <- 5 // ارسال داده به کانال 3} 4 5func main() { 6 ch := make(chan<- int) // تعریف کانال فقط ارسال 7 8 go sendData(ch) // ارسال داده در goroutine جداگانه 9}
<-chan T
فقط اجازه دریافت مقادیر را میدهد و ارسال در آن مجاز نیست.1func readData(ch <-chan int) { 2 x := <-ch // دریافت داده از کانال 3 fmt.Println(x) 4} 5 6func main() { 7 ch := make(<-chan int) // تعریف کانال فقط دریافت 8 9 go readData(ch) // خواندن داده در goroutine جداگانه 10}
کانالهای دو جهته امکان ارسال و دریافت را همزمان فراهم میکنند و به صورت پیشفرض کانالها این خاصیت را دارند:
1func sendData(ch chan int) { 2 ch <- 42 // ارسال داده 3} 4 5func readData(ch chan int) { 6 x := <-ch // دریافت داده 7 fmt.Println(x) 8} 9 10func main() { 11 ch := make(chan int) // ایجاد کانال دو جهته 12 13 go sendData(ch) 14 go readData(ch) 15 16 time.Sleep(time.Second) // منتظر ماندن برای اتمام goroutineها 17}
کانالهای دو جهته انعطافپذیری بیشتری دارند و برای سناریوهایی که نیاز به ارتباط و تبادل داده دو طرفه دارند مناسباند.
میتوانید کانال دو جهته را به کانال یکجهته ارسال یا دریافت تبدیل کنید و بالعکس، که این کار در پاس دادن کانال به توابع یا متغیرهای محدودشده کاربرد دارد:
1func sendData(ch chan<- int) { 2 ch <- 42 3} 4 5func main() { 6 ch := make(chan int) 7 8 sendCh := (chan<- int)(ch) // تبدیل به فقط ارسال 9 go sendData(sendCh) 10 11 readCh := (<-chan int)(ch) // تبدیل به فقط دریافت 12 x := <-readCh 13 fmt.Println(x) 14}
— بنبستها و بلوکهشدن
بنبست زمانی رخ میدهد که goroutineها برای مدت نامحدودی منتظر یکدیگر بمانند.
مثالها:
1package main 2 3func main() { 4 ch := make(chan int) 5 6 go func() { 7 ch <- 5 // ارسال که بلوک میشود چون گیرندهای نیست 8 }() 9 10 // خط دریافت (x := <-ch) کامنت شده است پس برنامه بنبست میزند 11}
1package main 2 3func main() { 4 ch := make(chan int) 5 6 go func() { 7 <-ch // تلاش برای دریافت که بلوک میشود 8 }() 9 10 // ارسال وجود ندارد؛ برنامه بنبست میزند 11}
1package main 2 3import "fmt" 4 5func main() { 6 ch := make(chan int, 2) 7 8 ch <- 1 9 ch <- 2 10 11 go func() { 12 ch <- 3 // این ارسال بلوک شده چون بافر پر است 13 fmt.Println("Sent 3 to the channel") 14 }() 15 16 fmt.Println(<-ch) 17 fmt.Println(<-ch) 18}
برای جلوگیری از بنبست، همگامسازی صحیح ارسال و دریافت ضروری است که اغلب با هماهنگی goroutineها، استفاده از wait group یا timeout حل میشود.
— بستن کانال (Closing Channels)
در Go میتوان کانالها را بست تا نشان داد دیگر مقداری ارسال نمیشود. بستن کانال برای اطلاع دادن به دریافتکننده مفید است. کانال بسته هنوز میتوان از آن دریافت کرد، اما پس از دریافت آخرین داده، همیشه مقدار صفر (zero value) نوع داده را برمیگرداند.
مثال:
1package main 2 3import "fmt" 4 5func main() { 6 ch := make(chan int) 7 8 go func() { 9 for i := 1; i <= 5; i++ { 10 ch <- i 11 } 12 close(ch) // بستن کانال پس از ارسال تمام دادهها 13 }() 14 15 for x := range ch { 16 fmt.Println(x) // خروجی: 1 2 3 4 5 17 } 18}
استفاده از range
روی کانال زمانی که بسته شود، باعث میشود دریافتکننده بداند دادهها تمام شدهاند.
— همگامسازی
یکی از اصلیترین فواید کانالها، همگامسازی اجرای goroutineها است. با استفاده از کانالها میتوان اجرای چند goroutine را همزمان کنترل کرد تا اطمینان حاصل شود همه کار خود را انجام دهند.
مثلاً فرض کنید تا چند goroutine کار مستقل انجام میدهند اما میخواهیم نتایج را فقط وقتی همه تمام کردند پردازش کنیم. با یک کانال میتوان هر goroutine را وادار کرد بعد از پایان کار، مقداری ارسال کند. سپس goroutine اصلی یا دیگری میتواند منتظر دریافت همه مقادیر باشد.
مثال همگامسازی با کانال و WaitGroup:
1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8func worker(id int, ch chan int, wg *sync.WaitGroup) { 9 defer wg.Done() 10 11 result := id * 2 // انجام کار 12 13 ch <- result // ارسال نتیجه 14} 15 16func main() { 17 numWorkers := 3 18 ch := make(chan int, numWorkers) 19 var wg sync.WaitGroup 20 21 for i := 0; i < numWorkers; i++ { 22 wg.Add(1) 23 go worker(i, ch, &wg) 24 } 25 26 go func() { 27 wg.Wait() 28 close(ch) // بستن کانال بعد از پایان همه 29 }() 30 31 for result := range ch { 32 fmt.Println(result) // پردازش نتایج 33 } 34}
در این مثال، هر worker مقداری را در کانال ارسال میکند، سپس WaitGroup منتظر پایان همه بوده و کانال را میبندد. سپس حلقه اصلی نتایج را پردازش میکند.
— انتقال داده از طریق کانالها
این امر کلیدیترین مفهوم در مدل همزمانی Go است. با کانالها، goroutineها میتوانند به شکل ایمن داده رد و بدل کنند و اجرایشان را هماهنگ نمایند. این امکان ایجاد روابط تولیدکننده-مصرفکننده (producer-consumer) را میدهد که در آن یکی داده تولید و ارسال میکند و دیگری دریافت و پردازش.
مثال:
1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8func squareWorker(id int, input <-chan int, output chan<- int, wg *sync.WaitGroup) { 9 defer wg.Done() 10 for num := range input { 11 square := num * num 12 output <- square 13 } 14} 15 16func main() { 17 input := make(chan int) 18 output := make(chan int) 19 20 var wg sync.WaitGroup 21 wg.Add(2) 22 23 go squareWorker(1, input, output, &wg) 24 go squareWorker(2, input, output, &wg) 25 26 go func() { 27 defer close(input) 28 for i := 1; i <= 5; i++ { 29 input <- i 30 } 31 }() 32 33 go func() { 34 defer close(output) 35 wg.Wait() 36 }() 37 38 for square := range output { 39 fmt.Println(square) 40 } 41 42 fmt.Println("All values processed") 43}
در این مثال، دو goroutine تولید مربعات اعداد را انجام میدهند و هر دو از کانالهای input و output به صورت همزمان استفاده میکنند. goroutine اصلی منتظر است تا همه کارها تمام شود و خروجیها را چاپ کند.
WaitGroupها یک مکانیزم برای مدیریت همزمانی در Go هستند که اجازه میدهند منتظر پایان یک گروه از goroutineها بمانید.
این نوع از sync
پکیج ساخته میشود و سه متد اصلی دارد: Add()
, Done()
, و Wait()
.
Add(n int)
تعداد goroutineهایی را که نیاز به انتظار دارند مشخص میکند (عدد درونی تکرار میشود).Done()
توسط هر goroutine پس از اتمام فراخوانی میشود که شمارنده داخلگروه را کاهش میدهد.Wait()
باعث بلوکهشدن میشود تا وقتی شمارنده به صفر برسد.مثال:
1package main 2 3import ( 4 "errors" 5 "fmt" 6 "sync" 7) 8 9func worker(id int, wg *sync.WaitGroup, err *error) { 10 defer wg.Done() 11 12 if id == 2 { 13 *err = errors.New("an error occurred") // شبیهسازی خطا 14 return 15 } 16 17 fmt.Println("Worker", id, "completed") 18} 19 20func main() { 21 numWorkers := 3 22 var wg sync.WaitGroup 23 var err error 24 25 for i := 0; i < numWorkers; i++ { 26 wg.Add(1) 27 go worker(i, &wg, &err) 28 } 29 30 wg.Wait() // انتظار برای پایان همه 31 32 if err != nil { 33 fmt.Println("An error occurred:", err) 34 } else { 35 fmt.Println("All workers finished") 36 } 37}
اینجا، هر worker بعد از پایان کار wg.Done()
را فراخوانی میکند، شمارنده کاهش پیدا میکند و نهایتا wg.Wait()
منتظر میماند تا همه به پایان برسند.
کانالها و Wait Groups هر دو ابزارهای قدرتمند مدیریت همزمانی در Go هستند. کانالها بیشتر برای ارتباط و هماهنگی داده در goroutineها کاربرد دارند، در حالی که Wait Groups روی همگامسازی و اطمینان از اتمام goroutineها تمرکز دارند. با درک نقاط قوت و ضعف هر کدام، میتوانید انتخابهای مناسبی در ساخت برنامههای همزمان و مقیاسپذیر در زبان Go داشته باشید و از قابلیتهای همزمانی Go نهایت بهره را ببرید.
۰
کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.