Channel trong Golang vốn đã là một chức năng và kỹ thuật rất quan trọng góp phần thành danh cho ngôn ngữ này. Trong đó có khái niệm Buffered Channel có lẽ được thường xuyên nhắc đến trong các buổi phỏng vấn.
Nhiều người quan niệm rằng, hiểu rõ Buffered và Unbuffer Channel sẽ khiến bạn trở thành Gopher thực thụ. Bài viết này sẽ giúp các bạn hiểu rõ và phân biệt được hai khái niệm trên, ghi điểm trong mắt các nhà tuyển dụng.
200Lab đã có một bài viết chi tiết về Unbuffered Channel trước đó, các bạn nên xem trước khi đọc tiếp bài này nhé.
Buffered Channel là gì?
Buffered Channel là một channel trong Golang có khả năng lưu trữ được dữ liệu bên trong nó. Khả năng này được mô tả như sức chứa (capacity) của channel.
Cú pháp khai báo buffered channel:
chanName := make(chan Type, capacity)
Ví dụ:
bufferedChan := make(chan int, 5)
Từ đó bufferedChan
là một buffered channel kiểu int
, có sức chứa là 5
. Tức là tối đa nó có thể lưu được 5 giá trị số nguyên (integer) vào channel.
Các đặc tính của Buffered Channel
Buffered Channel có len và cap
Vì chứa được dữ liệu nên ta sẽ quan tâm đến len
và cap
của Buffered Channel. Cụ thể:
len
: là số lượng giá trị/dữ liệu hiện đang có trong buffered channel.cap
: là sức chứa tối đa của buffered channel.
package main
import (
"fmt"
)
func main() {
bufferedChan := make(chan int, 5)
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
bufferedChan <- 1
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
bufferedChan <- 2
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
<-bufferedChan
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
}
BufferChan has len = 0, cap = 5
BufferChan has len = 1, cap = 5
BufferChan has len = 2, cap = 5
BufferChan has len = 1, cap = 5
Nếu các bạn để ý sẽ thấy rằng Buffered Channel sẽ không block goroutine main nếu sức chứa vẫn còn, mà không cần phải có một goroutine khác lấy dữ liệu từ channel.
Buffered Channel sẽ block goroutine hiện tại nếu vượt sức chứa
package main
import (
"fmt"
)
func main() {
bufferedChan := make(chan int, 5)
bufferedChan <- 1
bufferedChan <- 2
bufferedChan <- 3
bufferedChan <- 4
bufferedChan <- 5
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
bufferedChan <- 6 // deadlock here
}
Trong đoạn code trên, khi len
và cap
bằng nhau (đều là 5
), việc đẩy tiếp dữ liệu vào buffer channel sẽ bị block. Trường hợp này sẽ cần một goroutine khác lấy dữ liệu ra để unblock.
Hãy nhớ là chỉ bị block chứ không bị drop, tức là giá trị 6
vẫn block chờ ở đó khi buffered channel đã đầy (full capacity).
Lấy dữ liệu từ empty buffered channel sẽ block goroutine
Tương tự với việc đẩy dữ liệu vào một full buffered channel, việc lấy dữ liệu từ một empty bufffered channel cũng block goroutine hiện tại.
package main
import (
"fmt"
)
func main() {
bufferedChan := make(chan int, 5)
fmt.Printf("BufferChan has len = %d, cap = %d\n", len(bufferedChan), cap(bufferedChan))
<-bufferedChan // deadlock here
}
Lưu trữ dữ liệu theo thứ tự FIFO (First-In-First-Out)
Dữ liệu đẩy vào và lấy ra khỏi buffered channel theo thứ tự FIFO, việc này khiến nó hoạt động như một queue (hàng đợi):
import "fmt"
func main() {
bufferedChan := make(chan int, 5)
for i := 1; i <= 5; i++ {
bufferedChan <- i
}
for i := 1; i <= 5; i++ {
fmt.Println(<-bufferedChan)
}
}
1
2
3
4
5
Khác biệt giữa buffered channel và unbuffered channel
Sự khác biệt lớn nhất giữa buffered channel và unbuffered channel đó là về capacity. Buffered channel sẽ yêu cầu khi báo capacity lúc khởi tạo channel, unbuffer channel thì không cần.
Tuy nhiên, theo kinh nghiệm phỏng vấn, vài bạn dev Golang mới đã có sự nhầm lẫn giữa việc khai báo capacity
là 1
cho buffered channel thì sẽ tương đương với unbuffered channel là hoàn toàn sai.
package main
func main() {
bufferedChan := make(chan int, 1)
unbufferedChan := make(chan int)
bufferedChan <- 1 // OK
unbufferedChan <- 1 // deadlock
}
Ở ví dụ trên, khi cap = 1
, thì buffered channel chứa được một giá trị và không block main goroutine. Trong khi đó unbuffered channel sẽ block ngay.
Câu hỏi kinh điển: crawl giả lập 10K URLs với buffered channel và goroutines
Có lẽ nhiều bạn Golang Developer đã đôi lần được hỏi câu này trong phỏng vấn: "Hãy viết một chương trình Golang mô phỏng việc crawl 10K URLs nhưng chỉ giới hạn 5 workers (goroutines) chạy đồng thời".
Thực chất đây là câu hỏi kiểm tra khả năng sử dụng buffered channel và goroutines của các bạn mà thôi. Nếu bạn vẫn chưa nắm vững goroutines thì nên xem lại bài Lập trình concurrency chưa bao giờ dễ như Golang.
Bài này có rất nhiều cách giải đúng, nhưng trong khuôn khổ bài viết này thì mình sẽ dùng giải pháp đơn giản như sau:
- Sử dụng một buffered channel để làm queue.
- Chạy 5 goroutines như các tiến trình crawl URL. Crawl giả lập thôi, không cần làm thật đâu các bạn nha.
Code sẽ như sau:
package main
import (
"fmt"
"time"
)
const (
numberOfURLs = 10000
numberOfWorkers = 5
)
func crawlURL(queue <-chan int, name string) {
for v := range queue {
fmt.Printf("Worker %s is crawling URL %d\n", name, v)
time.Sleep(time.Second)
}
fmt.Printf("Worker %s done and exit\n", name)
}
func startQueue() <-chan int {
queue := make(chan int, 100)
go func() {
for i := 1; i <= numberOfURLs; i++ {
queue <- i
fmt.Printf("URL %d has been enqueued\n", i)
}
close(queue)
}()
return queue
}
func main() {
queue := startQueue()
for i := 1; i <= numberOfWorkers; i++ {
go crawlURL(queue, fmt.Sprintf("%d", i))
}
time.Sleep(time.Minute * 5)
}
Full code và chạy thử tại: https://play.golang.com/p/9uHR2jIDR9y
Các bạn có thể code thêm chỗ enqueue URL để tăng tính chân thực hoặc sử dụng WaitGroup để biết khi nào các goroutines đã hoàn tất để kết thúc chương trình nhé.
Lời kết
Qua bài viết này, mình hy vọng các bạn sẽ hiểu hơn về buffered channel và ứng dụng được nó cũng như giải quyết các câu hỏi của nhà tuyển dụng nhé.
Ngoài ra thì trong khoá học Golang của 200Lab cũng có hướng dẫn sử dụng buffered channel cho queue và cả pub/sub. Chúc các bạn học tốt!
Bài viết cùng chủ đề:
- Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang
- Những sai lầm thường thấy khi bạn sử dụng goroutines
- Golang Channel là gì? Các ứng dụng Channel trong Golang
Việt Trần
Yêu thích tìm hiểu các công nghệ cốt lõi, kỹ thuật lập trình và thích chia sẻ chúng tới cộng đồng
follow me :
Bài viết liên quan
Design Pattern là gì? 23 Classic Design Pattern với Golang
Aug 21, 2024 • 3 min read
Concurrency pattern trong Go - Phần 2
Apr 11, 2024 • 7 min read
Concurrency pattern trong Golang - Phần 1
Mar 25, 2024 • 8 min read
So sánh Golang và NodeJS chi tiết
Oct 14, 2023 • 22 min read
Golang Channel là gì? Các ứng dụng Channel trong Golang
Jun 14, 2023 • 12 min read
Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang
Jun 13, 2023 • 8 min read