Menu

Channel trong Golang: buffered, unbuffered, close và range

Cách channel trong Go truyền giá trị giữa các goroutine: channel unbuffered và buffered, đóng và duyệt channel, kiểu có chiều, lỗi deadlock, và một pipeline ghép từ các channel.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Gửi và nhận

Channel là một đường ống có kiểu nối giữa các goroutine. ch <- v để gửi, <-ch để nhận. Tạo channel bằng make:

Giá trị zero của kiểu channel là nil, nên var ch chan string mà không có make sẽ cho bạn một channel chặn mãi mãi. Luôn tạo channel bằng make.

Channel unbuffered dùng để đồng bộ

make(chan T) tạo một channel unbuffered. Một lần gửi sẽ chặn cho đến khi có bên nhận lấy giá trị, và một lần nhận sẽ chặn cho đến khi có bên gửi đưa giá trị tới. Hai goroutine gặp nhau tại điểm đó, nên channel unbuffered vừa là công cụ đồng bộ vừa là đường ống dữ liệu. Khi <-done ở dưới trả về, bạn biết worker đã làm xong mọi việc trước lần gửi của nó:

Ở đây đọc result trong main là an toàn mà không cần mutex. Mô hình bộ nhớ của Go bảo đảm mọi thứ worker làm trước lần gửi đều nhìn thấy được trong main sau lần nhận tương ứng. chan struct{} là kiểu chuẩn cho một tín hiệu thuần túy, vì struct{} không chiếm bộ nhớ.

Channel buffered

make(chan T, n) cho channel chỗ chứa n giá trị. Gửi vẫn thành công khi chưa có bên nhận, cho đến khi buffer đầy; nhận thành công cho đến khi buffer rỗng. Giá trị đi ra theo đúng thứ tự đi vào.

Buffer tách rời bên gửi và bên nhận để những đợt dữ liệu ngắn không làm bên gửi bị đứng. Nó không chữa được một producer luôn nhanh hơn consumer; nó chỉ lùi thời điểm bên gửi bị chặn. Hãy chọn kích thước buffer có lý do (số bên gửi, một kích thước lô đã biết), đừng chọn chỉ để deadlock biến mất.

len(ch) chỉ là ảnh chụp tại một thời điểm. Đến lúc bạn dùng nó, goroutine khác có thể đã làm nó thay đổi, nên đừng dùng nó để đoán một lần gửi có bị chặn hay không. Hãy dùng select với nhánh default cho việc đó.

Close và range

close(ch) báo cho bên nhận rằng sẽ không còn giá trị nào được gửi nữa. Sau khi đóng:

  • các giá trị đã nằm trong buffer vẫn được giao,
  • sau đó mọi lần nhận trả về giá trị zero ngay lập tức,
  • v, ok := <-ch cho kết quả ok == false,
  • for v := range ch kết thúc.

Lần nhận đầu tiên sau close vẫn lấy được "last" với ok == true. Lần thứ hai nhận giá trị zero "" và false.

Các quy tắc gây panic:

  • gửi vào channel đã đóng gây panic với send on closed channel,
  • đóng một channel đã đóng gây panic,
  • đóng một channel nil gây panic.

Vì vậy chỉ phía gửi mới đóng channel, và chỉ đóng một lần. Khi có nhiều bên gửi, không bên gửi nào biết khi nào những bên khác xong việc; hãy để một goroutine riêng chờ tất cả bên gửi (bằng sync.WaitGroup) rồi đóng channel sau khi Wait trả về. Chỉ cần đóng channel khi có bên nhận chờ điểm kết thúc. Một channel chưa đóng mà không ai tham chiếu tới sẽ được thu gom rác như mọi giá trị khác.

Kiểu có chiều

Một hàm có thể khai báo rằng nó chỉ gửi hoặc chỉ nhận trên một channel. Khi đó trình biên dịch từ chối thao tác còn lại.

KiểuÝ nghĩaĐược phép
chan Thai chiềugửi, nhận, đóng
chan<- Tchỉ gửigửi, đóng
<-chan Tchỉ nhậnnhận

Một chan T tự động chuyển thành một trong hai kiểu bị giới hạn khi bạn truyền nó vào hàm. Đó là lý do produce ở trên có thể trả về <-chan int: bên gọi duyệt range được nhưng không gửi vào hay đóng được. Hãy dùng kiểu có chiều cho mọi tham số hàm phù hợp. Chúng ghi rõ ai sở hữu channel và biến việc dùng sai thành lỗi biên dịch.

Deadlock

Nếu mọi goroutine đều bị chặn và không gì đánh thức được chúng, runtime sẽ dừng chương trình:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

Bản dump goroutine cho bạn biết thao tác nào bị kẹt (ở đây là chan send, hoặc chan receive, sync.WaitGroup.Wait, select). Các nguyên nhân thường gặp:

  • gửi vào channel unbuffered khi không có bên nhận nào đang chạy,
  • range qua một channel không bao giờ được đóng,
  • bộ đếm WaitGroup không bao giờ về 0,
  • hai goroutine chờ lẫn nhau.

