
Trong bài viết này chúng ta sẽ thảo luận về channel và cách mà các goroutine giao tiếp với nhau.
Trong bài viết này chúng ta sẽ thảo luận về channel và cách mà các goroutine giao tiếp với nhau.
Channel có thể được xem như là các đường ống giúp cho các channel có thể giao tiếp với nhau. Giống như nước có thể chảy từ đầu này đến đầu kia của ống, dữ liệu có thể được gửi và nhận ở hai đầu khác nhau bằng channel.
Mỗi channel được khai báo với một kiểu dữ liệu nhất định, và channel chỉ cho phép vận chuyển data có kiểu được khai báo trước mà thôi. Data thuộc kiểu khác kiểu được khai báo với channel sẽ không được vận chuyển qua channel.
Zero value của một channel là nil
.
chan T
là một channel có kiểu dữ liệu T
. Channel nil
không thể được sử dụng, vì vậy một channel phải được khai báo với make
, tương tự như map và slice.
Ví dụ về channel,
package main
import "fmt"
func main() {
var a chan int
if a === nil {
fmt.Println("channel a is nil, going to define it")
a = make(chan int)
fmt.Println("Type ò a í %T", a)
}
}
Channel a
được khai báo ở dòng var a chan int
có giá trị nil
(zero value của channel là nil). Vì vậy câu lệnh if
thỏa điều kiện và thực thi khai báo channel a
với make
. a
trong ví dụ trên mà một channel có kiểu int
. Kết quả in ra của chương trình trên,
channel a is nil, going to define it
Type of a is chan int
Channel có thể được khai báo ngắn gọn như sau
a := make(chan int)
Cú pháp gửi và nhận dữ liệu từ channel.
data := <- a // đọc dữ liệu từ channel a
a <- data // đưa dữ liệu vào channel a
Hướng của mũi tên đối với channel xác định rằng dữ liệu đang được gửi đi hay chúng ta đang nhận dữ liệu từ channel.
Ở dòng đầu tiên, mũi tên hướng ra ngoài channel a
, vì vậy chúng ta đang nhận dữ liệu từ channel và lưu vào biến data
.
Ở dòng thứ hai, mũi tên trỏ vào channel a
, vì vậy ở đây chúng ta đang gửi dữ liệu vào channel.
Khi dữ liệu được gửi vào channel thì goroutine thực hiện việc gửi dữ liệu sẽ bị block (dừng thực thi) lại ngay tại câu lệnh gửi cho đến khi có một goroutine khác đọc dữ liệu của channel này.
Tương tự, khi đọc dữ liệu từ một channel thì goroutine thực hiện việc đọc dữ liệu sẽ bị block ngay tại câu lệnh đọc cho đến khi có một goroutine khác gửi dữ liệu vào channel đó.
Thuộc tính trên của channel giúp cho các goroutine giao tiếp với nhau hiệu quả hơn.
Chúng ta cùng xem lại ví dụ trong bài Goroutine và viết lại nó sử dụng channel.
Ví dụ cũ,
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")
}
Đây là ví dụ trong bài trước, chúng ta dùng sleep
để goroutine main
đợi goroutine hello
hoàn thành.
Chúng ta sẽ cùng viết lại ví dụ trên với 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 trên, chúng ta tạo một boolean channel là done
tại dòng done := make(chan bool)
và truyền nó vào goroutine hello
. Chúng ta nhận nhận dữ liệu từ done
channel ở dòng <-done
. Như bạn đã biết thì chương trình sẽ bị block tại dòng này và dừng lại đợi cho đến khi một goroutine gửi dữ liệu vào channel done
. Vì vậy chúng ta không cần dùng sleep
ở đây nữa.
Ở dòng <-done
, chúng ta nhận dữ liệu từ channel nhưng không làm gì với dữ liệu trả về hết.
Bây giờ chúng ta có goroutine main
đã bị block và đợi dữ liệu từ channel done
. Goroutine hello
nhận vào tham số channel done
, in ra Hello world goroutine
, và sau đó gửi true
và channel done
. Khi dữ liệu được gửi vào channel done
cũng là lúc goroutine main
nhận được dữ liệu từ done
, goroutine main
sẽ tiếp tục được thực thi, in ra main function
.
Hello world goroutine
main function
Hãy chỉnh sửa ví dụ trên sử dụng sleep
trong goroutine hello
để hiểu rõ hơn về tính chất block của channel.
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")
}
Trong chương trình trên, chúng ta sleep
4 giây trong goroutine hello
.
Chương trình sẽ in ra Main going to call hello go goroutine
đầu tiên. Sau đó goroutine hello
bắt đầu chạy và in ra hello go routine is going to sleep
. Sau câu lệnh print, goroutine hello
sẽ sleep
4 giây, trong khoản thời gian này goroutine sẽ bị block vì nó cần đợi dữ liệu được gửi vào channel done
để thực thi các dòng tiếp theo. Sau 4 giây, chương trình in ra hello go routine awake and going to write to done
, cuối cùng sẽ in ra Main received data
.
Trong ví dụ này chúng ta sẽ in ra tổng của bình phương và lập phương của từng chữ số trong một số.
Ví dụ, với 123 thì chương trình sẽ tính toán như sau:
Chúng ta sẽ cấu trúc chương trình sao cho tổng bình phương và tổng lập phương của các chữ số sẽ được tính toán trong 2 goroutine khác nhau, và kết quả cuối cùng sẽ được tính trong goroutine main
.
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)
}
Hàm calcSquares
tính tổng bình phương của các chữ số và gửi kết quả về channel squareop
. Tương tự, hàm calcCubes
tính tổng lập phương của các chữ số và gửi kết quả về channel cubeop
.
Hai hàm này chạy trên hai goroutine khác nhau và mỗi hàm đều nhận vào một channel để truyền dữ liệu. Ở dòng squares, cubes := <-sqrch, <-cubech
, goroutine main
đợi kết quả trả về của hai channel truyền vào hai hàm trên. Khi nhận dữ liệu từ cả hai channel thì lưu vào hai biến squares
và cubes
. Kết quả chương trình in ra Final output 1536
Một khía cạnh quan trọng cần quan tâm khi sử dụng channel là deadlock. Nếu một goroutine gửi dữ liệu vào một channel, thì có nghĩa là chúng ta mong đợi rằng sau đó sẽ có goroutine nào đó nhận dữ liệu đó. Nếu điều đó không xảy ra thì chương trình xảy ra lỗi deadlock lúc runtime.
Tương tự, nêu một goroutine chờ để nhận dữ liệu từ một channel, thì có nghĩa là chúng ta mong đợi rằng sau đó có một goroutine nào khác gửi dữ liệu vào channel đó, nếu không chương trình sẽ xảy ra lỗi.
package main
func main() {
ch := make(chan int)
ch <- 5
}
Trong chương trình trên, channel ch
được tạo ra và chúng ta gửi 5
vào channel này trong dòng ch <- 5
. Trong chương trình này, không có goroutine nào khác nhận dữ liệu từ channel ch
. Vì vậy chương trình này sẽ lỗi lúc runtime.
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox046150166/prog.go:6 +0x50
Tất cả channel từ đầu đến giờ đều là channel 2 chiều (bidirectional channels), có nghĩa là chúng có thể được dùng cho cả hai mục đích là gửi và nhận dữ liệu. Ngoài ra chúng ta cũng có thể tạo ra channel 1 chiều, là channel chỉ gửi hoặc nhận dữ liệu.
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}
Trong chương trình trên, chúng ta tạo ra channel chỉ gửi là sendch
, chan<- int
dùng để khai báo channel chỉ gửi (mũi tên trỏ vào chan
). Tại dòng fmt.Println(<-sendch)
, chúng ta muốn nhận dự liệu của channel chỉ gửi. Điều này là không được phép và trình biên dịch sẽ báo lỗi.
./prog.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)
Ok nhưng tại sao chúng ta lại cần một channel chỉ có thể gửi dữ liệu mà không đọc được?
Đó là vì chúng ta có thể chuyển đổi từ channel 2 chiều sang channel 1 chiều (chỉ đọc hoặc chỉ viết). Lưu ý là bạn không thể làm ngược lại, chuyển từ channel 1 chiều sang channel 2 chiều.
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
chnl := make(chan int)
go sendData(chnl)
fmt.Println(<-chnl)
}
Chúng ta khai báo một channel hai chiều ở dòng chnl := make(chan int)
, sau đó truyền nó vào goroutine sendData
ở dòng tiếp theo. Hàm senData
chuyển đổi channel truyền vào thành channe 1 chiều chỉ có thể gửi dữ liệu (sendch chan<- int
). Vì vậy, channel này chỉ có thể gửi trong goroutine sendData
, nhưng nó là channel hai chiều trong goroutine main
. Chương trình trên sẽ in ra 10
.
Goroutine gửi dữ liệu vào channel có thể close
channel đó để báo hiệu cho goroutine nhận dữ liệu biết là không còn dữ liệu được gửi từ channel này nữa.
Khi nhận dữ liệu từ channel, bạn có thể dùng thêm một biến khác để check xem channel đã đóng hay chưa.
v, ok := <- ch
Trong ví dụ trên nếu ok
có giá trị false
thì có nghĩa là channel đã bị đóng. Giá trị đọc được của môt channel đã đóng là zero value của kiểu dữ liệu mà channel đó được khai báo. Ví dụ, giá trị nhận về của một channel đã đóng kiểu int
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 ví dụ trên, goroutine producer
gửi các số từ 0
đến 9
vào channel chnl
rồi sau đó đóng channel lại. Vòng lặp for
trong gouroutine main
lặp vô tận, nó sẽ kiểm tra xem channel đã đóng chưa bằng biến ok
. Nếu ok
có giá trị false
có nghĩa là channel đã được đóng, vì vậy nó sẽ thoát ra khỏi vòng lặp. Ngược lại, nếu channel chưa đóng thì sẽ in ra giá trị của value
và ok
.
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
Tương tự như trên, bạn có thể dùng for range để nhận dữ liệu từ channel cho đến khi nó đóng.
Chương trình trên được viết lại với for range như sau.
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 := range ch {
fmt.Println("Received ",v)
}
}
Dòng for v := range ch
nhận dữ liệu từ channel ch
cho đến khi nó bị đóng lại. Khi channel ch
bị đóng, chương trình sẽ tự động thoát ra khỏi vòng lặp. Kết quả in ra
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
Ví dụ trong phần trước, tính tổng bình phương và lập phương của các chữ số trong một số, có thể được viết lại với for range.
Nếu bạn chú ý kỹ hơn thì có thể thấy đoạn code để tìm ra từng chữ số của một số nào đó bị lặp lại ở hai hàm calcSquares
và calcCubes
. Chúng ta sẽ tách phần code này ra thành một hàm riêng và chạy nó đồng thời với hai hàm còn lại.
package main
import (
"fmt"
)
func digits(number int, dchnl chan int) {
for number != 0 {
digit := number % 10
dchnl <- digit
number /= 10
}
close(dchnl)
}
func calcSquares(number int, squareop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit * digit
}
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)
}
Hàm digits
trong chương trình trên chứa logic để lấy ra từng chữ số của một số nào đó, hàm này được gọi và thực thi đồng thời trong cả hai hàm calcSquares
và calcCubes
. Khi chữ số cuối cùng được gửi đi, channel dchnl
sẽ được đóng lại. Hai goroutine calcSquares
và calcCubes
lắng nghe channel dch
của riêng mình bằng for range
cho đến khi channel bị đóng lại. Phần còn lại của chương trình không thay đổi. Kết quả in ra vẫn là Final output 1536
Nguồn: https://golangbot.com/channels/