Giới thiệu
- Chúng tôi đang viết Dolt, cơ sở dữ liệu SQL có quản lý phiên bản đầu tiên trên thế giới, bằng ngôn ngữ Go
- Giống như hầu hết codebase Go, chúng tôi dùng channel và goroutine để triển khai thực thi đồng thời
- Vì lập trình đồng thời nhìn chung rất khó, nên chúng tôi thường dùng các cách đơn giản và trực quan
- Tuy nhiên, chúng tôi đã thừa hưởng từ một dự án mã nguồn mở khác đoạn mã sử dụng channel theo cách rất độc đáo
var c chan chan struct{}
- Đây là cách truyền channel giữa các goroutine khác nhau để triển khai mẫu fan-out giữa các goroutine worker
- Cách này khó hiểu và cũng khó làm việc cùng khi phải cân nhắc rò rỉ goroutine
- Cuối cùng, chúng tôi đã viết lại đoạn mã này và loại bỏ
chan chan struct{}
Vì sao lại làm vậy
- Có một trò đùa lập trình cũ từ thời C và các ngôn ngữ hậu duệ của nó còn thống trị
- Nhiều người gặp khó khăn trong việc hiểu con trỏ
- Vì Go cũng là một ngôn ngữ bắt nguồn từ C, nên nó cũng có thể làm điều tương tự
func main() {
i := 1
setInt(&i)
fmt.Printf("i is now %d", i)
}
func setInt(i *int) {
setInt2(&i)
}
func setInt2(i **int) {
setInt3(&i)
}
func setInt3(i ***int) {
setInt4(&i)
}
func setInt4(i ****int) {
****i = 100
}
- Đoạn mã này biên dịch được và in ra
i is now 100
- Trong Go, bạn cũng có thể làm điều tương tự bằng channel
Lập trình viên Go 4-chan
- Chúng ta sẽ viết một chương trình dùng 4 mức gián tiếp của channel
- Channel ở mức cao nhất được khai báo là 4-chan
_4chan := make(chan chan chan chan int)
- Giá trị được gửi vào channel này là 3-chan
_3chan := make(chan chan chan int)
- Ở mỗi mức gián tiếp, tạo producer theo một hệ số phân nhánh cố định
func sendChanChanChan(c chan chan chan chan int) {
for range factor {
go func() {
logrus.Debug("starting 3chan producer")
_3chan := make(chan chan chan int)
sendChanChan(c, _3chan)
}()
}
}
- Consumer cũng được xử lý tương tự
func receiveChanChanChan(c chan chan chan chan int) {
for _3chan := range c {
logrus.Debug("got message from 4chan")
for range factor {
logrus.Debug("starting 3chan consumer")
go receiveChanChan(_3chan)
}
}
}
- Cuối cùng ta đến bước gửi giá trị thực tế
func send(_2chan chan chan int, _1chan chan int) {
_2chan <- _1chan
for range factor {
go func() {
logrus.Debug("starting int producer")
for range factor {
go func() {
logrus.Debug("sending int")
_1chan <- 1
}()
}
}()
}
}
- Consumer cộng dồn các giá trị nhận được
var sum = &atomic.Int32{}
func receive(c chan int) {
for s := range c {
logrus.Debug("received int")
sum.Add(int32(s))
}
}
const factor = 3
var sum = &atomic.Int32{}
func main() {
// logrus.SetLevel(logrus.DebugLevel)
_4chan := make(chan chan chan chan int)
go sendChanChanChan(_4chan)
go receiveChanChanChan(_4chan)
time.Sleep(500 * time.Millisecond)
fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
- Chương trình này tính lũy thừa bậc 5 của một số theo cách phân tán tối đa có thể
Bình luận
- Có rất nhiều lý do không nên làm như vậy trong mã thực tế: khó triển khai và debug, vấn đề sĩ diện, và cả sự chê bai từ đồng nghiệp
- Tuy nhiên, nó rất thú vị vì vừa vui vừa thực sự chạy được
- Một lý do thực tế là khi gửi channel bên trong channel, việc đóng chúng trở nên rất khó
Kết luận
- Nếu bạn có câu hỏi hay ý kiến về các mẫu đồng thời thú vị trong Go, bạn có thể trò chuyện với nhóm chúng tôi và những người dùng Dolt khác trên Discord
Tóm tắt của GN⁺
- Bài viết này nói về một mẫu đồng thời độc đáo dùng channel trong ngôn ngữ Go
- Dù không hiệu quả để dùng trong mã thực tế, nó vẫn thú vị về mặt khái niệm
- Nó cho thấy cách có thể tận dụng tính năng đồng thời của Go trong các dự án như Dolt
- Các dự án có chức năng tương tự gồm PostgreSQL, MySQL, v.v.
1 bình luận
Ý kiến trên Hacker News
Với tư cách là một nhà khoa học, khi làm việc cùng các kỹ sư phần mềm chuyên nghiệp, tôi thường không hiểu nhiều việc họ làm
Tôi muốn để lại một bình luận thiếu thực chất với ít công sức bỏ ra
Những câu đùa lập trình cũ từ thời C và các ngôn ngữ phái sinh của nó thống trị vẫn còn đúng đến giờ
Nó làm tôi nhớ tới nhạc cổ điển của Buena Vista Social Club
Tôi đã từng dùng mẫu "chan chan Value" hoặc "chan struct{resp chan Value}" trong một số tình huống nhất định
Kênh của kênh là một mẫu phổ biến, thường xuất hiện dưới dạng một field kiểu channel trong struct
type request struct { params, reply chan response }Một bài blog đưa ra ý kiến phản đối việc dùng channel để triển khai cơ chế dynamic dispatch
Nó làm tôi nhớ tới "My favorite Erlang Program" của Joe Armstrong
Khi bấm vào liên kết, tôi đã kỳ vọng một thứ khác
Trong code LabVIEW, tôi cũng dùng một cách tương tự để nhận dữ liệu phản hồi bất đồng bộ