[Golang] Tìm hiểu về channel – gặp gỡ Hằng béo, tình địch của Hìn

Một phần của series “Đại ca Phong học Golang

Kì này, chúng ta sẽ làm quen với một nhân vật mới: Hằng béo, cũng là QA của công ty công nghệ A – tình địch của Hìn.

Tóm tắt kiến thức

— Channel giống như cái hộp có 2 đầu: 1 đầu gửi và 1 đầu nhận dữ liệu.

— Khai báo unbuffered channel:

  • a := make(chan int)

— Khai báo buffered channel

  • a := make(chan int, 10)

— Khác nhau giữa buffered channel và unbuffered channel

  • Unbuffered channel không có khoảng trống để chứa dữ liệu, yêu cầu cả 2 goroutines gửi và nhận đều sẵn sàng cùng lúc. Khi 1 goroutine gửi dữ liệu vào channel, luồng xử lý sẽ bị block lại cho tới khi có 1 goroutine đọc từ channel này ra.
Unbuffer channel. Nguồn: stackoverflow
  • Buffrered channel có từ 1 khoảng trống trở lên để chứa dữ liệu, không yêu cầu cả 2 goroutines gửi và nhận cùng phải sẵn sàng cùng lúc. Luồng xử lý chỉ bị block nếu: goroutines gửi bị block nếu không còn khoảng trống trong channel; goroutines nhận bị block nếu trong channel không có dữ liệu.
Bufferred channel. Nguồn: stackoverflow

— Gửi và nhận dữ liệu từ channel

  • data := <- a // lấy dữ liệu từ channel a ra
  • a <- 5 // đưa vào channel a

–> chiều mũi tên là chiều dữ liệu đi

— Deadlock: Có gửi vào thì phải có đọc ra. Nếu ko có cái nào đọc ra –> panic + deadlock

— Unidirection channel (channel 1 chiều):
+ channel 1 chiều chỉ đọc hoặc chỉ ghi, dùng khi chỉ có business logic chỉ đọc/chỉ ghi dữ liệu.
+ Có thể truyền channel bidirection (2 chiều) vào vị trí của params unidirection (1 chiều)

Chém gió về channel

Sơ qua về Hằng béo: Hằng là trùm QA tại công ty công nghệ A. Vốn thông minh từ bé, nên những việc tính toán logic hay lập trình đối với Hằng dễ như ăn kẹo.

Ngấp nghé hai tám cái xuân không còn xanh mấy, nên Hằng có mục tiêu lấy chồng trong năm nay. Tất nhiên, anh Khấc là lựa chọn hàng đầu. Sở hữu số đo ba vòng 100 – 120 – 100, Hằng béo là một đối thủ nặng kí của Hìn – cả về nghĩa đen lẫn nghĩa bóng.

Lại nói về kì trước, Hìn đang cùng anh Khấc tìm hiểu về Goroutines. Điều này không lọt qua được đôi mắt của Hằng béo. Không thể để anh Khấc lọt vào tay Hìn, Hằng quyết định ra tay, tiếp cận anh Khấc.

— Anh Khấc này, em có task purge cache consumer sử dụng channel, mà chưa thông lắm. Anh thông cho em nhé

— Ủa Hằng chuyển sang code Go luôn rồi hả?

— Thằng Phong code lắm bug quá, em vào code luôn cho rồi

— Ừa. Thế Hằng tìm hiểu được gì về channel rồi?

— Theo em hiểu, channel là một cái hộp chứa dữ liệu để giao tiếp giữa các goroutines. Cái hộp này có hai đầu: một đầu đưa dữ liệu vào, một đầu lấy dữ liệu ra.

— *gật gật*

— Có 2 loại channel là unbufferred channel và buffered channel. Unbuffered channel không có khoảng trống để chứa dữ liệu, yêu cầu cả goroutine gửi và goroutine nhận phải sẵn sàng cùng lúc.

— Nếu không sẵn sàng cùng lúc thì sao?

— Thì sẽ bị deadlock

— Thế theo em, chương trình dưới đây sẽ in ra kết quả gì?

package main
import (
"fmt"
)
func main() {
unbuffer := make(chan string)
unbuffer <-< span> "Có làm thì mới có ăn"
fmt.Println(<-< span>unbuffer)
}

view rawcolam_thimoi_coan.go hosted with ❤ by GitHub

— Dễ ợt. In ra dòng “Có làm thì mới có ăn

— Sai toét. Đoạn code trên chạy sẽ sinh ra lỗi

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!
 
goroutine 1 [chan send]:
main.main()
        /home/adminpc/go/src/tutorial/hangbeo.go:32 +0x59

— Ủa lạ nhỉ. Đoạn này đơn giản là đưa dữ liệu vào một unbuffered channel. Sau đó lấy ra để in thôi mà?

— Em quên mất chi tiết này: channel dùng để giao tiếp dữ liệu giữa các goroutines. Nhớ là “các” – tức là từ 2 trở lên. Và ví dụ trên dùng unbuffered channel, yêu cầu cả 2 goroutine gửi và nhận sẵn sàng cùng lúc.

— Oh. Như vậy là chương trình của em chỉ có 1 main goroutines. Mà main goroutines bị block do gửi dữ liệu vào channel? Sau đó chờ hoài không thấy có goroutines nào lấy dữ liệu ra nên bị deadlock và bị panic?

— Đúng là như vậy. Em có thể hiểu về unbuffered channel giống như việc đánh bóng bàn, hoặc chạy tiếp sức vậy. Luôn luôn cần có đối tác sẵn sàng thì mới hoạt động tiếp được

— Oh. Em hiểu hơn rồi. Giống việc em đợi anh Khấc gật đầu là cưới luôn vậy. Hihi

