3.6 کانال (channel)

3.6 کانال (channel)

channel

کانال یک نوع تایپ است که داده از نوع خاصی را نگه داری میکند و امکان برقراری ارتباط و همگام سازی داده بین گوروتین ها را فراهم می کند. شما می توانید کانال ها را به عنوان خط لوله هایی در نظر بگیرید که این خط لوله ها به گوروتین ها متصل می شوند و باعث برقراری ارتباط بین گوروتین ها می شوند. این ارتباط بین گوروتین ها به هیچ قفل صریحی نیاز ندارد (منظورم mutex و …) چون کانال ها بصورت داخلی قفل ها را مدیریت میکنند و در زمان های مناسب و مشخص Lock و UnLock می کنند.

به نقل از رابرت گریزمر که یکی از توسعه دهنده های اصلی زبان برنامه نویسی گو می باشد در خصوص کانال ها می گوید : با برقراری ارتباط حافظه را به اشتراک بزارید ولی با اشتراک گذاری حافظه ارتباط برقرار نکنید.

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

زبان گو برای بحث همزمانی ۲ تا مقوله خیلی مهم دارد که این دو با هم در ارتباط هستند :

  1. گوروتین ها : یک thread مستقل و سبک وزن در زبان گو که قابلیت برنامه نویسی همزمان (concurrency) را فراهم می کند.
  2. کانال ها : فراهم کننده ارتباط و همگام سازی داده ها بین گوروتین ها.

3.6.1 تعریف کانال ها #

همانطور که گفتیم کانال یک نوع تایپ است که داده از نوع خاصی را نگه داری میکند. برای استفاده از کانال ها، شما باید این تایپ را برای متغیری تعریف کنید تا بتوانید بواسطه آن متغیر از کانال استفاده کنید و بین گوروتین ها ارتباط برقرار کنید.

1var <variable_name> chan <type>

به مثال زیر توجه کنید :

1package main
2
3import "fmt"
4
5func main() {
6    var a chan int
7    fmt.Println(a)
8}
1$ go run main.go
2{nil}

در بالا ما یک متغیر با نام a تعریف کردیم که از نوع کانال با تایپ int می باشد و این کانال فقط انتقال داده از نوع int را انجام می دهد. و مقدار پیش فرض کانال nil می باشد که در خروجی می توانید ببینید.

توجه کنید همیشه سعی کنید کانال را با استفاده از تابع make ایجاد کنید.
1package main
2
3import "fmt"
4
5func main() {
6    a := make(chan int)
7    fmt.Println(a)
8}
1$ go run main.go
20xc0000240c0

در خروجی کد بالا همانطور که مشاهده می کنید به جای nil آدرس حافظه داده را نمایش می دهد.

زمانیکه شما یک کانال را به واسطه make ایجاد می کنید در واقع دارید یک instance از ساختار hchan ایجاد می کنید و تمامی فیلدهای این ساختار مقدار پیش فرض میگیرند.

 1type hchan struct {
 2    qcount   uint           // total data in the queue
 3    dataqsiz uint           // size of the circular queue
 4    buf      unsafe.Pointer // points to an array of dataqsiz elements
 5    elemsize uint16
 6    closed   uint32         // denotes weather channel is closed or not
 7    elemtype *_type         // element type
 8    sendx    uint           // send index
 9    recvx    uint           // receive index
10    recvq    waitq          // list of recv waiters
11    sendq    waitq          // list of send waiters
12    lock     mutex
13}

3.6.2 عملیات ها برروی کانال #

زمانیکه شما یک کانال ایجاد می کنید، دو عملیات اصلی بر روی کانال می توانید انجام دهید :

  • ارسال : ارسال داده به داخل کانال
  • دریافت : دریافت داده از کانال

3.6.2.1 عملیات ارسال #

برای ارسال داده به داخل کانال یک اپراتور استاندارد وجود دارد که بهتر است همیشه به خاطر بسپارید :

1ch <- val
  1. متغیر ch همان کانالی است که با استفاده از تایپ chan ساخته شده است.
  2. متغیر val هم مقداری است که توسط اپراتور <- به کانال ارسال شده است.
توجه کنید تایپ val باید با تایپی که برای کانال مشخص کردید حتما یکی باشد.

3.6.2.2 عملیات دریافت #

عملیات دریافت در کانال صرفا جهت خواندن داده از طریق کانال می باشد که یک قالب استاندارد همانند عملیات ارسال دارد :

1val := <- ch 
  1. در اینجاهم ch همان متغیر کانال می باشد.
  2. متغیر val هم منتظر دریافت داده به واسطه >- از طریق کانال ch می باشد.

