Golang 7 - Concurrency - Channels


Written on March 3, 2024

Channels là gì

Channel có thể hiêu là những đường ống (pipes) để các Goroutines giao tiếp với nhau. Giống như nước chảy từ đầu này qua đầu kia của một đường ống, data co thể được gửi từ đầu này qua đầu kia bằng cách sử dụng channels

Khai báo channels

Mỗi channel định nghĩa một kiểu dữ liệu. Kiểu dữ liệu là kiểu mà data được phép “chảy” qua nó. Không có kiểu dữ liệu nào khác ngoài kiểu dữ liệu của channel thể chuyển qua nó.

chan T là một chanel kiểu T. Chúng ta có thể sử dụng hàm make để khởi tạo giá trị, giống như maps and slices. Ví dụ:

a := make(chan, int)

Đoạn code trên định nnghiax một channel int tên a.

Gửi nhận data trong channel

Syntax để gửi và nhận data từ một channel như sau:

data := <- a // read from channel a
a <- data // write to channel a

Hướng của mũi tên chỉ hướng data gửi hay nhận.

Gửi và nhận data block by default

Khi data được gửi tới một channel, the control block cho tới khi Goroutinne khác đọc từ channel đó. Tương tự, khi data được đọc ở một channel, nó sẽ được block cho tới khi một Goroutine nào đó viết data vào channel đó. Thuộc tính này giúp Goroutine giao tiếp một cách hiệu quả mà không cần khai báo tường minh môt lock hoặc biến điều kiện (conditional variables) khá phổ biến ở những ngôn ngữ khác. Cùng đọc tiếp để hiểu rõ hơn về channel.

Ví dụ về channel

Đây là chương trình lần trước sử dụng Sleep() function.

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Viêt lại chương trình bằng cách sử dụng channel.

package main
import (
  "fmt"
)

func hello(done chan bool) {
  fmt.Println("hello world goroutine")
  done <- true
}

func main() {
  done := make(chan, bool)
  go hello(done)
  <- done
  fmt:Println("main function")
}

Trong chương trình này, chúng ta tạo done là một bool channel và pass nó vào hello Goroutine. Trong main Goroutine, dòng 14, chúng ta nhận gía trị từ channel done. Dòng code này được block cho tới khi một Goroutine nào đó ghi dữ liệu vào, the control sẽ không di chuyển tới dòng code tiếp theo. Do đó, chúng ta không cần sử dụng time.Sleep để tránh main Goroutine kết thúc chương trình.

Chú ý, dòng 14, <- done đọc data từ channel done nhưng không store lại vào bất kì biết nào, điều đó hoàn toàn hợp lệ.

Bây giờ, main Goroutine bị block và đợi cho tới khi hello Goroutine nhận channel này là một paramters, in hello world goroutine và ghi dữ liệu vào done channel. Sau khi ghi xong, main Goroutine nhận data, unblocked and text main funnction được in, chương trình kết thúc.

Ouput của chương trình

Hello world goroutine
main function

Program exited.

Chỉnh sửa một chút bằng cách thêm Sleep vào hello Goroutine.

package main

import (
	"fmt"
	"time"
)

func hello(done chan bool) {
	fmt.Println("hello go routine is going to sleep")
	time.Sleep(4 * time.Second)
	fmt.Println("hello go routine awake and going to write to done")
	done <- true
}
func main() {
	done := make(chan bool)
	fmt.Println("Main going to call hello go goroutine")
	go hello(done)
	<-done
	fmt.Println("Main received data")
}

Output của chương trình:

Main going to call hello go goroutine
hello go routine is going to sleep
hello go routine awake and going to write to done
Main received data

Program exited.

Một ví dụ khác của channel

Chương trình sau in ra tổng của bình phương và lập phương của từng chữ số của một số. Ví dụ nếu input là 123 thì chương trình sẽ tính toán output là:

squares = (1 * 1) +  (2 * 2) + (3*3) = 14
cubes = (1 * 1 *  1) +  (2 * 2 * 2) + (3*3*3) = 36
output = squares + cubes = 50

squares sẽ được tính bằng một Goroutine riêng biệt, cubes cũng tương tự. Sau đó việc tính tổng xảy ra ở main Goroutine.

package main

import (  
    "fmt"
)

func calcSquares(number int, squareop chan int) {  
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {  
    sum := 0 
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
} 

func main() {  
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}

Output:

Final output 1536

Deadlock

Một điều cần lưu ý nữa khi làm việc với channel là vấn đề deadlock. Nếu một Goroutine đang gửi data trên một channel và nó sẽ mong đợi một Goroutine khác nhận dữ liệu đó. Nếu không có Goroutine nào nhận, chương trình sẽ bị panic at runtime, và khi đó deadlock xảy ra.

Deadlock cũng xảy ra tương tự, khi một Goroutine nhận dữ liệu và không có Goroutine nào khác gửi, chương trình cũng bị panic.

Ví dụ

package main


func main() {
	ch := make(chan int)
	ch <- 5
}

Chương trình tạo channel ch và gửi 5 tới channel đó, nhưnng không có Goroutine nào đọc. Chương trình bị panic.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/sandbox046150166/prog.go:6 +0x50

Unidirectional channel

Cách channel chúng ta thảo luận điều là channel 2 chiều, có nghĩa là data đều có thể gửi và nhận. Go cũng support tạo channel 1 chiều, tức là chỉ có thể gửi hoăc nhận data.

package main

import "fmt"

func sendData(sendch chan<- int) {
	sendch <- 10
}

func main() {
	sendch := make(chan<- int)
	go sendData(sendch)
	fmt.Println(<-sendch)
}

Output của chương trình chỉ ra là không thể nhận data từ chỉ-gửi channel.


./prog.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)

Đóng channel và for range loops ở channnel

Bên gửi có khả năng close channel để thông báo cho bên nhận rằng không có thêm data được gửi.

Bên nhận có thể nhận thêm một biến trong khi nhận data để kiểm tra channel đã được đóng hay chưa

v, ok := <- ch

Nếu ok bằng False điều đó có nghĩa là channel đã bị đóng. v sẽ là giá tri zero của channel’s type. Ví dụ channel type là int thì v sẽ là 0

package main

import (
	"fmt"
)

func producer(chnl chan int) {
	for i := 0; i < 10; i++ {
		chnl <- i
	}
	close(chnl)
}
func main() {
	ch := make(chan int)
	go producer(ch)
	for {
		v, ok := <-ch
		if ok == false {
			break
		}
		fmt.Println("Received ", v, ok)
	}
}

Trong chương trình trên, producer Goroutine gửi data từ 1 tới 9 tới channel chnl và đóng channel. Trong main Goroutinne, một vòng lặp vô tận kiểm tra giá trị đọc đươc từ channel, nếu ok false, có nghĩa là channel đã đóng, thì thoát khỏi vòng lăp.

Output:

Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true

Bài viết được lược dịch từ: https://golangbot.com/channels/