— Hằng liên tưởng bậy quá

. Hằng thử sửa chương trình trên cho nó chạy anh xem nào.

— Hm… Vấn đề ở đây là chỉ có 1 goroutines duy nhất, nên bị block rồi thì không còn goroutines nào để đọc ra. Vậy em chỉ cần wrap đoạn code đọc dữ liệu hoặc gửi dữ liệu vào riêng 1 goroutines là được. Em sẽ sửa như này

package main
import (
"fmt"
)
func main() {
unbuffer := make(chan string)
unbuffer <-< span> "Có làm thì mới có ăn"
go func(){
fmt.Println(<-< span>unbuffer)
}()
}

view rawcolam_thimoi_coan_v2.go hosted with ❤ by GitHub

— Sai tiếp. Chương trình của em vẫn deadlock và panic như thường.

— Hm… Có 2 goroutines rồi mà nhỉ?

— Đúng là có 2 goroutines, nhưng hàm main của em đã bị block ở dòng số 9 khi gửi dữ liệu vào channel rồi, nên gorotines đọc ra còn chưa được tạo ra. Như vậy, có 2 cách sửa cho em:
+ Cách 1: move đoạn code đọc ra lên trước đoạn code gửi vào
+ Cách 2: wrap đoạn code gửi dữ liệu vào 1 goroutines.
Anh viết thử cho coi nè

package main
import (
"fmt"
)
// Cách 1: Move đoạn code đọc dữ liệu ra lên trước
func main() {
unbuffer := make(chan string)
go func() {
fmt.Println(<-< span>unbuffer)
}()
unbuffer <-< span> "Có làm thì mới có ăn"
}
// Cách 2: Wrap đoạn code đưa dữ liệu vào trong 1 goroutines
func main() {
unbuffer := make(chan string)
go func() {
unbuffer <-< span> "Có làm thì mới có ăn"
}()
fmt.Println(<-< span>unbuffer)
}

view rawcolam_thimoi_coan_v3.go hosted with ❤ by GitHub

— Oh. Em hiểu rồi. Thế mà em tưởng thằng Phong code dâm, tự dưng tạo thêm gorotines làm gì.

— Còn buffered channel thì sao? Hằng nói anh nghe coi.

— Buffered channel thì có khoảng trống để chứa dữ liệu. Do vậy, không cần cả hai goroutines gửi và nhận cùng sẵn sàng một lúc. Luồng xử lý chỉ bị block trong 2 trường hợp:
+ Nếu channel bị rỗng –> goroutines đọc ra sẽ chờ cho tới khi có dữ liệu được đẩy vào để lấy ra
+ Nếu channel bị đầy –> goroutines gửi vào sẽ chờ cho tới khi có dữ chỗ trống để đẩy dữ liệu vào.

— Đúng vậy. Khái niệm này giống như Blocking Queue trong JAV… à nhầm, Java vậy.

— Ủa, như vậy thì xài luôn buffered channel cho rồi. Cần gì sinh ra unbuffered channel nữa nhỉ? Buffered channel rõ ràng là nhanh hơn, do không luôn bị block như unbuffered channel.

— Unbuffered channel thường dùng khi cần sự đồng bộ trong giao tiếp giữa hai goroutines. Còn buffered channel thường dùng khi cần sự bất đồng bộ, để xử lý cho nhanh. Bài này cũng dài rồi, để bài sau anh giới thiệu về worker pool, tiện xài buffered channel cho Hằng coi nhé ^^

— Ơ, nhưng em vẫn còn thắc mắc.

— Hằng sủa… à nhầm, nói đi xem còn thắc mắc gì?

— Em tìm hiểu thì có thấy có kiểu unidirectional channel, tức là channel một chiều. Em muốn hỏi:
+ “một chiều” ở đây có nghĩa là gì?
+ Khi nào thì dùng channel “một chiều” ?

— Hỏi hay lắm. Unidirectional channel, hay channel một chiều, là loại channel chỉ gửi, hoặc chỉ nhận dữ liệu. Loại channel này thường được dùng trong những đoạn logic mà chỉ được đọc, hoặc chỉ được ghi dữ liệu, tránh những action không mong muốn khi maintain sau này.

— Anh ví dụ đi

— Ví dụ như đoạn code dưới đây, anh có 2 function read và write. Function read chỉ đọc dữ liệu từ channel ra, còn function write chỉ ghi dữ liệu vào channel

package main
import (
"fmt"
"time"
)
func read(data <-< span>chan string) {
fmt.Println(<-< span>data)
}
func write(data chan<-< span> string) {
data <-< span> "Có làm thì mới có ăn"
}
func main() {
unbuffer := make(chan string)
go write(unbuffer)
go read(unbuffer)
time.Sleep(1 * time.Second)
}

view rawcolam_thimoi_coan_v4.go hosted with ❤ by GitHub

— Nếu em cố tình ghi dữ liệu trong hàm đọc thì sao?

— Thì em ăn quả compile error vào mồm chứ sao. Error kiểu như này

1
2
# command-line-arguments
./hangbeo.go:9:7: invalid operation: data <- "hihi" (send to receive-only type <-chan string)< code>

— Em hiểu rồi. Thanks anh Khấc. Để em đọc thêm về worker pool rồi sang giao lưu với anh Khấc nhé

Tham khảo

Lời cảm ơn

Như thường lệ, cảm ơn Meo đã giúp anh review bài viết ^^

Cảm ơn bạn đã bỏ thời gian nghe Phong chém. Hi vọng bài viết sẽ giúp ích được cho bạn.

Nếu có gì chưa đúng trong bài thì hãy comment cho Phong biết nhé.

Have a good day ^^