[Golang nâng cao] Channel trong Golang

Post on: 2023-03-16 23:54:43 | in: Golang
Channel là một tính năng quan trọng trong Go, nó cho phép các goroutine trao đổi dữ liệu một cách an toàn và đồng bộ

Tổng quan

Channel là một kiểu dữ liệu trong Go cung cấp đồng bộ hóa và giao tiếp giữa các goroutine. Chúng có thể được coi như các ống dẫn được sử dụng bởi các goroutine để giao tiếp. Việc giao tiếp giữa các goroutine này không đòi hỏi bất kỳ khóa tường minh nào. Khóa được quản lý bên trong kênh chính chúng. Kênh cùng với goroutine làm cho ngôn ngữ lập trình Go có khả năng đồng thời. Vì vậy, chúng ta có thể nói rằng golang có hai nguyên tắc đồng thời:

  • Goroutine - thực thi độc lập nhẹ để đạt được tính đồng thời / song song.
  • Channels - cung cấp đồng bộ hóa và giao tiếp giữa các goroutine.

Khai báo Channels

Mỗi biến channel chỉ có thể chứa dữ liệu của một kiểu dữ liệu cụ thể. Go sử dụng từ khóa đặc biệt chan khi khai báo một channel. Dưới đây là định dạng để khai báo một channel

var  chan

Điều này chỉ khai báo một channel có thể chứa dữ liệu của kiểu <type> và nó tạo ra một channel nil là giá trị mặc định của một channel. Hãy xem một chương trình để xác nhận điều này.

package main

import "fmt"

func main() {
    var a chan int
    fmt.Println(a)
}

Output

{nil}

Để định nghĩa kênh, chúng ta có thể sử dụng hàm tích hợp sẵn make.

package main

import "fmt"

func main() {
    var a chan int
    a = make(chan int)
    fmt.Println(a)
}

Output

0xc0000240c0

Nó có thể cho địa chỉ khác nhau trên máy của bạn khi được xuất ra

Vậy hàm make ở đây làm gì? Một kênh được đại diện bên trong bởi một cấu trúc hchan với các thành phần chính là:

type hchan struct {
    qcount   uint           //
tổng số dữ liệu trong hàng đợi
    dataqsiz uint           // kích thước của hàng đợi vòng tròn
    buf      unsafe.Pointer // trỏ đến một mảng gồm dataqsiz phần tử
    elemsize uint16
    closed   uint32         //
xác định kênh đã đóng hay chưa
    elemtype *_type         // kiểu phần tử
    sendx    uint           // chỉ mục gửi
    recvx    uint           // chỉ mục nhận
    recvq    waitq          // danh sách các người chờ nhận
    sendq    waitq          // danh sách các người chờ gửi
    lock     mutex
}

Khi sử dụng make, một phiên bản của cấu trúc hchan được tạo ra và tất cả các trường được khởi tạo với các giá trị mặc định của chúng.

Hoạt động trên Channel

Có hai hoạt động chính có thể được thực hiện trên một channel

  • Gửi (Send)
  • Nhận (receive)

Hãy xem mỗi hoạt động một cách chi tiết.

Send Operation

Hoạt động Gửi (Send) được sử dụng để gửi dữ liệu đến kênh. Dưới đây là định dạng để gửi dữ liệu đến một kênh.

ch <- val

trong đó:

  • ch là biến channel
  • val là giá trị được gửi đến channel

Lưu ý rằng kiểu dữ liệu của val và kiểu dữ liệu của channel phải khớp nhau.

Receive Operation

Hoạt động Nhận (Receive) được sử dụng để đọc dữ liệu từ channel. Dưới đây là định dạng để nhận dữ liệu từ một channel.

val := <- ch

trong đó:

  • ch là biến channel,
  • val là biến trong đó dữ liệu được đọc từ channel sẽ được lưu trữ.
Hãy xem một ví dụ về nơi chúng ta sẽ gửi dữ liệu từ một goroutine và nhận dữ liệu đó trong một goroutine khác.
 
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)

    fmt.Println("Sending value to channel")
    go send(ch)

    fmt.Println("Receiving from channel")
    go receive(ch)

    time.Sleep(time.Second * 1)
}