3.6.2.3 مثال عملیات ارسال و دریافت #

در زیر یک مثال میزنیم که داده ای را بواسطه کانال ارسال/دریافت می کنیم بین گوروتین ها.

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func main() {
 9    ch := make(chan int)
10
11    fmt.Println("Sending value to channel")
12    go send(ch)
13
14    fmt.Println("Receiving from channel")
15    go receive(ch)
16
17    time.Sleep(time.Second * 1)
18}
19
20func send(ch chan int) {
21    ch <- 1
22}
23
24func receive(ch chan int) {
25    val := <-ch
26    fmt.Printf("Value Received=%d in receive function\n", val)
27}
1$ go run main.go
2Sending value to channel
3Receiving from channel
4Value Received=1 in receive function

در کد فوق ما یک کانال با نام ch از نوع int ایجاد کردیم. سپس ۲ تابع send و received را داخل گوروتین قرار دادیم که هر دو تابع به عنوان پارامتر ورودی تایپ int را بصورت کانال میگیرد. حال متغیر کانال ch را به هر دو تابع پاس دادیم. و در هر دو تابع ۲ تا عملیات صورت گرفته :

  • تابع send مقدار عدد ۱ را به داخل کانال ارسال کرده
  • تابع recived مقدار را از کانال ch دریافت کرده و داخل متغیر val قرار داده است و در نهایت متغیر val را چاپ کرده است.

در انتهای تابع main ما یک sleep به مدت ۱ ثانیه قرار دادیم که بتوانیم خروجی برنامه را ببینیم. اگر اینکار را نکنیم گوروتین main ممکن است سریع تر از اینکه دو گوروتین دیگر اجرا شوند و خروجی آنها را ببینیم، کارش به اتمام برسد و برنامه متوقف شود.

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

بزارید یک مثال ساده برای اینکه ببینید چطور کانال ها Lock و UnLock می شوند بزنیم :

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func main() {
 9  ch := make(chan int)
10  go send(ch)
11
12  go receive(ch)
13  time.Sleep(time.Second * 2)
14}
15
16func send(ch chan int) {
17  time.Sleep(time.Second * 1)
18  fmt.Println("Timeout finished")
19  ch <- 1
20}
21
22func receive(ch chan int) {
23  val := <-ch
24  fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
25}
1$ go run main.go
2Timeout finished
3Receiving Value from channel finished. Value received: 1

در کد فوق ما داخل تابع send یک sleep به مدت ۱ ثانیه قرار دادیم. و پس از اینکه ۱ ثانیه تمام شد مقدار را داخل کانال ch ارسال کردیم و سپس مقدار داخل تابع recived دریافت شد.

اتفاقی که در کد فوق رخداد زمانیکه شما عملیات دریافت را انجام می دهید تا زمانیکه مقداری از کانال دریافت نشود اون بخش از کد شما Lock می شود و پس از اینکه دریافت شد مقدار از کانال آن بخش Unlock خواهد شد.

3.6.3 ایجاد کانال بافر شده #

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

در زبان گو شما می توانید کانال های بافر شده ایجاد کنید. یک کانال بافر دارای ظرفیتی مشخص برای نگه داری داده است. در این صورت نیازی نیست حتما یک دریافت کننده منتظر دریافت داده باشد تا بتوانیم داخل کانال چیزی ارسال کنیم. بلکه تا زمانی که ظرفیت کانال پر نشده باشد، می توانیم داده ارسال کنیم.

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

برای ایجاد یک کانال بافر شده از روش زیر استفاده کنید :

1a := make(chan , capacity)

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

به مثال زیر توجه کنید :

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7func main() {
 8    ch := make(chan int, 1)
 9    ch <- 1
10    fmt.Println("Sending value to channnel complete")
11    val := <-ch
12    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
13}
1$ go run main.go
2Sending value to channnel complete
3Receiving Value from channel finished. Value received: 1

در کد فوق ما یک کانال بافر شده با ظرفیت ۱ ایجاد کردیم و مقدار ۱ را به کانال ارسال کردیم و در ادامه این مقدار را از کانال دریافت کردیم.

1ch := make(chan int, 1)

3.6.3.1 ارسال داده برروی کانال با ظرفیت پر #

حالا فرض کنید می خواهیم به کانال بافر شده کد فوق یک مقدار دیگری را ارسال کنیم :

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func main() {
 8	ch := make(chan int, 1)
 9	ch <- 1
10	ch <- 2
11	fmt.Println("Sending value to channnel complete")
12	val := <-ch
13	fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
14}
1$ go run main.go
2fatal error: all goroutines are asleep - deadlock!
3
4goroutine 1 [chan send]:
5main.main()
6	/tmp/sandbox2390960160/prog.go:10 +0x4b

