همزمانی در زبان Go: کانال‌ها و WaitGroupها


۰


Concurrency یا همزمانی یکی از قابلیت‌های قدرتمند زبان Go (یا Golang) است که به توسعه‌دهندگان اجازه می‌دهد برنامه‌های کارآمد و مقیاس‌پذیر بنویسند. دو مکانیزم متداول برای مدیریت همزمانی در Go، کانال‌ها (Channels) و wait groups هستند. در این مقاله، به بررسی شباهت‌ها و تفاوت‌های این دو می‌پردازیم و نکاتی درباره زمان و نحوه استفاده مؤثر از هرکدام ارائه می‌دهیم.

درک کانال‌ها (Channels)

کانال‌ها بخش اساسی مدل همزمانی در 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)

کانال‌های تک‌جهت (Unidirectional Channels)

این کانال‌ها جریان داده را محدود می‌کنند، فقط اجازه ارسال یا دریافت می‌دهند و از نماد "<-" برای مشخص کردن جهت استفاده می‌شود.

  • کانال فقط ارسال (Send-only Channels): نوع 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}
  • کانال فقط دریافت (Receive-only Channels): نوع <-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}

کانال‌های دو جهته (Bidirectional Channels)

کانال‌های دو جهته امکان ارسال و دریافت را همزمان فراهم می‌کنند و به صورت پیش‌فرض کانال‌ها این خاصیت را دارند:

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}

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

تبدیل جهت کانال (Channel Direction Conversion)

می‌توانید کانال دو جهته را به کانال یک‌جهته ارسال یا دریافت تبدیل کنید و بالعکس، که این کار در پاس دادن کانال به توابع یا متغیرهای محدودشده کاربرد دارد:

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 اصلی منتظر است تا همه کارها تمام شود و خروجی‌ها را چاپ کند.

بررسی Wait Groups

WaitGroupها یک مکانیزم برای مدیریت همزمانی در Go هستند که اجازه می‌دهند منتظر پایان یک گروه از goroutineها بمانید.

مبانی WaitGroup

این نوع از 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 ⚔️

ارتباط (Communication) در برابر همگام‌سازی (Synchronization)

  • کانال‌ها ساختاری برای ارتباط ایمن و سینکرون یا آسنکرون بین goroutineها فراهم می‌کنند.
  • Wait Groups بیشتر بر همگام‌سازی تمرکز دارند و برای اطمینان از پایان همه goroutineها قبل از ادامه مناسب‌اند.

انعطاف‌پذیری

  • کانال‌ها گزینه‌های متنوع‌تری مانند کانال‌های یک‌جهت و دو جهته را ارائه می‌دهند و برای الگوهای پیچیده ارسال و دریافت داده مناسب‌ترند.
  • Wait Groups برای سناریوهای ساده‌تر و شمارش روند پایان goroutineها به کار می‌روند.

سهولت استفاده

  • Wait Groups نصب و استفاده ساده‌تری دارند، مخصوصاً وقتی تعداد مشخصی goroutine برای انتظار داشته باشید.
  • کانال‌ها نیاز به مدیریت دقیق ارسال و دریافت دارند و ممکن است کمی پیچیده‌تر باشند.

مدیریت خطا

  • در کانال‌ها، هر goroutine می‌تواند خطا را از طریق کانال ارسال کند.
  • در Wait Groups معمولا با متغیر اشتراکی خطا کار می‌شود که هر goroutine آن را بروزرسانی می‌کند.

انتخاب مکانیزم مناسب 🤔

  • از کانال‌ها استفاده کنید هنگامی که نیاز به ارتباط امن و تبادل داده بین goroutineها دارید.
  • از کانال‌های با بافر برای قطع ارتباط همزمانی مستقیم و کار با چند مقدار استفاده کنید.
  • از Wait Groups استفاده کنید وقتی تنها نیاز دارید که منتظر پایان یک گروه goroutine باشید و همگام‌سازی ساده می‌خواهید.
  • برای سناریوهای همگام‌سازی ساده و تعداد شناخته‌شده goroutineها Wait Group بهترین گزینه است.

نتیجه‌گیری 🔚

کانال‌ها و Wait Groups هر دو ابزارهای قدرتمند مدیریت همزمانی در Go هستند. کانال‌ها بیشتر برای ارتباط و هماهنگی داده در goroutineها کاربرد دارند، در حالی که Wait Groups روی همگام‌سازی و اطمینان از اتمام goroutineها تمرکز دارند. با درک نقاط قوت و ضعف هر کدام، می‌توانید انتخاب‌های مناسبی در ساخت برنامه‌های همزمان و مقیاس‌پذیر در زبان Go داشته باشید و از قابلیت‌های همزمانی Go نهایت بهره را ببرید.

همزمانی
غیر همزمان
گو

۰


نظرات


author
نویسنده مقاله: علی رحمانی

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

تمام حقوق این سایت متعلق به وبسایتcodebymeمیباشد.