func send(ch chan int) {
    ch <- 1
}

func receive(ch chan int) {
    val := <-ch
    fmt.Printf("Value Received=%d in receive function\n", val)
}

Output

Sending value to channel
Receiving from channel
Value Received=1 in receive function

Trong chương trình trên, chúng ta đã tạo một channel có tên là "ch" với kiểu dữ liệu là "int" có nghĩa là nó chỉ có thể truyền dữ liệu kiểu "int". Hàm "send()" và "receive()" được khởi chạy như là một goroutine. Chúng ta đưa dữ liệu vào channel "ch" trong goroutine "send()" và nhận dữ liệu từ "ch" trong goroutine "receive()".

Một điểm quan trọng cần lưu ý về phép nhận là một giá trị cụ thể được gửi đến channel chỉ có thể được nhận một lần trong bất kỳ goroutine nào. Như bạn có thể thấy, không có khóa nào được sử dụng trong goroutine khi gửi hoặc nhận từ channel. Các khóa được quản lý bên trong các channel và không cần sử dụng khóa rõ ràng trong mã.

Mặc định khi chúng ta tạo channel bằng cách sử dụng hàm make, nó sẽ tạo ra một channel không đệm, điều này có nghĩa là channel được tạo ra không thể lưu trữ bất kỳ dữ liệu nào. Vì vậy, bất kỳ lần gửi dữ liệu nào trên channel đều bị chặn cho đến khi có một goroutine khác để nhận nó. Vì vậy, trong hàm send(), dòng này sẽ bị chặn:

ch <- 1

Trong hàm receive(), mã sẽ bị chặn cho đến khi giá trị được nhận.

val := <-ch

Chúng ta cũng đã thiết lập một thời gian chờ trong hàm chính để cho phép cả hai hàm send và receive hoàn thành. Nếu chúng ta không có một thời gian chờ ở cuối hàm chính, chương trình sẽ kết thúc và hai goroutine có thể không được lên lịch.

Để minh họa việc chặn khi gửi, chúng ta sẽ thêm một đoạn log sau khi gửi giá trị đến channel ch trong hàm send() và thêm một thời gian chờ trong hàm receive() trước khi nhận giá trị từ ch.

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan int)
go send(ch)
go receive(ch)
time.Sleep(time.Second * 2)
}

func send(ch chan int) {
ch <- 1
fmt.Println("Sending value to channel complete")
}

func receive(ch chan int) {
time.Sleep(time.Second * 1)
fmt.Println("Timeout finished")
_ = <-ch
return
}

Output

Timeout finished
Sending value to channel complete

Thông báo

Timeout finished

luôn xuất hiện trước

Sending value to channel complete

Hãy thử thay đổi các giá trị timeout khác nhau trong hàm receive(). Bạn sẽ nhận thấy thứ tự trên luôn được giữ nguyên. Điều này cho thấy rằng khi gửi trên một channel không được lưu trữ thì mã sẽ bị chặn cho đến khi một goroutine khác nhận trên channel đó. Khi nhận trên một channel, cũng bị chặn cho đến khi có một goroutine khác gửi đến channel đó.

Để minh họa chặn khi nhận, chúng ta sẽ thêm một đoạn log sau khi nhận giá trị trong hàm receive() và thêm một thời gian chờ trong hàm send() trước khi gửi giá trị.

package main

import (
"fmt"
"time"
)

func main() {
  ch := make(chan int)
  go send(ch)

  go receive(ch)
  time.Sleep(time.Second * 2)
}

func send(ch chan int) {
  time.Sleep(time.Second * 1)
  fmt.Println("Timeout finished")
  ch <- 1
}

func receive(ch chan int) {
  val := <-ch
  fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
}

Output

Timeout finished
Receiving Value from channel finished. Value received: 1

Trong chương trình trên, chúng tôi đã thêm một thời gian chờ trước khi gửi đến channel.

