自己學習起來感覺跟之前碰Thread有點像,然後去詢問了一下差別,最大差距應該是系統佔用程度,另外就是goroutine(協程)是由go runtime(這兩個單字超像)內建排程去調度,必非是由os執行緒,我們來看AI怎麼說
Goroutine 與傳統 OS 的多執行緒 (Multi-threading / OS Thread) 的差別,主要在於對「系統資源的消耗」與「排程機制」的不同。以下為您整理幾個最核心的差異:
1. 資源消耗大小 (Memory Consumption)
- Goroutine(協程):非常輕量化。初始建立時通常只需要約 2KB 的堆疊 (Stack) 記憶體空間,並且會根據需求自動擴充或縮小。
- OS 執行緒 (OS Thread):傳統作業系統的執行緒,建立時通常會分配固定的記憶體大小(約為 1MB 到 8MB 不等)。因此,如果開啟一萬個 Thread 會吃掉 GB 級的記憶體甚至崩潰,但開啟一萬個 Goroutine 對 Go 來說卻非常輕鬆。
2. 建立與銷毀的成本 (Creation/Destruction Cost)
- Goroutine(協程):由 Go 語言自己的 Runtime(執行階段)在使用者空間 (User Space) 來管理,建立和銷毀的成本極低,就相當於程式語言中一般變數的記憶體分配與回收。
- OS 執行緒 (OS Thread):必須向作業系統核心發出請求 (System Call) 才能建立或銷毀。這個過程牽涉到切換至核心空間 (Kernel Space),開銷相當昂貴。
3. 排程者與上下文切換 (Context Switch & Scheduler)
- Goroutine(協程):由 Go Runtime 內建的排程器調度 (稱為 GMP 模型) 處理。你可以把它想像成「程式語言自己內部的執行緒」。當某個 Goroutine 遇到阻塞(例如等待 Channel、網路 I/O 或 Sleep)時,Go 會將它掛起,然後把同一個底層的 OS 執行緒讓給其他準備好的 Goroutine 使用(這稱為 M:N 排程)。切換過程只需保存和恢復極少數的暫存器資料,速度極快 (約幾十奈秒)。
- OS 執行緒 (OS Thread):完完全全由作業系統的排程器 (OS Scheduler) 負責調度。執行緒的切換需要保存大量的狀態(包含硬體暫存器、程式計數器、記憶體分頁表等),且必須切換到作業系統底層(Kernel Mode),速度相對較慢許多 (約幾微秒,相差百倍以上)。
總結
Goroutine 就是站在傳統 OS 執行緒的肩膀上,用更聰明、更輕量的方式來達到高並發 (High Concurrency) 的效果。因為它的成本極低,所以我們在 Go 裡面非常鼓勵**「不要透過共用記憶體來通訊,而是透過通訊 (Channel) 來共用記憶體」**。
以下就是作過的幾個練習
第一個練習goruntine(channel)+select
package main
import (
"log"
"time"
)
func main() {
// 不是在執行緒了的size
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 2
ch <- 3
}()
select {
// 第一次觸發
case v, ok := <-ch:
if ok {
log.Printf("I got %v", v) // 第一次觸發 I got 2 // end
}
}
針對chan取資料最後有完整補充
練習二,多個worker,想像就是多個工人開工,執行速度會比較快
make(chan int )第二個參數是緩衝區,簡單說可以預先設定能放幾比資料進去,不設定就是0,那就會排隊,資料被取走了下一筆資料才能進來,最後有個worker_pool會練習到
package main
import (
"fmt"
"time"
)
func writeIntData(intChan chan int) {
for i := 1; i <= 3; i++ {
fmt.Printf("WriteIntData = %v\n", i)
intChan <- i
}
close(intChan)
}
func writeSecData(secChan chan int) {
for i := 1; i <= 3; i++ {
fmt.Printf("WriteSecData ==> %v\n", i)
secChan <- i
}
close(secChan)
}
func main() {
intChan := make(chan int, 3)
secChan := make(chan int, 10)
go writeIntData(intChan)
go writeSecData(secChan)
//設定逾時
timeout := time.After(5 * time.Second)
Loop:
for {
select {
case v, ok := <-intChan:
if ok {
fmt.Printf("ReadIntData = %v\n", v)
}
case n, ok := <-secChan:
if ok {
fmt.Printf("ReadSecData ==> %v\n", n)
}
case <-timeout:
fmt.Println("timeout")
break Loop
default:
//在這裡可決定是否結束
fmt.Printf("Nothine input\n")
}
}
fmt.Println("End")
}
第三題,worker_pool,多工協作,這裡有三個worker,五個任務,所以假如任務內容相同,應該兩run就可以完成,range jobs就是一種取資料的方式,前提是chan要cloase
一樣來看一下AI怎麼說
- 為何不用
range:如果要用 range results,前提是 results channel 必須被 close() 關閉。但在目前的設計中,是由 3 個不同的 worker 並行寫入 results。如果要關閉它,必須引入 sync.WaitGroup 來等待所有 worker 完成後才進行 close(results)。既然這裡已經知道只收 5 個,用 for 迴圈直接收是最簡潔的做法。 - 為何不用
select:select 通常用於「同時監聽多個 Channel」或者「需要設定 Timeout 機制」。在這裡只有單一一個 results channel 需要讀取,且我們願意阻塞直到拿滿 5 個結果為止,因此不需要使用 select。
特別注意,如果這邊不給緩衝區,五筆資料無法一次塞入,就不會跑到下方開始監聽results ,但這裡設計worker做完任務需要回報results ,沒有人把results收走,worker就無法進行下一輪工作,如此就會產生死鎖!
package main
import (
"fmt"
"time"
)
// worker 負責從 jobs channel 接收任務並將結果放入 results channel
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d 開始處理任務 %d\n", id, j)
time.Sleep(time.Second) // 模擬工作耗時
fmt.Printf("Worker %d 完成任務 %d\n", id, j)
results <- j * 2 // 假設工作結果是將數值乘以 2
}
}
func main() {
start := time.Now() // 記錄開始時間
const numJobs = 5
jobs := make(chan int, numJobs) // 建立一個有緩衝的 Channel ,一次可以塞五個任務,因為worker要把資料塞回results channel,但一直等不到人來接(Line37),就會死鎖
results := make(chan int, numJobs)
// 啟動 3 個 Worker Goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 派發 5 個任務
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 所有任務都已派發,關閉 jobs channel
// 從結果 channel 中收集 5 個結果
for a := 1; a <= numJobs; a++ {
res := <-results
fmt.Printf("收到任務結果: %d\n", res)
}
elapsed := time.Since(start) // 計算總耗時
fmt.Printf("總執行時間: %s\n", elapsed)
}
第四題,AI取名goroutine_pipe,你在不同任務需要多工,但又需要串連,就可以這樣寫
package main
import "fmt"
// 第一階段:產生數字
func generateNumbers(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out) // 發送完畢關閉 channel
}()
return out // 1 -> 2 -> 3 -> 4 -> 5 -> 6
}
// 第二階段:將收到的數字進行平方
func squareNumbers(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out // 1 -> 4 -> 9 -> 16 -> 25 -> 36
}
// 第三階段:過濾偶數
func filterEvenNumbers(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 == 0 {
out <- n
}
}
close(out)
}()
return out // 4 -> 16 -> 36
}
func main() {
// 建立 Pipeline: 產生 -> 平方 -> 過濾偶數
nums := generateNumbers(1, 2, 3, 4, 5, 6)
squares := squareNumbers(nums)
evens := filterEvenNumbers(squares)
// 印出最終結果
for n := range evens {
fmt.Println("Pipeline 輸出結果:", n)
}
}
第五題,典型生產者與消費者,如果上面都看懂了這題就沒啥好講的
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) { // chan<- int 只能寫入,進到channel
for i := 1; i <= 5; i++ {
fmt.Printf("生產者產生資料: %d\n", i)
ch <- i
time.Sleep(500 * time.Millisecond) // 模擬生產時間
}
close(ch) // 生產完畢後關閉 Channel,通知消費者不會再有資料
}
func consumer(ch <-chan int) { // <-chan int 只能讀取,從channel出來
// 使用 for range 不斷接收資料,直到 channel 被關閉
for data := range ch {
fmt.Printf("消費者收到資料: %d\n", data)
}
fmt.Println("消費者完成接收")
}
func main() {
ch := make(chan int) // 建立一個無緩衝的 Channel
// 啟動生產者 Goroutine
go producer(ch)
// 主力 Goroutine 當作消費者
consumer(ch)
}
補充針對chan取資料方式
AI回答,基本上就三種,select是針對多channel做的,所以第一題就是脫褲子放屁,直接取就好
Channel 取資料的三種常見方式
從 Channel 中取資料 (Receive) 基本上有這三種常見寫法,會根據情境而有所不同:
1. 基礎取值 (搭配 ok idiom 判斷是否關閉)
最基本的讀取方式,可以順便檢查 channel 是否已經被關閉且內部沒有資料了。
// ok == false 代表 channel 已經被 close(),且裡面沒東西了
fmt.Println("Channel closed")
- 適用情境:單次讀取,需嚴格確認拿出來的是有效資料,還是因為 Channel 被關閉而拿到的「預設零值」。
2. 結合 for range 迴圈 (最推薦的走訪方式)
Go 語言提供了對 Channel 的 range 語法,它會不斷且阻塞地從 Channel 取值,直到該 Channel 被 close() 關閉為止,迴圈才會自動結束。
// 另一端有人負責送資料,送完後會 close(ch)
// 當 ch 被 close 時,會自動跳出這個迴圈
- 適用情境:資料數量未知,發送方(Sender)承諾在送完所有資料後會呼叫
close()。像你 worker_pool 裡的 for j := range jobs 就是最好的例子。
3. 結合 select 多路復用 (Multiplexing)
當你有「兩個以上」的 Channel 要同時監聽,或者需要實現「非阻塞讀取 (Default)」、「逾時機制 (Timeout)」,就必須使用 select。
fmt.Println("從 ch1 收到:", v1)
fmt.Println("從 ch2 收到:", v2)
case <-time.After(3 * time.Second):
fmt.Println("3 秒內都沒有任何 Channel 送資料過來,超時了!")
// 如果這裡有 default,那上面都沒有資料時,就不會阻塞,立刻執行這裡 (非阻塞讀取)
- 適用情境:需要監聽多個 Channel、需要設定 Timeout、或是需要非阻塞地嘗試讀取時。
總結來說:
- 已知數量:直接用
for i := 0; i < N; i++ { <-ch } - 未知數量,但有人幫忙
close:用 for v := range ch { ... } - 多重 Channel 或防卡死 (Timeout):用
select { case <-ch: ... }