1 คะแนน โดย GN⁺ 2024-08-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

บทนำ

  • เรากำลังเขียน Dolt ซึ่งเป็นฐานข้อมูล SQL แบบมีระบบควบคุมเวอร์ชันตัวแรกของโลกด้วยภาษา Go
  • เช่นเดียวกับโค้ดเบส Go ส่วนใหญ่ เราใช้ channel และ goroutine เพื่อทำงานพร้อมกัน
  • โดยทั่วไปแล้วการเขียนโปรแกรมแบบ concurrent นั้นยาก เราจึงมักใช้วิธีที่เรียบง่ายและเข้าใจได้ตรงไปตรงมา
  • แต่เราได้รับช่วงต่อโค้ดจากโปรเจ็กต์โอเพนซอร์สอีกตัวหนึ่งที่ใช้ channel อย่างสร้างสรรค์มาก
var c chan chan struct{}
  • นี่คือการส่ง channel ข้ามไปมาระหว่าง goroutine อื่น ๆ เพื่อทำ fan-out pattern ระหว่าง worker goroutine
  • วิธีนี้เข้าใจได้ยาก และยังทำงานด้วยลำบากเมื่อคำนึงถึงปัญหา goroutine leak
  • ท้ายที่สุดเราจึงเขียนโค้ดส่วนนี้ใหม่และเอา chan chan struct{} ออกไป

ทำไมถึงทำแบบนี้

  • มีมุกโปรแกรมเมอร์เก่าแก่จากยุคที่ภาษา C และภาษาตระกูลเดียวกันครองโลก
  • หลายคนมีปัญหาในการทำความเข้าใจ pointer
  • Go ก็เป็นภาษาที่สืบทอดแนวคิดมาจาก C จึงทำสิ่งแบบเดียวกันได้
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
}
  • โค้ดนี้คอมไพล์ได้และพิมพ์ i is now 100
  • ใน Go เราก็สามารถทำสิ่งเดียวกันนี้ด้วย channel ได้

โปรแกรมเมอร์ Go สาย 4-chan

  • เราจะเขียนโปรแกรมที่ใช้การอ้างอิงทางอ้อมของ channel 4 ชั้น
  • channel ชั้นบนสุดประกาศเป็น 4-chan
_4chan := make(chan chan chan chan int)
  • ค่าที่ส่งเข้า channel นี้คือ 3-chan
_3chan := make(chan chan chan int)
  • ในแต่ละชั้นของการอ้างอิงทางอ้อม จะสร้าง producer ตาม branching factor ที่กำหนดไว้
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 ก็ทำแบบเดียวกัน
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)
    }
  }
}
  • สุดท้ายก็จะไปถึงขั้นที่ส่งค่าจริง
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 จะนำค่าที่ได้รับมาบวกสะสม
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())
}
  • โปรแกรมนี้คำนวณเลขยกกำลัง 5 ของตัวเลขด้วยวิธีที่กระจายตัวมากที่สุดเท่าที่จะทำได้

ความเห็น

  • มีหลายเหตุผลที่ไม่ควรทำแบบนี้ในโค้ดจริง: ทั้งความยากในการพัฒนาและดีบัก เรื่องศักดิ์ศรี และการโดนเพื่อนร่วมงานบ่น
  • แต่ก็น่าสนใจเพราะมันสนุกมากและใช้งานได้จริง
  • เหตุผลเชิงปฏิบัติข้อหนึ่งคือ เมื่อส่ง channel ผ่าน channel การปิดมันจะกลายเป็นเรื่องยากมาก

บทสรุป

  • ถ้ามีคำถามหรือความเห็นเกี่ยวกับรูปแบบ concurrency สนุก ๆ ใน Go สามารถเข้าไปคุยกับทีมของเราและผู้ใช้ Dolt คนอื่น ๆ ได้ใน Discord

