iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
Modern Web

起步Go!Let's Go!系列 第 26

[ Day 26 ] Go 題外話:Goroutine 和 Channel

  • 分享至 

  • xImage
  •  

最近一直聽到公司同事再說 Go 的高流量高併發,那這兩個又是甚麼呢?
這邊舉一個生活中的例子這樣大家比較好懂:

高流量 & 高併發

假設你是一家快遞公司的管理員,而快遞員則是 Goroutine。你收到了一大堆客戶的包裹送遞訂單(代表高流量),而你的任務是盡快將這些包裹送遞到目的地。

這裡有兩種方式可以處理這個任務:

  1. 傳統方式:
    你(管理員)一次只能處理一個包裹的請求。當你處理一個包裹的請求時,其他包裹的請求必須等待,因此整個過程變得很慢。
    這就像一個普通的同步程式,每個任務都是按照順序進行,沒有並行處理的能力。
  2. 並發方式:
    你將所有包裹的送遞請求交給多個快遞員(Goroutine)。每個快遞員獨立地處理一個包裹的送遞請求,而不需要等待其他快遞員完成任務。
    這就像使用 Goroutine 實現的並發程式設計,每個 Goroutine 可以獨立地執行任務,並且可以同時進行多個任務,從而提高了效率。
    Channel 則扮演著快遞公司的郵件系統的角色。每個快遞員(Goroutine)在完成任務後,都會將包裹的狀態信息通過郵件系統(Channel)發送給你。你可以隨時查看這些郵件,瞭解每個包裹的狀態,從而及時跟進。

通過使用 GoroutineChannel,你可以像管理一家快遞公司一樣高效地處理大量的任務和資料,並確保任務的及時完成和準確交付。

Goroutine

在 Go 的併發機制運作十分簡便,只要偷過 go 關鍵字就可以開啟 goroutine 了。

宣告

Goroutine 可以使用匿名函式來宣告,也可以使用具名函式來宣告,宣告的方式如下:

func sayHello() {
	time.Sleep(600 * time.Millisecond)
	fmt.Println("Hello from named Goroutine!")
}

func main() {
    // Anonymous Goroutine
	go func() {
		time.Sleep(500 * time.Millisecond)
		fmt.Println("Hello from anonymous Goroutine!")
	}()
    
    // Named Goroutine
	go sayHello()

	time.Sleep(3 * time.Second)
	fmt.Println("Main function exiting...")
}

上面程式碼中,有兩個 Goroutine,一個匿名另外一個為具名,接下來,舉個簡單的例子來演示一下 Goroutine 的運作方式,首先,會有四個任務,有三個需要花點時間去執行,另外一個一下子就可以得到結果了:

package main

import (
	"fmt"
	"time"
)

func taskA() {
	for i := 1; i <= 5; i++ {
		fmt.Println("Task A:", i)
		time.Sleep(200 * time.Millisecond)
	}
}

func taskB() {
	for i := 1; i <= 5; i++ {
		fmt.Println("Task B:", i)
		time.Sleep(300 * time.Millisecond)
	}
}

func taskC() {
	for i := 1; i <= 5; i++ {
		fmt.Println("Task C:", i)
		time.Sleep(400 * time.Millisecond)
	}
}

func normalTask() {
	fmt.Println("Normal Task: Running")
	time.Sleep(500 * time.Millisecond)
	fmt.Println("Normal Task: Completed")
}

func main() {
	go taskA()
	go taskB()
	go taskC()

	normalTask()

	time.Sleep(3 * time.Second)
}

執行結果:
Normal Task: Running
Task C: 1
Task B: 1
Task A: 1
Task A: 2
Task B: 2
Task A: 3
Task C: 2
Normal Task: Completed
Task A: 4
Task B: 3
Task A: 5
Task C: 3
Task B: 4
Task C: 4
Task B: 5
Task C: 5

你會不會覺得好像哪裡怪怪的,照邏輯來看 Task A 應該是要先印出東西出來,為甚麼是 Task C 先印出來?
這是由於 Goroutine 是非阻塞的,所以它們的執行順序是不確定的,這取決於 Goroutine 被調度的順序、CPU 資源的分配以及休眠時間的長短等因素。
但這就衍伸出一個問題,如果要讓 main() 函式等待 Goroutine 執行結束後才退出,我們需要一種機制來知道何時 Goroutine 應該退出,以及如何知道所有 Goroutine 都已經結束。這通常通過使用通道channel 來實現,通道可用於 Goroutine 之間的溝通和同步。

Channel

通道 channel 是一個用來在傳遞資料的資料結構。當一個資源需要在 goroutine 之間共用時,它便在 goroutine 之間架起了一個通道,並確保同步交換資料的機制,讓 goroutine 之間可以安全地傳遞數據。
Channel 是一種特別的類型。在任何時候,同時只能有一個 goroutine 存取通道進行發送和接受資料。使用這種佇列的方式是最為高效的,它遵循著先入先出 (First In First Out) 的規則,從而保證了收發資料的順序。

創建通道

那我們要怎麼創建 channel 呢?可以使用 make() 函式來創建。通道的類型是使用 chan 加上元素的類型表示的,如下:

// 創建一個整數類型的通道
ch1 := make(chan int)

// 創建一個字符串類型的通道
ch2 := make(chan string)

// 創建一個自定義類型的通道
type CustomType struct {
    // 自定義類型的字段
}
ch3 := make(chan CustomType)

用 Channel 發送及接收資料

當 Channel 倍創建完後,就可以利用它來收發資料了,那要怎麼做呢?

發送資料

Channel 發送資料的格式長這樣:

通道變數 <- 通道值