در کد فوق اتفاقی که افتاد ما یک کانال بافر شده با ظرفیت ۱ ایجاد کردیم ولی به این کانال ۲ تا مقدار ارسال کردیم:

1ch <- 1
2ch <- 2

در ادامه اتفاقی که صورت گرفت کانال ما به خاطر پر شدن ظرفیتش بلاک شده بود و داده دیگه ای را نمی توانست نگه داری کند. در نتیجه با خطای deadlock مواجه شد و برنامه کاملا متوقف شد :

1fatal error: all goroutines are asleep - deadlock!

دریافت مجدد داده از کانال خالی شده #

به مثال زیر توجه کنید :

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7func main() {
 8    ch := make(chan int, 1)
 9    ch <- 1
10    fmt.Println("Sending value to channnel complete")
11    val := <-ch
12    val = <-ch
13    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
14}
1$ go run main.go
2Sending value to channnel complete
3fatal error: all goroutines are asleep - deadlock!
4
5goroutine 1 [chan receive]:
6main.main()
7	/tmp/sandbox3239418330/prog.go:12 +0xad

در کد فوق ما یک کانال بافر شده با ظرفیت ۱ ایجاد کردیم و یک مقدار را به کانال ارسال کردیم و در نهایت ۲ بار دریافت از کانال را فرخوانی کردیم. اما اتفاقی که افتاده بازم با خطای deadlock مواجه شدیم چون کانال خالی شده است و هیچ داده ای بیشتر از بافر اش ندارد.

3.6.4 جهت های کانال #

شما می توانید کانال را با جهت های مختلفی تعریف کنید که به شرح زیر است :

  • دو طرفه : کانال با جهت دوطرفه مانند مثال های قبلی می باشد که شما chan int به این شکل تعریف می کنید.
  • یک طرفه فقط ارسال : شما می توانید یک کانال ایجاد کنید که فقط عملیات ارسال chan<- int را انجام می دهد.
  • یک طرفه فقط دریافت : شما می توانید یک کانال ایجاد کنید که فقط عملیات دریافت <-chan int را انجام می دهد.

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

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

  • chan کانال دوطرفه
  • chan <- کانال فقط ارسال
  • <-chan کانال فقط دریافت

3.6.4.1 کانال فقط ارسال #

برای ایجاد کانال فقط ارسال شما می توانید به شکل زیر برای ورودی یا خروجی تابع تعریف کنید :

1func process(ch chan<- int){ doSomething }

زمانیکه شما تلاش کنید کانال با جهت های مختلف به پارامتر ورودی تابع فقط ارسال کانالی را پاس دهید با خطای زیر مواجه خواهید شد :

1invalid operation: <-ch (receive from send-only type chan<- int)

به مثال ساده زیر توجه کنید :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 3)
 7	go process(ch)
 8	fmt.Println(<-ch)
 9}
10func process(ch chan<- int) {
11	ch <- 2
12}
1$ go run main.go
22

در کد فوق ما یک تابع به نام process ایجاد کردیم که کانال فقط ارسال به عنوان پارامتر ورودی دارد و در ادامه ما کانال ch را که دو طرفه است به این تابع پاس دادیم و مقدار دریافتی را چاپ کردیم.

3.6.4.2 کانال فقط دریافت #

برای ایجاد کانال فقط دریافت شما می توانید به شکل زیر برای ورودی یا خروجی تابع تعریف کنید :

1func process(ch <-chan int){ doSomething }

زمانیکه شما تلاش کنید کانال با جهت های مختلف به پارامتر ورودی تابع فقط دریافت پاس دهید با خطای زیر مواجه خواهید شد :

1invalid operation: ch <- 2 (send to receive-only type <-chan int)

به مثال ساده زیر توجه کنید :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 3)
 7	ch <- 2
 8	process(ch)
 9}
10func process(ch <-chan int) {
11	s := <-ch
12	fmt.Println(s)
13}
1$ go run main.go
22

3.6.5 گرفتن ظرفیت یک کانال #

شما می توانید همانند slice یا آرایه ظرفیت یک کانال را با استفاده از تابع ()cap ببینید.

1package main
2
3import "fmt"
4
5func main() {
6    ch := make(chan int, 3)
7    fmt.Printf("Capacity: %d\n", cap(ch))
8}
1$ go run main.go
2Capacity: 3
توجه کنید ظرفیت کانال بافر نشده همیشه صفر است.

3.6.6 گرفتن طول یک کانال #

