自己學習起來感覺跟之前碰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
針對chan取資料最後有完整補充
練習二,多個worker,想像就是多個工人開工,執行速度會比較快
make(chan int )第二個參數是緩衝區,簡單說可以預先設定能放幾比資料進去,不設定就是0,那就會排隊,資料被取走了下一筆資料才能進來,最後有個worker_pool會練習到
第三題,worker_pool,多工協作,這裡有三個worker,五個任務,所以假如任務內容相同,應該兩run就可以完成,range jobs就是一種取資料的方式,前提是chan要cloase
一樣來看一下AI怎麼說
- 為何不用
range:如果要用range results,前提是resultschannel 必須被close()關閉。但在目前的設計中,是由 3 個不同的 worker 並行寫入results。如果要關閉它,必須引入sync.WaitGroup來等待所有 worker 完成後才進行close(results)。既然這裡已經知道只收 5 個,用 for 迴圈直接收是最簡潔的做法。 - 為何不用
select:select通常用於「同時監聽多個 Channel」或者「需要設定 Timeout 機制」。在這裡只有單一一個resultschannel 需要讀取,且我們願意阻塞直到拿滿 5 個結果為止,因此不需要使用select。
特別注意,如果這邊不給緩衝區,五筆資料無法一次塞入,就不會跑到下方開始監聽results ,但這裡設計worker做完任務需要回報results ,沒有人把results收走,worker就無法進行下一輪工作,如此就會產生死鎖!
第四題,AI取名goroutine_pipe,你在不同任務需要多工,但又需要串連,就可以這樣寫
第五題,典型生產者與消費者,如果上面都看懂了這題就沒啥好講的
補充針對chan取資料方式
AI回答,基本上就三種,select是針對多channel做的,所以第一題就是脫褲子放屁,直接取就好
Channel 取資料的三種常見方式
從 Channel 中取資料 (Receive) 基本上有這三種常見寫法,會根據情境而有所不同:
1. 基礎取值 (搭配 ok idiom 判斷是否關閉)
最基本的讀取方式,可以順便檢查 channel 是否已經被關閉且內部沒有資料了。
gov, ok := <-chif !ok {// ok == false 代表 channel 已經被 close(),且裡面沒東西了fmt.Println("Channel closed")} else {fmt.Println("Got:", v)}
- 適用情境:單次讀取,需嚴格確認拿出來的是有效資料,還是因為 Channel 被關閉而拿到的「預設零值」。
2. 結合 for range 迴圈 (最推薦的走訪方式)
Go 語言提供了對 Channel 的 range 語法,它會不斷且阻塞地從 Channel 取值,直到該 Channel 被 close() 關閉為止,迴圈才會自動結束。
go// 另一端有人負責送資料,送完後會 close(ch)for v := range ch {fmt.Println("Got:", v)}// 當 ch 被 close 時,會自動跳出這個迴圈
- 適用情境:資料數量未知,發送方(Sender)承諾在送完所有資料後會呼叫
close()。像你worker_pool裡的for j := range jobs就是最好的例子。
3. 結合 select 多路復用 (Multiplexing)
當你有「兩個以上」的 Channel 要同時監聽,或者需要實現「非阻塞讀取 (Default)」、「逾時機制 (Timeout)」,就必須使用 select。
goselect {case v1 := <-ch1:fmt.Println("從 ch1 收到:", v1)case v2 := <-ch2:fmt.Println("從 ch2 收到:", v2)case <-time.After(3 * time.Second):fmt.Println("3 秒內都沒有任何 Channel 送資料過來,超時了!")// default:// 如果這裡有 default,那上面都沒有資料時,就不會阻塞,立刻執行這裡 (非阻塞讀取)}
- 適用情境:需要監聽多個 Channel、需要設定 Timeout、或是需要非阻塞地嘗試讀取時。
總結來說:
- 已知數量:直接用
for i := 0; i < N; i++ { <-ch } - 未知數量,但有人幫忙
close:用for v := range ch { ... } - 多重 Channel 或防卡死 (Timeout):用
select { case <-ch: ... }
沒有留言:
張貼留言