, June 27, 2022

0 kết quả được tìm thấy

Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev


  •   5 min reads
Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev

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õ BufferedUnbuffer Channel sẽ khiến bạn trở thành Gopher thực thụ. Bài viết này mình 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.

Mình đã 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 lencap 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 lencap 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 của mình, vài bạn dev Golang mới đã có sự nhầm lẫn giữa việc khai báo capacity1 cho buffered channel thì sẽ tương đương với unbuffered channelhoà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:

  1. Sử dụng một buffered channel để làm queue.
  2. 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 mình 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 liên quan

Golang Channel là gì? Các ứng dụng Channel trong Golang

Channel là kênh giao tiếp trung gian giữa các Goroutines có thể gởi và nhận dữ liệu cho nhau một cách an toàn thông qua cơ chế lock-free....

Golang Channel là gì? Các ứng dụng Channel trong Golang
Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang

Goroutines bản chất là các hàm được thực thi một các độc lập và đồng thời trong Golang cực kỳ đơn giản và gọn nhẹ so với các Thread truyền thống....

Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang
Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022

Rust và Go hiện là 2 ứng cử viên nặng ký và tin dùng cho các hệ thống lớn. Rust và Golang trông có vẻ tương tự nhau nhưng chúng cũng rất khác biệt...

Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022
Ứng dụng Clean Architecture cho service Golang REST API

Trong bài viết này mình sẽ cung cấp một ví dụ cụ thể cho việc vận dụng nguyên tắc và tư duy Clean Architecture cho một service REST API Golang...

Ứng dụng Clean Architecture cho service Golang REST API
Golang Interface - Những lỗi sai thường gặp và giải pháp

Liệu bạn đã dùng Golang Interface đúng cách và hiệu quả? Bài viết này mình sẽ hướng dẫn chi tiết để các bạn dễ dàng sử dụng trong các service Golang nhé...

Golang Interface - Những lỗi sai thường gặp và giải pháp
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.