iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
Modern Web

Go 快 Go 高效: 從基礎語法到現代Web應用開發系列 第 14

【Day14】多線程/平行化處理 I | Goroutines 和 Channels

  • 分享至 

  • xImage
  •  

Goroutines 是 Go 語言內建的一種併發(Concurrency)機制,它是一種輕量級的執行單元。Goroutines 與傳統的線程不同,主要是因為它們是由 Go 的運行時(runtime)來管理的,而不是依賴於作業系統的線程。因此,Go 可以在單一程序中高效地管理成千上萬個 Goroutines,而不會產生大量的系統資源開銷。

  • 輕量級:每個 Goroutine 的啟動需要的內存很少(大約 2 KB),相較於傳統線程動輒數 MB 的內存需求,Goroutines 具備更好的資源利用率。
  • 併發調度:Go 的運行時系統負責協調和調度 Goroutines,它會將 Goroutines 映射到系統線程上,但 Goroutines 本身的管理是由 Go 的調度器自動處理,這使得開發者可以專注於編寫業務邏輯,而不需要手動管理線程。
go func() {
    fmt.Println("這是一個 Goroutine!")
}()
  • Channels 的用法(無緩衝):
ch := make(chan int)
go func() {
    ch <- 1 // 傳遞數據
}()
val := <-ch // 接收數據
fmt.Println(val)

Channels 是用來在 Goroutines 之間傳遞資料的管道,它們能保證 Goroutines 之間的通訊是安全且同步的。

  • Channels 的用法(有緩衝):
ch := make(chan int, 2) // 創建一個緩衝區大小為 2 的 Channel
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)

有緩衝的 Channel 允許在不阻塞接收方的情況下,臨時存儲一定數量的數據。

  • 利用 struct{} 型別的 Channel 傳遞信號:
done := make(chan struct{})
go func() {
    // 執行某些操作
    done <- struct{}{} // 傳送完成信號
}()
<-done // 等待完成信號
fmt.Println("操作已完成")

在某些情況下,可能不需要傳遞數據,而只是發送和接收一個 "完成" 信號。在這種情況下,使用空的 struct{} 可以減少記憶體開銷,因為它不佔任何空間。

  • 搭配 defer 確保資源能夠被正確釋放
func main() {
    done := make(chan struct{})

    go func() {
        defer close(done) // 確保完成後關閉通道
        // 執行一些操作
        fmt.Println("正在執行 Goroutine 操作...")
    }()

    <-done // 等待 Goroutine 完成
    fmt.Println("操作已完成")
}

在這個範例中,defer 保證了無論 Goroutine 中的業務邏輯如何,done Channel 都會被關閉。這樣 main 函數就能安全地等待通道被關閉,然後繼續執行後續操作。
defer 是在函數執行結束前自動執行被延遲的操作,即使函數中間有 return發生了錯誤defer 的動作依然會被執行。可以在 Goroutine 的結束或同步信號傳遞後執行清理工作,這樣可以確保資源不會被洩漏。

  • 引入 context 包進行取消和超時控制
import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    go func(ctx context.Context) {
        select {
        case <-time.After(3 * time.Second):
            fmt.Println("Goroutine 完成工作")
        case <-ctx.Done():
            fmt.Println("Goroutine 被取消:", ctx.Err())
        }
    }(ctx)

    // 等待 Goroutine 結束或被取消
    time.Sleep(4 * time.Second)
    fmt.Println("主函數結束")
}
</* Output: */>
Goroutine 被取消: context deadline exceeded
主函數結束

使用 context 可以方便地控制 Goroutines 的生命週期,避免因為 Goroutines 長時間運行而導致資源泄漏。

  • 使用 select 來處理多個 Channel:
ch1 := make(chan int)
ch2 := make(chan string)

go func() {
    ch1 <- 1
}()

go func() {
    ch2 <- "Hello"
}()

select {
case msg1 := <-ch1:
    fmt.Println("接收到數字:", msg1)
case msg2 := <-ch2:
    fmt.Println("接收到字串:", msg2)
}
</* Output: */>(非一定,根據執行環境可能會有所不同)
接收到字串: Hello

