Go – Channel

Go channel

Channel là các đường ống dùng để kết nối giữa các goroutine bên trong ứng dụng dựa trên Go, cho phép trao đổi thông tin và sau đó truyền các giá trị đến và từ các biến. Giống như cách nước chảy từ đầu bên này sang đầu bên kia đường ống, dữ liệu có thể được gửi từ một đầu và được nhận từ đầu bên kia bằng channel. Nó rất tiện dụng và giúp chúng ta giải quyết được các bài toán hiệu xuất cao

Khai báo

Mỗi channel có một kiểu riêng gắn với nó. Đây là kiểu dữ liệu channel cho phép truyền đi

chan T là channel kiểu dữ liệu T

Giá trị 0 của channel là nil. Channel nil tức là không sử dụng và do đó channel phải được định nghĩa sử dụng make giống như mapslice.

Hãy cùng xem ví dụ về đơn giản về khai báo channel như sau

package main

import "fmt"

func main() {  
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}

channel a được khai báo với giá trị nil. Do đó câu lệnh bên trong điều kiện if sẽ được chạy và channel được định nghĩa sử dụng hàm make. a được khai báo là int channel.

Kết quả

channel a is nil, going to define it  
Type of a is chan int

Gửi và nhận từ channel

Cú pháp gửi và nhận dữ liệu từ môt channel như sau

data := <- a // đọc từ channel a  
a <- data // ghi vào channel a

Hướng của mũi tên đối với channel chỉ định liệu dữ liệu được gửi hay nhận. Mũi tên hướng ra bên ngoài a tức là đang đọc dữ liệu từ a và gán và data. Còn mũi tên hướng vào a tức là đang ghi dữ liệu vào a

Hãy cùng xem ví dụ sau

package main

import (
    "fmt"
    "math/rand"
)

func CalculateValue(values chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    values <- value
}

func main() {
    fmt.Println("Go Channel Tutorial")

    values := make(chan int)
    defer close(values)

    go CalculateValue(values)

    value := <-values
    fmt.Println(value)
}

Trong hàm main, chúng ta gọi values := make(chan int) để tạo một channel mới và sau đó sử dụng bên trong goroutine CalculateValue

Sau khi tạo channel, tiếp theo gọi hàm defer close(values) dùng để đóng channel khi hàm main kết thúc. Tiếp theo chạy một goroutine CalculateValue(values)  truyền vào một tham số values là channel. Bên trong hàm CalculateValue, chúng ta tính toán giá trị ngẫu nhiên đơn từ 1-10 và in ra các giá trị và sau đó gửi các giá trị này đến channel values bằng cách gọi values <- value

Quay trở lại hàm main, chúng ta gọi value := <-values để nhận giá trị từ channel values.

Kết quả

Go Channel Tutorial
Calculated Random Value: {} 7
7

Gửi và nhận mặc định bị chặn

Gửi và nhận cho một channel mặc định bị chặn. Nghĩa là, khi dữ liệu được gửi đến một channel, việc kiểm soát câu lệnh gửi bị chặn cho tới khi một số goroutine khác đọc từ channel đó. Tương tự khi dữ liệu được đọc từ một channel, việc đọc này sẽ bị chặn cho tới khi một số goroutine ghi dữ liệu cho channel đó.

Đặc tính này của channel giúp cho những gì goroutine truyền đi hiệu quả mà không cần phải khóa rõ ràng hoặc các biến điều kiện được sử dụng phổ biến trong các ngôn ngữ lập trình khác.

Channel không có bộ đệm

Sử dụng channel truyền thống bên trong goroutine thường có khả năng dẫn đến những vấn đề mà bạn không mong đợi. Với các channel truyền thống không có bộ đệm, bất cứ khi nào một goroutine gửi giá trị đến channel này, goroutine ngay lập tức bị chặn cho tới khi giá trị được nhận từ channel.

Hãy cùng xem ví dụ sau, code tương tự như ví dụ bên trên. Tuy nhiên, chúng ta thay đổi hàm CalculateValue() cho việc thực hiện fmt.Println sau khi nó đã gửi các tính toán giá trị ngẫu nhiên cho channel.

Trong hàm main, chúng ta thêm vào gọi lại lần 2 cho go CalculateValue(valueChannel), chúng ta mong muốn giá trị 2 gửi cho channel này một cách nhanh chóng

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func CalculateValue(c chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    time.Sleep(1000 * time.Millisecond)
    c <- value
    fmt.Println("Only Executes after another goroutine performs a receive on the channel")
}

func main() {
    fmt.Println("Go Channel Tutorial")

    valueChannel := make(chan int)
    defer close(valueChannel)

    go CalculateValue(valueChannel)
    go CalculateValue(valueChannel)

    values := <-valueChannel
    fmt.Println(values)
}

Sau khi chạy chương trình này bạn sẽ thấy rằng chi có câu lệnh in cuối cùng của goroutine đầu tiên thực sự được chạy. Nguyên nhân cho điều này là việc gọi c <- value đã bị chặn trong goroutine thứ 2 và sau đó hàm main kết thúc chạy trước khi goroutine thứ 2 có cơ hội hoàn thành phần chạy của mình.

Kết quả

Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
1
Only Executes after another goroutine performs a receive on the channel

Channel với bộ đệm

Để khắc phục hành vi bị chặn này chúng ta sẽ sử dụng channel với bộ đệm. Những channel với những bộ đệm này bản chất là các hàng đợi có kích thước nhất định có thể được sử dụng để liên lạc các goroutine chéo nhau. Mục đích để tạo các channel với bộ đệm thay vì không có bộ đệm, chúng ta sử dụng tham số capacity trong câu lệnh make.

bufferedChannel := make(chan int, 3)

Với việc thay đổi thành kênh với bộ đệm, hoạt động gửi, c <- value chỉ chặn bên trong các goroutine nên channel bị đầy.

Hãy cùng nhau cập nhập ví dụ bên trên để sử dụng channel với bộ đệm và hãy cùng xem kết quả. Chú ý rằng, chúng ta phải thêm time.Sleep() ở đoạn cuối cùng hàm main với mục đích để chờ cho tới khi các goroutine kết thúc việc thực thi

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func CalculateValue(c chan int) {
    value := rand.Intn(10)
    fmt.Println("Calculated Random Value: {}", value)
    time.Sleep(1000 * time.Millisecond)
    c <- value
    fmt.Println("This executes regardless as the send is now non-blocking")
}

func main() {
    fmt.Println("Go Channel Tutorial")

    valueChannel := make(chan int, 2)
    defer close(valueChannel)

    go CalculateValue(valueChannel)
    go CalculateValue(valueChannel)

    values := <-valueChannel
    fmt.Println(values)

    time.Sleep(1000 * time.Millisecond)
}

Khi chạy chương trình, chúng ta nên nhìn thấy rằng goroutine thứ 2 quả thực tiếp tục thực thi bất kể goroutine thứ 2  nhận được đã không được gọi trong hàm main. Nhưng nhờ có hàm time.Sleep(), chúng ta có thể nhìn thấy sự khác nhau giữa channel không bộ đệm và bản chất chặn của chúng và channel với bộ đệm và bản chất không chặn (khi không bị đầy).

Kết quả

Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
7
This executes regardless as the send is now non-blocking
This executes regardless as the send is now non-blocking

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.