1 điểm bởi GN⁺ 2024-08-29 | 1 bình luận | Chia sẻ qua WhatsApp

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))
  }
}
  • Ghép mọi thứ lại và chạy
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

 
GN⁺ 2024-08-29
Ý 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 từng thấy một dòng code được gọi qua 4 "hàm interface"
    • Mỗi hàm nằm ở một file và thư mục khác nhau, khiến việc đọc code trở nên rất mệt mỏi
    • Sau vài bước đi sâu vào, tôi bắt đầu tự hỏi bao giờ mới tới được phần thực sự thực hiện tính toán
  • 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

    • Mấy meme ở vài đoạn đầu khá buồn cười với tôi với tư cách là một lập trình viên C
    • Tôi thích nhìn thấy những biến thể kỳ lạ của ngôn ngữ, và thấy điều đó trong Go khá thú vị
  • 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

    • Tôi có thể đã dùng message bus thay thế, nhưng rồi lại rơi vào tình huống phải xử lý message bus
  • 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

    • Gửi request đi, rồi worker sau khi hoàn thành việc sẽ đặt kết quả vào kênh phản hồi
    • type request struct { params, reply chan response }
    • Hai kênh thì hữu ích, còn từ ba kênh trở lên thì tôi chưa từng thấy
  • Một bài blog đưa ra ý kiến phản đối việc dùng channel để triển khai cơ chế dynamic dispatch

    • Được dùng trong ngôn ngữ Limbo, cùng một khái niệm như Go
    • Liên kết blog
  • 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

    • Tôi không phải là lập trình viên Go nên đã không nhận ra trò đùa ngay lập tứ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ộ

    • Thay vì đổ phản hồi vào queue, tôi truyền một message có chứa callback event channel
    • Dù hơi tốn bộ nhớ, nhưng nó hiệu quả vì sẽ được đóng lại sau khi phản hồi cho một lần dùng duy nhất