Monday, 19 January 2015

[Go] select và giao thông

Bài viết này giải thích cách hoạt động của câu lệnh "select" trong Go, sử dụng ví dụ trong tutorial "A tour of Go"

1. function fibonacci
(Xem code ở dưới).
- Nhận input là 2 channel kiểu int. (channel sẽ chỉ "truyền" dữ liệu có kiểu "int").
"for { }" => tương đương với "while true" trong khác ngôn ngữ khác, ở đây lặp vô hạn.

Trong ví dụ này, câu lệnh ``select`` có chứa 2 case, theo phần giới thiệu của tutorial:
``select`` lets a goroutine wait on multiple communication operations.
 ``select`` cho phép một goroutine chọn hướng chạy dựa trên kết quả của các hoạt động giao tiếp (truyền data trong channel)
câu lệnh ``select`` sẽ block cho đến khi một trong các ``case`` của nó có thể chạy (channel hoạt động - không bị block), khi đó nó sẽ chạy các câu lệnh trong ``case`` ấy, nếu có nhiều ``case`` đều có thể chạy cùng lúc thì nó sẽ chọn ngẫu nhiên một ``case``.

Để dễ hiểu, ở ví dụ dưới tưởng tượng 2 channel là 2 con đường, nó giao nhau ở ngã tư có 1 anh cảnh sát giao thông.
Nếu dòng xe tiến vào làn ``c``, câu lệnh tính số fibonacci tiếp theo sẽ được thực hiện (x, y = y, x+y), nếu dòng xe tiến vào làn  ``quit`` thì câu lệnh in ra chữ "quit" và return sẽ được thực hiện.

Với ký hiệu truyền dữ liệu vào channel của Go (<-), dòng "c <- x" có nghĩa là khi nào làn đường ``c`` cũng được truyền vào giá trị của x, nhưng với tính chất đồng bộ của một channel, dòng này chỉ "chạy" nếu đầu kia của channel c có ai đó tiêu thụ giá trị x đã được truyền vào.
``select`` giống như anh cảnh sát giao thông đứng giữa ngã tư, luôn kiểm tra 2 làn đường, và nếu có dòng xe đi vào làn "quit" khi có dòng xe đi vào làn "c", thì với sự công tâm của mình, anh sẽ tung đồng xu để cho 1 trong 2 làn xe vượt qua ngã tư.

Vậy func fibonacci sẽ chỉ return khi nào channel quit có giá trị nào đó truyền vào, và nó được anh "select" cho đi.

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

2. function main

Hai dòng đầu tiên tạo 2 channel truyền int.
Các dòng tiếp theo định nghĩa và chạy một hàm không tên (sau từ khoá ``func`` không có tên của hàm) bằng goroutine. Hàm không tên này đơn giản chỉ lặp 10 lần, và in ra giá trị từ channel ``c``, sau đó truyền giá trị 0 vào channel quit.

Sau khi code chạy qua dòng này "}()", vẫn chưa có gì xảy ra, bởi hàm không tên in ra giá trị từ channel ``c``, nhưng hiện tại ``c`` không có giá trị nào được truyền vào cả, goroutine này "block" tại đó, chờ giá trị được truyền vào c.
  • Ở main thread, fibonacci(c, quit) thực hiện gọi function fibonacci đã giải thích ở trên, nó sinh ra liên tục các số fibonacci liên tiếp và truyền vào channel ``c``. 
  • Ở goroutine không tên: sau khi nhận lần lượt nhận và in ra từng số fibonacci, đến khi đủ 10 số (hàm fibonacci chỉ sinh 10 số, vì hàm không tên chỉ tiêu thụ 10 số), dòng ``quit <- 0`` mới được chạy. Sau khi giá trị 0 được truyền vào channel quit, select ở trong func fibonacci sẽ nhận được "tín hiệu" có giá trị truyền vào ở "case <- quit", hiện tại ở đầu kia của channel c không được tiêu thụ (tức case này không chạy) nên select sẽ cho chạy code trong "case <- quit" và in ra dòng chữ "quit" rồi return function. 
Chương trình kết thúc.

Hết!
hvn@familug.org

PS: nếu định nghĩa về "select" ở đầu bài là chính xác, thì trong ví dụ này nó khẳng định main thread của chương trình go cũng là 1 goroutine.
PS2: default case cho phép ``select`` chạy các lệnh trong đó nếu tất cả các case khác đều bị block.