select 語法的作用是讓程式在多個 Channel 中選擇一個可以立即進行操作的通道,並執行相應的邏輯。這裡的邏輯是:

  • 如果 ch1 中有數據傳遞,msg1 := <-ch1 會從 ch1 讀取數據並賦值給 msg1,然後執行第一個 case,輸出 "接收到數字" 和相應的數據。
  • 如果 ch2 中有數據傳遞,msg2 := <-ch2 會從 ch2 讀取數據並賦值給 msg2,然後執行第二個 case,輸出 "接收到字串" 和相應的數據。

由於兩個 Goroutines 是並行運作的,這裡的程式有一個無法預測的執行順序問題。ch1 和 ch2 中的數據可能會以任何順序到達,因此 select 語句最終執行哪一個 case 取決於哪個 Channel 首先準備好數據。

  • 使用 select 來處理資源管理與清理

import (
    "fmt"
    "time"
)

func main() {
    done := make(chan struct{})

    go func() {
        defer close(done)
        // 執行操作
    }()
    
    select {
    case <-done:
        fmt.Println("Goroutine 完成")
    case <-time.After(5 * time.Second):
        fmt.Println("操作超時")
    }
}
  • 使用 select 可以設置超時機制,避免因為等待過久而導致資源無法釋放。
注意:
  • select 語句會等待至少一個 Channel 可用時才繼續執行。這裡的 select 不會在兩個 Channel 都未準備好時立即執行,而是會阻塞直到其中一個 Channel 傳遞數據。
  • 一旦其中一個 Channel 傳遞了數據,對應的 case 就會被執行,程式不會同時執行兩個 case

了解 Goroutines 的調度和執行

  • 調度模型:Go 的運行時使用 M
    調度器,將 M 個 Goroutines 映射到 N 個操作系統線程(OS Threads)上。這種模型允許 Go 高效地管理大量 Goroutines,而不會造成過多的系統資源消耗。

  • GOMAXPROCS 的設置:GOMAXPROCS 控制著 Go 程式同時運行的最大 CPU 核心數量。通過調整 GOMAXPROCS,你可以影響程式的併發性能,特別是在多核處理器上運行時。

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 查看當前 GOMAXPROCS 設置
    runtime.GOMAXPROCS(2) // 設置使用 2 個 OS 線程
    fmt.Println("GOMAXPROCS 設置為:", runtime.GOMAXPROCS(0))
}
</* Output: */>
GOMAXPROCS: 8
GOMAXPROCS 設置為: 2

初始的 GOMAXPROCS 通常設置為可用的 CPU 核心數量。
通過 runtime.GOMAXPROCS(n),可以將 GOMAXPROCS 設置為 n,這會影響 Go 調度器同時運行的 OS 線程數量。


應用場景:將業務邏輯與同步控制邏輯分離

type Task struct {
    done chan struct{}
}

func (t *Task) DoWork() {
    go func() {
        defer close(t.done) // 確保任務完成後關閉通道
        // 執行業務邏輯
        // ...
        t.done <- struct{}{} // 同步完成信號
    }()
}

func main() {
    task := &Task{done: make(chan struct{})}
    task.DoWork()
    <-task.done // 等待任務完成
    fmt.Println("任務已完成")
}
  • 將業務邏輯封裝在 Goroutines 中,通過 Channels 傳遞信號來控制同步。

總結

在 Go 語言中,Goroutines 提供了一種輕量且高效的併發處理機制。相較於傳統的線程,Goroutines 具有更好的資源利用率,啟動內存開銷非常小,並且由 Go 的運行時系統自動管理和調度,避免了手動管理線程的複雜性。
ChannelsGoroutines 之間通訊的關鍵,保證了數據傳遞的安全性和同步性。使用 struct{} 型別的 Channel,可以傳遞信號而不涉及額外的數據開銷,節省資源。通過搭配 defer,能夠確保資源在 Goroutine 完成後被正確釋放。
select 語法允許程式在多個 Channel 中進行選擇,這讓開發者能夠在多個併發操作中動態決定接收數據的順序。Goroutines 的執行是並行的,這使得數據傳遞的順序難以預測,但通過 select,程式可以高效處理多個 Goroutines 的通訊。


上一篇
【Day13】Golang 管理程式碼邏輯 | 函數與方法(Functions & Methods)
下一篇
【Day15】多線程/平行化處理 II | WaitGroup 與 Caching 應用
系列文
Go 快 Go 高效: 從基礎語法到現代Web應用開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言