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)發送給你。你可以隨時查看這些郵件,瞭解每個包裹的狀態,從而及時跟進。

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

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) }}
直播中

尚未有邦友留言

立即登入留言