Thông báo

Timeout finished

luôn xuất hiện trước

Receiving Value from channel finished. Value received: 1

Hãy thử thay đổi các giá trị timeout khác nhau trong hàm send(). Bạn sẽ nhận thấy thứ tự trên luôn được giữ nguyên. Điều này cho thấy rằng khi nhận trên một channel không được lưu trữ thì mã sẽ bị chặn cho đến khi một goroutine khác gửi trên channel đó.

Chúng ta cũng có thể nhận giá trị trong chính hàm main().

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)
    fmt.Println("Sending value to channel start")
    go send(ch)
    val := <-ch
    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
}

func send(ch chan int) {
    ch <- 1
}

Output

Sending value to channel start
Receiving Value from channel finished. Value received: 1

Chúng ta đã xem ví dụ về một channel không được lưu trữ cho đến bây giờ. Channel không được lưu trữ không có bất kỳ bộ nhớ đệm nào, do đó với một channel không được lưu trữ:

  • Gửi trên kênh bị chặn cho đến khi có goroutine khác để nhận.
  • Nhận bị chặn cho đến khi có goroutine khác ở phía bên kia để gửi.

Trong Go, bạn cũng có thể tạo một channel được lưu trữ. Một channel được lưu trữ có một số dung lượng để chứa dữ liệu, do đó đối với một channel được lưu trữ:

  • Gửi trên channel được lưu trữ chỉ bị chặn nếu bộ đệm đầy.
  • Nhận chỉ bị chặn nếu kênh rỗng.

Đây là cú pháp để tạo một channel được lưu trữ bằng cách sử dụng hàm make:

a = make(chan , capacity)

Đối số thứ hai chỉ định khả năng chứa của channel. Channel không có bộ đệm là có khả năng chứa bằng 0. Đó là lý do tại sao gửi sẽ bị chặn nếu không có bên nhận và nhận sẽ bị chặn nếu không có bên gửi cho channel không có bộ đệm.

Dưới đây là một chương trình cho một buffered channel

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println("Sending value to channnel complete")
    val := <-ch
    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
}

Trong chương trình trên, chúng ta đã tạo một kênh có bộ đệm (buffered channel) với chiều dài là 1 như sau

ch := make(chan int, 1)

Chúng ta đã gửi một giá trị và nhận lại cùng giá trị trong goroutine chính (main goroutine). Điều này là có thể vì việc gửi tới một channel có bộ đệm không bị khóa nếu channel chưa đầy. Do đó, dòng sau không bị khóa đối với một channel có bộ đệm.

ch <- 1

Channel được tạo với dung lượng là một. Do đó, việc gửi đến channel không bị chặn lại và giá trị được lưu trữ trong bộ đệm của channel. Vì vậy, việc gửi và nhận trong cùng một goroutine chỉ có thể thực hiện được cho một channel có bộ đệm. Chúng ta hãy xem hai điểm quan trọng mà chúng ta đã đề cập ở trên:

  • Gửi trên một channel bị chặn lại khi channel đó đã đầy.
  • Nhận trên một channel bị chặn lại khi channel đó rỗng.

Hãy xem một chương trình cho mỗi trường hợp.

Gửi trên một channel bị chặn khi channel đầy. Chúng ta sẽ xem một ví dụ về điều này.

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    ch <- 1
    fmt.Println("Sending value to channnel complete")
    val := <-ch
    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
}

Output

fatal error: all goroutines are asleep - deadlock!

Trong chương trình trên, chúng ta tạo một channel có dung lượng là một. Sau đó, chúng ta gửi một giá trị đến channel và sau đó gửi một giá trị khác đến channel.

ch <- 1
ch <- 1

Chương trình trở thành tình trạng tắc nghẽn do không thể tiếp tục và đó là lý do tại sao như bạn có thể thấy đầu ra là:

fatal error: all goroutines are asleep - deadlock!

Khi nhận trên channel không có gì, thì nó bị chặn.

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println("Sending value to channnel complete")
    val := <-ch
    val = <-ch
    fmt.Printf("Receiving Value from channel finished. Value received: %d\n", val)
}