Runtime chỉ phát hiện trường hợp tất cả goroutine đều bị kẹt. Trong một máy chủ còn các goroutine khác đang sống (một HTTP listener, một ticker), cùng lỗi đó không làm gì sập cả; goroutine bị chặn chỉ đơn giản bị rò rỉ.

Channel nil

Gửi và nhận trên một channel nil sẽ chặn mãi mãi. Nghe có vẻ vô dụng, nhưng đây là một mẹo chuẩn bên trong select: gán biến channel thành nil sẽ vô hiệu hóa nhánh của nó. Đoạn code này gộp hai channel và ngừng lắng nghe từng channel khi nó đã đóng:

Nếu không có các phép gán nil, một channel đã đóng luôn ở trạng thái sẵn sàng, và vòng lặp sẽ quay mãi trên các giá trị zero.

Một pipeline

Các channel ghép lại thành pipeline: mỗi giai đoạn là một goroutine nhận từ một channel và gửi sang channel tiếp theo, rồi đóng đầu ra của mình khi đầu vào đã hết.

Mỗi giai đoạn chạy đồng thời, và thứ tự đầu ra là xác định (1, 16, 81) vì mỗi giai đoạn là một goroutine duy nhất giữ nguyên thứ tự. Các lần đóng lan truyền theo dây chuyền: generate đóng, làm vòng range của square kết thúc, square đóng đầu ra của nó, và cứ thế xuống đến main.

Điểm yếu của pipeline này: nếu main ngừng đọc sớm, các giai đoạn sẽ bị chặn ở lần gửi mãi mãi. Pipeline thực tế nhận thêm một context.Context hoặc một channel done và dùng select trên nó cạnh mỗi lần gửi.

Channel hay mutex

Channel dùng để chuyển quyền sở hữu dữ liệu và để báo hiệu sự kiện. Một sync.Mutex đơn giản hơn khi cần bảo vệ trạng thái dùng chung mà nhiều goroutine đọc và cập nhật tại chỗ, như một cache hay một bộ đếm. Một struct có mutex bên trong thường rõ ràng hơn một goroutine giữ trạng thái và phục vụ các yêu cầu qua channel. Hãy chọn cách nào khiến code ngắn hơn và quyền sở hữu rõ ràng hơn.

Tra cứu nhanh

Thao tácchannel nilchannel đang mởchannel đã đóng
ch <- vchặn mãi mãichặn đến khi được nhận hoặc buffer còn chỗpanic
<-chchặn mãi mãichặn đến khi có giá trịcác giá trị trong buffer, rồi giá trị zero
v, ok := <-chchặn mãi mãiok là trueok là false khi đã hết giá trị
close(ch)panicđóngpanic
len(ch), cap(ch)0, 0số giá trị trong buffer, kích thước buffersố giá trị còn lại, kích thước buffer

Câu hỏi thường gặp

Channel buffered và unbuffered trong Go khác nhau thế nào?

Channel unbuffered (make(chan int)) không có chỗ chứa: một lần gửi sẽ chặn cho đến khi goroutine khác nhận, nên mỗi lần gửi cũng là một lần trao tay và một điểm đồng bộ. Channel buffered (make(chan int, 3)) chứa được tối đa 3 giá trị; gửi chỉ bị chặn khi buffer đầy, còn nhận chỉ bị chặn khi buffer rỗng.

Đọc từ một channel đã đóng trong Go thì sao?

Nhận từ channel đã đóng không bao giờ bị chặn. Trước tiên nó lấy hết các giá trị còn trong buffer, sau đó luôn trả về giá trị zero của kiểu phần tử. Dùng v, ok := <-ch để phân biệt: ok là false khi channel đã đóng và rỗng. Vòng lặp for v := range ch dừng lại ở thời điểm đó.

Ai nên đóng channel trong Go?

Bên gửi, và chỉ khi bên nhận cần biết sẽ không còn giá trị nào nữa (ví dụ để kết thúc một vòng lặp range). Gửi vào channel đã đóng sẽ gây panic, đóng một channel hai lần cũng vậy, nên nếu bên nhận đóng channel thì sẽ xảy ra tranh chấp với bên gửi. Bạn không cần đóng channel để giải phóng nó; bộ thu gom rác vẫn thu hồi các channel không còn được tham chiếu.

"fatal error: all goroutines are asleep" nghĩa là gì?

Mọi goroutine trong chương trình đều đang bị chặn ở một thao tác channel hoặc một khóa mà không gì có thể hoàn tất, nên runtime dừng chương trình. Nguyên nhân hay gặp nhất là gửi vào một channel unbuffered trong main mà không có goroutine nào khác nhận, hoặc duyệt range qua một channel không bao giờ được đóng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu