Tìm hiểu về Chan Chan trong Go


  • Trùm cuối

    Chan Chan trong Go

    b2ccc0c2-86c4-481d-a9a9-75c828faaedb-image.png

    TRUYỀN CHAN CHAN THÔNG QUA CHANNEL

    Như các bạn đã biết, channel là một trong những tính năng concurrency mạnh mẽ nhất của Golang. Được trang bị với Goroutine và biểu thức select, bạn có thể xây dựng những chương trình phức tạp chạy đồng thời một cách chính xác, thuận tiện và dễ hiểu nhất.

    Về bản chất, một channel là một hàng đợi được chia sẻ một cách an toàn và đồng thời. Mục đích chính của nó là truyền dữ liệu giữa các goroutines. Một cách nói khác là bạn có thể gửi và nhận một instance của bất kỳ loại dữ liệu nào trên một channel. Trong bài viết hôm nay mình sẽ tập trung vào vấn đề gửi một chan type thông qua một channel.

    KHI NÀO CẦN TỚI CHAN CHAN?

    Một ví dụ đơn giản là bạn gửi một chan thông qua một chan để thông báo cho goroutine thực hiện một công việc sau đó nhận kết quả hoặc thông báo công việc đã hoàn thành.

    Cách khởi tạo một chan chan:

    chanOverChan := make(chan chan int)
    

    CÁCH KHAI BÁO

    Bạn sẽ ít khi gặp một chan chan int đơn giản. Thông thường channel sẽ được chứa trong struct

    type data struct {
      retCh chan<- int
    }
    dataCh := make(chan data)
    

    Và bạn có thể thấy chan được trừu tượng hoá bởi func

    type abstractedCh := chan func(int)
    

    Trong trường hợp này, người gửi có thể theo dõi được channel bên trong một func(int) nếu họ muốn hoặc họ có thể gửi những công việc khác nếu họ muốn. Cách sử dụng này được gọi là function closure và nó cực kỳ linh hoạt 😀

    VÍ DỤ MẪU

    Bên dưới đây là một số code mẫu sử dụng 3 trường hợp phổ biến. Trong mỗi trường hợp chúng ta sẽ mô phỏng công việc sử dụng một time.Sleep đơn giản.

    👁 Ví dụ 1: Sử dụng channel bên trong một channel
    Đây là ví dụ đơn giản nhất và dễ hiểu nhất nhưng nó có một số giới hạn:

    • Mỗi doStuff goroutine sẽ sleeep trong một khoảng thời gian. Bạn không thể thay đổi thời gian sleep khi bạn gửi trên ch
    • Mỗi doStuff goroutine chỉ có thể nhận một chan time.Duration duy nhất. Chúng ta sẽ khắc phục giới hạn này trong ví dụ tiếp theo.
    package main
    
    import (
    	"log"
    	"sync"
    	"time"
    )
    
    // function được thực hiện trong goroutine. Nó nhaận một channel trên `chan`, sleep với `t`, sau đó gửi t trở lại trên channel mà nó nhận được.
    func doStuff(t time.Duration, ch <-chan chan time.Duration) {
    	ac := <-ch
    	time.Sleep(t)
    	ac <- t
    }
    
    func main() {
    	// tạo một channel-over-channel type
    	sendCh := make(chan chan time.Duration)
    
    	// khởi tạo 10 doStuff goroutines
    	for i := 0; i < 10; i++ {
    		go doStuff(time.Duration(i+1)*time.Second, sendCh)
    	}
    
    	// gửi channels đến mỗi doStuff goroutine. 
    	recvCh := make(chan time.Duration)
    	for i := 0; i < 10; i++ {
    		sendCh <- recvCh
    	}
    
    	// Nhận dữ liệu trả về trên mỗi channel đã gửi đi, đây là nơi mà dữ liệu được gửi trả lại từ doStuff ở trên
    	var wg sync.WaitGroup
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func() {
    			defer wg.Done()
    			dur := <-recvCh
    			log.Printf("slept for %s", dur)
    		}()
    	}
    	wg.Wait()
    }
    

    Bạn có thể chạy thử ở đây: https://play.golang.org/p/-1lY-4gd4N

    👁 Ví dụ 2: Sử dụng một channel được lưu trữ trong struct

    Code này sẽ trông tương tự như code mẫu trên ngoại trừ:

    • channel sẽ được lưu trữ trong struct
    • Thời gian sleep sẽ được lưu trữ trong cùng một struct đó, vì thế chúng ta có thể truyền nó thông qua channel (Điều này giúp cho code dễ tuỳ chỉnh bởi vì chúng ta có thể thông báo với doStuff thời gian mà chúng ta muốn gửi nó hơn là truyền vào lúc bắt đầu.
    package main
    
    import (
    	"log"
    	"sync"
    	"time"
    )
    
    // trong struct chúng ta sẽ truyền channel tới 1 goroutine chạy doStuff
    type process struct {
    	dur time.Duration
    	ch  chan time.Duration
    }
    
    // hàm goroutine sẽ nhận một process struct 'p' trên ch, sleep với p.dur, sau đó gửi p.dur trên p.ch
    func doStuff(ch <-chan process) {
    	proc := <-ch
    	time.Sleep(proc.dur)
    	proc.ch <- proc.dur
    }
    
    func main() {
    	// khởi tạo goroutines
    	sendCh := make(chan process)
    	for i := 0; i < 10; i++ {
    		go doStuff(sendCh)
    	}
    
    	// tạo 1 mảng lưu trữ mỗi struct mà chúng ta gửi tới goroutines
    	processes := make([]process, 10)
    	for i := 0; i < 10; i++ {
    		dur := time.Duration(i+1) * time.Second
    		proc := process{dur: dur, ch: make(chan time.Duration)}
    		processes[i] = proc
    		sendCh <- proc
    	}
    
    	// nhận trên mỗi struct channel
    	var wg sync.WaitGroup
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func(ch <-chan time.Duration) {
    			defer wg.Done()
    			dur := <-ch
    			log.Printf("slept for %s", dur)
    		}(processes[i].ch)
    	}
    	wg.Wait()
    }
    

    Bạn có thể chạy thử ở đây: https://play.golang.org/p/bJoiGP9ua2

    👁 Ví dụ 3: Sử dụng một channel bên trong một Function Closure:

    Code này cũng sẽ tương tự như ví dụ mẫu trước, bởi vì doStuff sẽ không biết gì về type & dữ liệu được trả về channel. Phương pháp này có cả mặt tốt và mặt xấu. Về mặt tốt thì chúng ta có thể thay đổi code sau này thành bất cứ xử lý nào bên trong function, nhưng về mặt xấu thì chúng ta không thể truyền time.Duration động vào trong doStuff goroutines như ví dụ mẫu trước.

    package main
    
    import (
    	"log"
    	"sync"
    	"time"
    )
    
    func doStuff(dur time.Duration, ch <-chan func(time.Duration)) {
    	ackFn := <-ch
    	time.Sleep(dur)
    	ackFn(dur)
    }
    
    func main() {
    	// khởi tạo doStuff goroutines
    	sendCh := make(chan func(time.Duration))
    	for i := 0; i < 10; i++ {
    		dur := time.Duration(i+1) * time.Second
    		go doStuff(dur, sendCh)
    	}
    
    	// tạo channel và closure function và gửi nó vào send-back channel
    	recvChs := make([]chan time.Duration, 10)
    	for i := 0; i < 10; i++ {
    		recvCh := make(chan time.Duration)
    		recvChs[i] = recvCh
    		fn := func(dur time.Duration) {
    			recvCh <- dur
    		}
    		sendCh <- fn
    	}
    
    	// nhận dữ liệu từ function 
    	var wg sync.WaitGroup
    	for _, recvCh := range recvChs {
    		wg.Add(1)
    		go func(recvCh <-chan time.Duration) {
    			defer wg.Done()
    			dur := <-recvCh
    			log.Printf("slept for %s", dur)
    		}(recvCh)
    	}
    	wg.Wait()
    }
    

    Bạn có thể chạy thử ở đây: https://play.golang.org/p/JAtGxdBVRW

    Tổng kết

    Có rất nhiều cách để ứng dụng chan chan. Trong nhiều trường hợp, bạn muốn "trả về" một thứ gì đó cho goroutine khác, gửi cho nó một chan để gửi về lại một giá trị sẽ là cách đơn giản nhất.



Có thể bạn cũng quan tâm

.
DMCA.com Protection Status