Output

fatal error: all goroutines are asleep - deadlock!

Trong chương trình trên, chúng ta tạo một channel với dung lượng là một, sau đó chúng ta gửi một giá trị vào channel và sau đó nhận một giá trị từ channel. Sau đó, chúng ta thử nhận giá trị thứ hai từ channel và kết quả là gây ra một tình huống tắc nghẽn vì chương trình không thể tiếp tục khi channel rỗng và không có gì để nhận. Đó là lý do tại sao bạn có thể thấy đầu ra là.

fatal error: all goroutines are asleep - deadlock!

Đoạn mã trên minh họa cho việc receive bị chặn nếu buffer của channel là trống.

Channel Direction

Đến nay, chúng ta đã thấy các channel song hướng trong đó chúng ta có thể gửi và nhận dữ liệu. Trong Golang cũng có thể tạo các channel một chiều. Một channel có thể được tạo ra chỉ để chúng ta gửi dữ liệu và một channel có thể được tạo ra từ đó chúng ta chỉ có thể nhận dữ liệu. Điều này được xác định bởi hướng mũi tên của channel.

  • Một channel mà chúng ta chỉ có thể gửi dữ liệu. 

Đây là cú pháp cho một channel như vậy.

chan<- int
  • Một channel mà chúng ta chỉ có thể gửi dữ liệu từ nó.

Đây là cú pháp cho một channel mà chúng ta chỉ có thể gửi dữ liệu:

<-chan in

Câu hỏi hiện tại là tại sao bạn muốn tạo ra một channel thông qua đó bạn chỉ có thể gửi dữ liệu hoặc từ đó bạn chỉ có thể nhận dữ liệu. Điều đó đến cực kỳ hữu ích khi chúng ta muốn giới hạn chức năng của hàm, chỉ cho phép hàm gửi dữ liệu hoặc chỉ cho phép hàm nhận dữ liệu thông qua channel đã được truyền vào.

Có nhiều cách để truyền kênh như một đối số của hàm. Hướng mũi tên cho một kênh xác định hướng dòng dữ liệu.

  • chan  :kênh hai chiều (đọc và ghi)
  • chan <-  :chỉ ghi vào kênh
  • <- chan  :hỉ đọc từ kênh (kênh đầu vào)
Only Send Channel
  • Chữ ký của một channel mà bạn chỉ có thể gửi đến như vậy sẽ là như sau khi được truyền vào một hàm như một tham số.
