บทนำ
- เรากำลังเขียน 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 ความคิดเห็น
ความเห็นจาก Hacker News
ในฐานะนักวิทยาศาสตร์ เวลาได้ทำงานร่วมกับวิศวกรซอฟต์แวร์มืออาชีพ มักมีหลายอย่างที่พวกเขาทำแล้วผมไม่เข้าใจ
อยากแปะคอมเมนต์แบบไม่จริงจังและใช้ความพยายามต่ำ
มุกโปรแกรมมิงเก่า ๆ จากยุคที่ C และภาษาตระกูลเดียวกันครองโลก ยังใช้ได้อยู่เสมอ
ทำให้นึกถึงดนตรีคลาสสิกของ Buena Vista Social Club
เคยใช้แพตเทิร์น "chan chan Value" หรือ "chan struct{resp chan Value}" ในบางสถานการณ์
channel of channels เป็นแพตเทิร์นที่พบได้ทั่วไป และมักมาในรูปฟิลด์ของ struct type ที่เป็น channel
type request struct { params, reply chan response }บล็อกที่แสดงมุมมองตรงข้ามเกี่ยวกับการใช้ channel เพื่อทำกลไก dynamic dispatch
ทำให้นึกถึง "My favorite Erlang Program" ของ Joe Armstrong
ตอนคลิกลิงก์คาดว่าจะเจออีกอย่างหนึ่ง
ในโค้ด LabVIEW ก็ใช้วิธีคล้ายกันเพื่อรับข้อมูลตอบกลับแบบอะซิงก์