這裡稍微解釋一下,通道變數就是剛剛使用 make() 創建好的實例,通道值可以是變數、字串、函數返回值或運算式等,只要跟剛 Channel 類型一致就好了。
那我們就來發資料吧!

func main() {
    ch := make(chan string)
    ch <- "data"
}

當我們執行後,發現出現 fatal error: all goroutines are asleep - deadlock!,為什麼會這樣呢?
這是因為當把資料往 channel 中發送時,如果接收方一直沒有接受,發送的操作就會持續阻塞。 Go 則可以在執行期間發現一些永遠無法發送成功的地方並作出提示。也就會出現剛剛上面的提示。

接收資料

既然都已經發送資料過來了,那就接收吧!要怎麼接收呢?一樣也是用 <-,用 Channel 接收資料也幾個特性:

  • Channel 的發送和接收在不同的 goroutine 間操。而且 Channel 的資料沒有被對方接收到就會持續阻塞,所以接收並需在另外一個 goroutine 進行。
  • 接收將持續阻塞到發送方發送資料。
  • 如果接收方接收時,通道中沒有資料可供接收,那麼接收操作將會阻塞,直到有資料可供接收為止
  • Channel 只能接收一個資料元素。

接收的方式有四種:

  1. 阻塞接收資料:
    當使用 <- 來接收單個資料元素時,如果通道中沒有可用的資料,接收操作將會阻塞,直到有資料可供接收為止。這種情況下,接收操作會一直等待,直到資料被發送到通道中為止。

    data := <-ch
    

    執行該段程式將阻塞,直到接收到資料並傳給 data。

  2. 非阻塞接收資料:
    使用這種非阻塞的寫法從 Channel 接收資料時,將不會發生阻塞。

    data, ok := <-ch
    

    data 為接收到的資料, 如未接收到資料時,data 為 Channel 類型的零值。ok 為是否接收到資料。
    但這種接收方式,可能會造成 CPU 的高佔用,因此使用的很少。

  3. 接收任意資料,忽略掉接收的值:
    下面這種寫法,接收到的值將會被忽略:

    <-ch
    

    執行該程式會發生阻塞,直到接收到資料為止,但接收到的資料會被忽略掉。這個方法實際上只是透過 goroutine 間阻賽收發,進而實現併發同步,實際的作法如下:

    func main() {
        ch := make(chan string)
        go func () {
            fmt.Println("開始 goroutine")
            ch <- "signal"
            fmt.Println("退出 goroutine")
        }()
        fmt.Println("等待 goroutine")
        <-ch
        fmt.Println("完成")
    }
    

    執行結果:
    等待 goroutine
    開始 goroutine
    退出 goroutine
    完成

    主程式在 <-ch 這一行會進行通道的接收操作,所以如果在這之前通道 ch 中還沒有資料,主程式會被阻塞,直到從 goroutine 中的 ch <- "signal" 行發送了資料到通道中為止。這樣就確保了主程式會等待 goroutine 中的資料傳入後才會繼續執行下一步。

  4. 使用迴圈接收資料:
    如果需要進行多個元素的接收操作,那我們可以透過 for-range 來進行資料的接收:

    for data := range ch {
    }
    

    Channel 是可以被遍歷的,遍歷的結果就是接收到的資料。透過 for 遍歷獲得的變數只有一個,就是上面程式碼的 data,實際操作如下:

    /*
    這段程式碼,有一個 `goroutine` 在通道 ch 中發送整數資料,而主程式在通道 ch 上進行接收。接收到通道中的資料後,主程式會將資料印出來,並檢查是否有 6 的元素,如果有,則印出 "通道中有 6 的元素" 並結束程式。
    */
    func main() {
        ch := make(chan int)
        go func(){
            fmt.Println("開始 goroutine")
            for i := 1; i <= 8; i++{
                ch <- i
                time.Sleep(time.Second)
            }
            fmt.Println("退出 goroutine")
        }()
        fmt.Println("等待 goroutine")
        for receive := range ch {
            fmt.Println(receive)
            if receive ==  6 {
                fmt.Println("通道中有 6 的元素")
                time.Sleep(time.Second)
                break
            }
        }
    }
    

    執行結果:
    等待 goroutine
    開始 goroutine
    3
    4
    5
    6
    通道中有 6 的元素

    主程式啟動了一個新的 goroutine,這個 goroutine 中執行了一個匿名函式。在這個匿名函式中,使用了一個迴圈從 3 到 8 發送整數到通道 ch 中,每次發送完後休眠一秒鐘,然後輸出一條訊息 "退出 goroutine"。
    主程式輸出一條訊息 "等待 goroutine",然後進入一個無窮迴圈,用來持續從通道 ch 中接收資料。當接收到資料後,將其印出來,然後檢查是否為 6,如果是,則輸出 "通道中有 6 的元素",然後休眠一秒鐘並跳出迴圈。
    goroutine 中的迴圈發送完所有資料後,如果主程式還在等待接收通道中的資料,主程式會持續等待。當接收到 6 之後,主程式輸出 "通道中有 6 的元素",並在一秒後結束程式。
    這個例子展示了 goroutine 的併發特性,以及通道收發資料的特性。透過這種方式,可以在不同的 goroutine 中進行併發處理,並通過通道進行資料交換,實現程式的併發執行。

以上就是在 Go 中高流量高併發的介紹,當然還不只這些,還有很多可以來解決這個問題,還請大家慢曼去發掘吧!那我們明天見!

參考資料:

  1. 最速網頁開發:用Go Web一手建立高能效網站系統

上一篇
[ Day 25 ] Go 資料庫:建立和管理資料庫連接的指南
下一篇
[ Day 27 ] Go 與 Gin:打造強大的 Web 應用程式 (上)
系列文
起步Go!Let's Go!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言