func process(ch chan<- int){ //doSomething }
  • Khi cố gắng nhận dữ liệu từ một channel như vậy sẽ cho lỗi sau.
invalid operation: <-ch (receive from send-only type chan<- int)

Hãy bỏ comment khỏi dòng dưới đây trong code để xem lỗi phía trên.

s := <-ch

Code:

package main
import "fmt"
func main() {
    ch := make(chan int, 3)
    process(ch)
    fmt.Println(<-ch)
}
func process(ch chan<- int) {
    ch <- 2
    //s := <-ch
}

Output: 2

Only Receive Channel
  • Chữ ký của một channel mà bạn chỉ có thể nhận dữ liệu, khi được chuyển làm tham số cho một hàm, sẽ giống như sau:
func process(ch <-chan int){ //doSomething }
  • Khi cố gắng gửi dữ liệu đến kênh chỉ có thể nhận, sẽ xảy ra lỗi sau đây
invalid operation: ch <- 2 (send to receive-only type <-chan int)

Hãy thử bỏ comment dòng mã dưới đây trong đoạn mã để xem lỗi được nhắc đến trên

ch <- 2

Code:

package main
import "fmt"
func main() {
    ch := make(chan int, 3)
    ch <- 2
    process(ch)
    fmt.Println()
}
func process(ch <-chan int) {
    s := <-ch
    fmt.Println(s)
    //ch <- 2
}

Output: 2

Dung lượng của một channel sử dụng hàm cap()"

Dung lượng của một channel buffered là số phần tử mà channel đó có thể chứa. Dung lượng này thể hiện kích thước của buffer của channel. Dung lượng của channel có thể được chỉ định khi tạo channel bằng hàm make. Đối số thứ hai là dung lượng của channel

Một channel không buffered (unbuffered channel) sẽ không có dung lượng nào, nhưng vẫn có thể truyền dữ liệu qua channel đó.

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    fmt.Printf("Capacity: %d\n", cap(ch))
}

Output

Capacity: 3

Trong chương trình trên, chúng ta đã chỉ định khả năng chứa là 3 trong hàm make().

make(chan int, 3)

Độ dài của một kênh có thể được lấy bằng hàm tích hợp sẵn len()

Hàm tích hợp len() có thể được sử dụng để lấy độ dài của một kênh trong Golang. Độ dài của một kênh là số phần tử hiện có trong kênh. Do đó, độ dài thực sự đại diện cho số phần tử được đợi trong bộ đệm của kênh. Độ dài của một kênh luôn nhỏ hơn hoặc bằng với dung lượng của kênh.

Độ dài của một unbuffered channel luôn là không.

package main

import "fmt"

func main() {
ch := make(chan int, 3)
ch <- 5
fmt.Printf("Len: %d\n", len(ch))

 ch <- 6
fmt.Printf("Len: %d\n", len(ch))
ch <- 7
fmt.Printf("Len: %d\n", len(ch))
}

Output

Len: 1
Len: 2
Len: 3

Trong đoạn code trên, chúng ta đã tạo một channel có dung lượng là 3. Sau đó, chúng ta tiếp tục gửi một số giá trị đến channel. Như bạn có thể thấy từ đầu ra, sau mỗi thao tác gửi đến channel, độ dài của channel tăng lên một vì độ dài đề cập đến số lượng các phần tử trong bộ đệm của channel.

Thao tác đóng một channel

Hàm Close là một hàm tích hợp sẵn trong Golang để đóng một channel. Khi đóng channel, không thể gửi thêm dữ liệu vào channel nữa. Thông thường, channel được đóng khi đã gửi hết dữ liệu và không có dữ liệu nào khác cần được gửi nữa. Chúng ta hãy xem một chương trình minh họa.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    go sum(ch, 3)
    ch <- 2
    ch <- 2
    ch <- 2
    close(ch)
    time.Sleep(time.Second * 1)
}

func sum(ch chan int, len int) {
    sum := 0
    for i := 0; i < len; i++ {
        sum += <-ch
    }
    fmt.Printf("Sum: %d\n", sum)
}

Output

Sum: 6

Trong chương trình trên, chúng ta đã tạo một channel. Sau đó, chúng ta gọi hàm sum trong một goroutine. Trong hàm main, chúng ta gửi 3 giá trị vào channel và sau đó, chúng ta đóng channel để chỉ ra rằng không thể gửi thêm giá trị nào vào channel được nữa. Hàm sum lặp lại channel bằng cách sử dụng vòng lặp for và tính toán giá trị tổng.
Gửi trên một channel đã đóng sẽ gây ra lỗi panic.

Xem chương trình dưới đây.

package main
func main() {
    ch := make(chan int)
    close(ch)
    ch <- 2
}

Output

panic: send on closed channel

Đóng một channel đã bị đóng sẽ gây ra một lỗi "panic".

Khi nhận dữ liệu từ một kênh, ta cũng có thể sử dụng một biến bổ sung để xác định xem kênh đã bị đóng chưa. Cú pháp dưới đây cho việc đó:

val,ok <- ch

Giá trị của ok sẽ là gì.

  • True nếu channel chưa bị đóng
  • False nếu channel đã bị đóng
package main
import (
    "fmt"
)
func main() {
    ch := make(chan int, 1)
    ch <- 2
    val, ok := <-ch
    fmt.Printf("Val: %d OK: %t\n", val, ok)


    close(ch)
    val, ok = <-ch
    fmt.Printf("Val: %d OK: %t\n", val, ok)
}

Output