شما با استفاده از تابع ()len می توانید طول و اندازه یک کانال را بگیرید و ببینید چه مقدار داده داخل کانال قرار دارد.

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 3)
 7	ch <- 5
 8	fmt.Printf("Len: %d\n", len(ch))
 9
10	ch <- 6
11	fmt.Printf("Len: %d\n", len(ch))
12	ch <- 7
13	fmt.Printf("Len: %d\n", len(ch))
14}
1$ go run main.go
2Len: 1
3Len: 2
4Len: 3

3.6.7 عملیات بستن (close) یک کانال #

در زبان گو ما یک تابع Built-in به نام close داریم که می توانیم برای بستن یک کانال استفاده کنیم و زمانیکه که یک کانال بسته شود دیگر نمی توانیم داده ای را به آن کانال ارسال کنیم. کانال معمولا زمانی بسته می شود که همه داده ها ارسال شده است و داده دیگری برای ارسال نداریم و باید کانال را ببندیم.

به مثال زیر توجه کنید :

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func main() {
 9    ch := make(chan int)
10    go sum(ch, 3)
11    ch <- 2
12    ch <- 2
13    ch <- 2
14    close(ch)
15    time.Sleep(time.Second * 1)
16}
17
18func sum(ch chan int, len int) {
19    sum := 0
20    for i := 0; i < len; i++ {
21        sum += <-ch
22    }
23    fmt.Printf("Sum: %d\n", sum)
24}
1$ go run main.go
2Sum: 6

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

توجه کنید ارسال داده برروی کانال بسته شده ممکن است برنامه شما با خطای panic مواجه و کاملا متوقف شود.

1package main
2func main() {
3    ch := make(chan int)
4    close(ch)
5    ch <- 2
6}
1$ go run main.go
2panic: send on closed channel

اما برای اینکه بتوانیم جلوی این panic رخ داده را بگیریم می توانیم زمانیکه داریم از کانال مقدار دریافت می کنیم assertion انجام دهیم تا متوجه بسته بودن کانال شویم :

 1package main
 2import (
 3    "fmt"
 4)
 5func main() {
 6    ch := make(chan int, 1)
 7    ch <- 2
 8    val, ok := <-ch
 9    fmt.Printf("Val: %d OK: %t\n", val, ok)
10
11    close(ch)
12    val, ok = <-ch
13    fmt.Printf("Val: %d OK: %t\n", val, ok)
14}
1$ go run main.go
2Val: 2 OK: true
3Val: 0 OK: false

اگر مقدار ok از کانال دریافتی true باشد یعنی کانال بسته نشده است و اگر مقدار false دریافت کنیم یعنی کانال بسته شده است.

3.6.8 استفاده از حلقه for-range برروی کانال #

یکی از کاربردی ترین حالت های دریافت داده از کانال استفاده از حلقه for-range است که می توانید تا زمان بسته شدن کانال مقدار دریافت کنید.

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func main() {
 9	ch := make(chan int, 3)
10	ch <- 2
11	ch <- 2
12	ch <- 2
13	close(ch)
14	go sum(ch)
15	time.Sleep(time.Second * 1)
16}
17
18func sum(ch chan int) {
19	sum := 0
20	for val := range ch {
21		sum += val
22	}
23	fmt.Printf("Sum: %d\n", sum)
24}
1$ go run main.go
2Sum: 6

در کد فوق ما یک کانال بافر شده با ظرفیت ۳ ایجاد کردیم سپس ۳ تا مقدار به کانال ارسال کردیم و داخل تابع sum مقادیر را با استفاده از حلقه for-range دریافت و پس از آن چاپ کردیم.

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

پس سعی کنید همیشه و همه جا در جای درست بستن کانال را انجام دهید تا دچار مشکلات مختلف نشوید.

3.6.9 کانال nil #

همانطور که در اوایل این بخش گفتیم مقدار پیش فرض یک کانال nil است و زمانیکه ما یک کانال بدون تابع make تعریف می کنیم مقدار پیش فرضش nil خوهد بود.

1package main
2
3import "fmt"
4
5func main() {
6    var a chan int
7    fmt.Print("Default zero value of channel: ")
8    fmt.Println(a)
9}
1$ go run main.go
2Default zero value of channel: <nil>

یکسری نکات در خصوص کانال nil وجود دارد :

  • ارسال داده به یک کانالی که nil است باعث بلاک شدن همیشگی کد شما در آن خط خواهد شد.
  • دریافت داده به یک کانالی که nil است باعث بلاک شدن همیشگی کد شما در آن خط خواهد شد.
  • بستن یک کانالی که nil باشد باعث panic برنامه شما خواهد شد.