สรุปโดย GN⁺

  • บทความนี้พูดถึงรูปแบบ concurrency แบบสร้างสรรค์ที่ใช้ channel ในภาษา Go
  • แม้จะไม่มีประสิทธิภาพพอสำหรับใช้งานในโค้ดจริง แต่ก็น่าสนใจในเชิงแนวคิด
  • แสดงให้เห็นว่าโปรเจ็กต์อย่าง Dolt สามารถใช้ความสามารถด้าน concurrency ของ Go ได้อย่างไร
  • โปรเจ็กต์ที่มีความสามารถคล้ายกัน ได้แก่ PostgreSQL, MySQL เป็นต้น

1 ความคิดเห็น

 
GN⁺ 2024-08-29
ความเห็นจาก Hacker News
  • ในฐานะนักวิทยาศาสตร์ เวลาได้ทำงานร่วมกับวิศวกรซอฟต์แวร์มืออาชีพ มักมีหลายอย่างที่พวกเขาทำแล้วผมไม่เข้าใจ

    • เคยเห็นโค้ดหนึ่งบรรทัดถูกเรียกผ่าน "ฟังก์ชันอินเทอร์เฟซ" 4 ชั้น
    • แต่ละฟังก์ชันอยู่คนละไฟล์และคนละโฟลเดอร์ ทำให้อ่านโค้ดเหนื่อยมาก
    • พอไล่เข้าไปหลายขั้นก็เริ่มสงสัยว่าจะไปถึงส่วนที่คำนวณจริงเมื่อไร
  • อยากแปะคอมเมนต์แบบไม่จริงจังและใช้ความพยายามต่ำ

    • มีมในไม่กี่ย่อหน้าแรกทำให้ผมหัวเราะในฐานะโปรแกรมเมอร์ C
    • ชอบเห็นภาษาถูกดัดแปลงแปลก ๆ และก็น่าสนใจที่ได้เห็นแบบนี้ใน Go
  • มุกโปรแกรมมิงเก่า ๆ จากยุคที่ C และภาษาตระกูลเดียวกันครองโลก ยังใช้ได้อยู่เสมอ

  • ทำให้นึกถึงดนตรีคลาสสิกของ Buena Vista Social Club

  • เคยใช้แพตเทิร์น "chan chan Value" หรือ "chan struct{resp chan Value}" ในบางสถานการณ์

    • อาจใช้ message bus แทนก็ได้ แต่สุดท้ายก็จะกลายเป็นต้องมาจัดการกับ message bus
  • channel of channels เป็นแพตเทิร์นที่พบได้ทั่วไป และมักมาในรูปฟิลด์ของ struct type ที่เป็น channel

    • ส่งคำขอไป แล้วเมื่อ worker ทำงานเสร็จก็ใส่ผลลัพธ์ลงใน response channel
    • หน้าตาแบบ type request struct { params, reply chan response }
    • สอง channel มีประโยชน์ แต่ยังไม่เคยเห็นเกินสาม channel
  • บล็อกที่แสดงมุมมองตรงข้ามเกี่ยวกับการใช้ channel เพื่อทำกลไก dynamic dispatch

  • ทำให้นึกถึง "My favorite Erlang Program" ของ Joe Armstrong

  • ตอนคลิกลิงก์คาดว่าจะเจออีกอย่างหนึ่ง

    • ไม่ได้เป็นโปรแกรมเมอร์ Go เลยไม่ทันจับมุกในทันที
  • ในโค้ด LabVIEW ก็ใช้วิธีคล้ายกันเพื่อรับข้อมูลตอบกลับแบบอะซิงก์

    • แทนที่จะเท response ลงคิว ก็ส่งข้อความที่มี callback event channel รวมอยู่ด้วย
    • แม้จะเปลืองหน่วยความจำ แต่พอใช้ตอบกลับครั้งเดียวแล้วปิดทิ้ง ก็ถือว่ามีประสิทธิภาพ