Val: 2 OK: true
Val: 0 OK: false

Trong chương trình trên, chúng ta đã tạo một channel với dung lượng là một. Sau đó, chúng ta gửi một giá trị đến channel đó. Biến ok trong lần nhận đầu tiên là true vì channel chưa bị đóng. Biến ok trong lần nhận thứ hai là false vì channel đã bị đóng.

Vòng lặp for range trên một channel

Vòng lặp for range trên một channel có thể được sử dụng để nhận dữ liệu từ channel cho đến khi nó bị đóng

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan int, 3)
ch <- 2
ch <- 2
ch <- 2
close(ch)
go sum(ch)
time.Sleep(time.Second * 1)
}

func sum(ch chan int) {
sum := 0
for val := range ch {
  sum += val
}
fmt.Printf("Sum: %d\n", sum)
}

Output

Sum: 6

Trong chương trình trên, chúng ta đã tạo một channel. Trong hàm main, chúng ta đã gửi ba giá trị đến channel và sau đó, chúng ta đã đóng channel. Sau đó, chúng ta gọi hàm sum và chúng ta đã truyền channel đến hàm đó. Trong hàm sum, chúng ta đã sử dụng một vòng lặp for range trên channel. Sau khi duyệt qua tất cả các giá trị trong channel, vòng lặp for range sẽ thoát vì channel đã bị đóng.

Bây giờ câu hỏi đặt ra là điều gì sẽ xảy ra nếu bạn không đóng channel trong hàm main. Hãy thử bình luận dòng mà chúng ta đóng channel. Sau đó chạy chương trình. Nó cũng sẽ xuất ra deadlock vì vòng lặp for range sẽ không bao giờ kết thúc trong hàm sum.

fatal error: all goroutines are asleep - deadlock!

Nil channel

Giá trị không của channel là nil. Do đó, chỉ khai báo một channel sẽ tạo ra một channel nil vì giá trị không mặc định của channel là nil. Hãy xem một chương trình để minh họa điều đó.

package main

import "fmt"

func main() {
    var a chan int
    fmt.Print("Default zero value of channel: ")
    fmt.Println(a)
}

Output

nil

Một số điểm cần lưu ý về nil channel

  •  Gửi đến nil channel sẽ bị chặn mãi mãi
  •  Nhận từ nil channel sẽ bị chặn mãi mãi
  •  Đóng một nil channel sẽ dẫn đến lỗi panic."

Bảng tóm tắt

Cho đến nay, chúng ta đã thấy 5 hoạt động trên một kênh

  • Send
  • Receive
  • Close
  • Length
  • Capacity

"Hãy xem một bảng tóm tắt hiển thị kết quả của mỗi hoạt động trên các loại channel khác nhau

Command Unbuffered Channel(Not Closed and not nil) Buffered Channel(Not Closed and not nil) Closed Channel Nil Channel
Send Block nếu không có người nhận tương ứng, ngược lại thì thành công Block nếu kênh đầy, ngược lại thì thành công. Panic Chặn mãi mãi
Receive Block nếu không có người gửi tương ứng, ngược lại thì thành công Block nếu kênh trống, ngược lại thì thành công Nhận giá trị mặc định của kiểu dữ liệu từ kênh nếu kênh trống, nếu không thì nhận giá trị thực tế Chặn mãi mãi
Close Success Success Panic Panic
Length 0 Số lượng phần tử được đưa vào hàng đợi trong bộ đệm của channel -0 nếu kênh không có bộ đệm, hoặc số lượng phần tử được đưa vào hàng đợi trong bộ đệm nếu kênh có bộ đệm 0
Capacity 0 Dung lượng bộ đệm của kênh -0 nếu kênh không có bộ đệm - Dung lượng bộ đệm nếu kênh có bộ đệm 0

Tổng kết

 Đây là tất cả về các channel trong golang. Hy vọng bạn thích bài viết. Vui lòng chia sẻ phản hồi/cải tiến/sai lầm trong nhận xét.

 
 
 
 
Tag: go Golang nâng cao