พูดถึงการจัดการกับ Channels ใน Go


Golang นั้นมี concept อย่าง Goroutines และ Channels ที่ช่วยให้สามารถจัดการงานแบบคู่ขนานได้ง่าย ๆ อยู่

เริ่มจากวิธีสร้าง channel

// var := make(chan <channel_type>, <buffer_size>?)
unbufferedChannel := make(chan int)
bufferedChannel := make(chan int, 10)

buffer ในแชนแนล คือการระบุว่าแชนแนลนี้สามารถรับค่าเข้ามาให้ในแชนแนลได้สูงสุดกี่ตัว

การส่งค่ารับค่าในแชนแนลนั้นสามารถทำได้โดย

bufferedChannel <- 1
newVariable := <- bufferedChannel

ความหมายก็ให้นำค่า 1 ไปเก็บไว้ใน bufferedChannel และบรรทัดต่อมาให้ดึงค่าจากแชนแนลออกมาให้ตัวแปร newVariable ซึ่งในตอนนี้มีค่า 1; ดังนั้น newVariable จะมีค่าเท่ากับ 1 หลังจากจบโปรแกรมนี้

Channels นั้นทำงานเสมือนกับ Queue (FIFO) ดังนั้นในโปรแกรมข้างล่างนี้ a,b,c จะมีค่าเป็น 1,2,3 ตามลำดับ

bufferedChannel <- 1
bufferedChannel <- 2
bufferedChannel <- 3
a := <- bufferedChannel
b := <- bufferedChannel
c := <- bufferedChannel

ทีนี้ unbuffered channels ล่ะต่างกันยัง

Buffered vs Unbuffered

unbufferedChannel <- 1
d := <- unbufferedChannel

ทำแบบนี้โปรแกรมจะไม่สามารถทำงานต่อได้ เนื่องจากว่า unbuffered channels นั้นไม่สามารถใช้เก็บค่าใด ๆ ได้; งั้นแล้วมีให้ใช้มาทำไม? เราสามารถทำให้ค่า 1 ข้างบนถูกเข้า unbufferedChannel ได้ แต่มีเงื่อนไขว่าจะต้องมีตัวแปรมารับจาก unbufferedChannel เหมือนกัน; ในส่วนนี้ Goroutines จะเข้ามามีบทบาทสำหรับงานนี้

go func() {
 unbufferedChannel <- 1
}()
d := <- unbufferedChannel

Goroutines นั้นจะทำงานอยู่ใน background ของตัวโปรแกรมหลัก ดังนั้นในขณะที่ d จะไม่รอรับค่าจาก unbufferedChannel จนติด deadlock เพราะมีฟังก์ชันที่ทำงานอยู่และมีการ assign ค่า 1 ให้กับ unbufferedChannel; อาจจะเข้ามา unbuffered channels นั้นไม่สามารถเก็บค่าได้ แต่ถ้ามีตัวแปรที่รอรับค่าจาก channel นั้นอยู่จะไม่มีปัญหาอะไร

สำหรับ buffered channels นั้นก็ไม่ใช่ว่าจะติด deadlock ไม่ได้

anotherChannel := make(chan int, 2)
anotherChannel <- 1
anotherChannel <- 2
anotherChannel <- 3

anotherChannel สามารถเก็บค่า int ได้อยู่สองตัว แต่ว่าในโค้ดด้านบนนั้นมีการ assign ค่าถึงสามครั้ง ดังนั้นในกรณีนี้จะติด deadlock เอาได้ หากว่าอยากจะให้รับค่า 3 อาจจะต้องเอาค่าบางส่วนออกจากแชนแนลก่อน

การปิด channels ที่ไม่ได้ใช้แล้ว

เราสามารถใช้ฟังก์ชัน close เพื่อทำการปิดแชนแนลที่ไม่มีการทำงานต่อแล้วได้

sampleChannel := make(chan int, 2)
sampleChannel <- 1
sampleChannel <- 2
close(sampleChannel)

แต่ก็ยังสามารถอ่านค่าที่ข้างในแชนแนลอยู่ได้

s1, s2 := <- sampleChannel, <- sampleChannel

สำหรับ channels ที่การันตีว่าจะโดนใช้ close แน่ ๆ จะสามารถใช้ for range อ่านค่าได้

for v := range sampleChannel {
  fmt.Println(v)
}

สามารถใช้ได้ทั้งสองแบบ ให้เลือกใช้แบบนึง

สามารถเช็คว่าแชนแนลนั้นถูกปิดหรือยังได้โดยเพิ่ม variable ตัวที่สองเข้ามาในตอนที่อ่านค่าจากแชนแนล

v, ok := <- sampleChannel
if ok {
  // not close
} else {
  // closed
}

กรณีที่ปิดไปแล้ว v จะมีค่าเท่ากับ default value ของ type แชนแนลนั้น

Select

select เป็นฟังก์ชันที่ใช้เวลาทำงานร่วมกันหลาย ๆ แชนแนลนั้น

sumChannel := make(chan int)
printChannel := make(chan struct{})
go func() {
  sum := 0
  for {
    select {
      case v := <- sumChannel:
        sum += v
      case <- printChannel:
        fmt.Println(sum)
    }
  }
}()
sumChannel <- 4
sumChannel <- 7
sumChannel <- 11
printChannel <- struct{}{}
time.Sleep(time.Second)

select มีการใช้การที่คล้ายกับ switch; select จะ block การทำงานจนกว่าจะมี case ไหนในแชนแนลทำงานได้ (มีค่าจากแชนแนลส่งมา); แต่ select ก็รองรับ default case เหมือนกัน

nonstopChannel := make(chan int)
select {
  case <- nonstopChannel:
    fmt.Println("nonstopChannel is working")
  default:
    fmt.Println("default case is working")
}

ในกรณีนี้ select จะไม่รอรับค่าจาก nonstopChannel หากเวลาที่ select ทำงานแล้วไม่มีค่าใด ๆ ส่งมา default case จะถูกเรียกใช้ กรณีนี้ก็จะแสดงข้อความว่า "default case is working"