iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0
Modern Web

就是個Go,我也可以啦!GOGO系列 第 18

2023鐵人賽Day 18 Go 探索併發的魅力: 為何我們需要它以及如何入門

  • 分享至 

  • xImage
  •  

當我學javascript的時候,我非常喜歡一個觀念,那就是非同步,我覺得這個觀念非常的方便,他可以讓使用者可以繼續互動,而不會因為某個操作而感到網頁"凍結",更可以更容易地進行錯誤處理

生為現在多核時代,我們大多希望透過多線程的方式來處理程式,已達到高效率...疑,先等等,有些觀念我們可能要說的清楚一點,像是...

  • 在單線程的程式語言可以做到多線程嗎
  • 非同步、同步、併發、併行有什麼差別

釐清觀念

非同步、同步、併發、併行

  • 同步 (Synchronous):
    • 當一個操作在另一個操作之前完成時,我們稱之為同步
    • 如果一個操作(例如,讀取文件或查詢數據庫)需要一段時間,整個程序將被阻塞,直到該操作完成
  • 非同步 (Asynchronous)
    • 代碼將繼續運行,而不等待非同步操作完成,當該操作完成時,通常會通過回調、promises 或其他機制來通知
    • 這在處理 I/O 操作、網絡請求等可能需要一段時間的操作時特別有用,因為它允許程序在等待這些操作完成時繼續運行

https://ithelp.ithome.com.tw/upload/images/20231003/20150980rkbom7cgmx.png

  • 併發 (Concurrency)

    • 併發是指多個任務在同一時間段內啟動、執行和完成,但在任何給定的時刻,只有一個任務在執行。
    • 併發可以在單核心(single-core)或多核心(multi-core)的系統上發生。
    • 在單核心系統上,併發是通過時間片切換來模擬多任務同時運行的。系統會迅速地在不同的任務之間切換,給人一種它們似乎是同時運行的錯覺
  • 並行 (Parallelism)

    • 並行是指多個任務或多個計算步驟在同一時刻真正同時執行。
    • 並行通常在多核心或多處理器的系統上實現,其中每個核心或處理器同時執行不同的任務。

所以,併發可以在單核或多核系統上發生,而並行則特指在多核系統上的真正同時執行的任務。

https://ithelp.ithome.com.tw/upload/images/20231003/201509802cXu3qY09h.png

Go - 原生併發

在傳統的程式語言C++和Java最初被設計出來時,它們主要是為了解決一般的編程問題,而不是專門為併發編程設計的,雖然C++和Java都可以使用多線程(即同時執行多個任務),但它們的多線程功能主要是依賴於操作系統提供的,當你在C++或Java中創建一個新的線程時,實際上是操作系統在背後幫你創建和管理這個線程。這意味著線程的行為和性能受到操作系統的影響

Go 語言的設計哲學之一就是原生併發輕量高效, 並且在處理併發時,並不直接依賴於操作系統提供的線程,因為Go引入了 Goroutines 作為其併發模型的核心部分

那Goroutines跟操作系統的線程 是什麼關係呢
下面我們用服務生跟廚師來比喻

  1. 服務生 (Goroutines)

    • 每個服務生(Goroutine)可以迅速地接受顧客的需求(任務)
    • 服務生不直接製作食物,而是將顧客的需求整理成訂單
  2. 廚師 (OS Threads)

    • 廚師(OS Thread)是真正製作食物的人。他們根據服務生給的訂單來製作食物
    • Go 的調度器會確保每個廚師(OS Thread)都有足夠的訂單(任務)來製作,這意味著一個廚師可能同時處理多個訂單,這就是併發的概念
  3. 併發

    • 在這個情境中,併發是指多個訂單(任務)可以在同一時間段內被接受、整理和製作
    • 儘管在任何給定的時刻,只有一個訂單正在被一個廚師製作,但由於廚師可以迅速地切換到另一個訂單,所以給人的感覺就像多個訂單同時在製作

這種方式允許 Go 語言有效地管理大量的 Goroutines,並確保它們都能被 OS Threads 正確地處理,從而實現高效的併發處理

接下來我們再來看這操作系統的線程及Goroutines的比較

  1. 操作系統的線程 (OS Threads)

    • 在許多傳統的程式語言中,當你想要同時執行多個任務時,你會創建多個操作系統的線程
    • 這些線程是由操作系統管理和調度的,並且每個線程都有其自己的資源和上下文。因此,創建、管理和切換這些線程有一定的開銷
  2. Go 的 Goroutines

    • Go 語言引入了 Goroutines 作為其併發模型的核心部分
    • Goroutines 是非常輕量級的,比操作系統的線程更小、更快。你可以輕鬆地創建成千上萬的 Goroutines
  3. Go 的調度器

    • Go 語言有自己的內部調度器,它決定哪些 Goroutines 應該在哪些 OS Threads 上運行
    • 這意味著多個 Goroutines 可能會在同一個 OS Thread 上順序執行,而 Go 的調度器會確保這一切運行得很順暢

以下是 Goroutines及OS Threads的關係圖
https://ithelp.ithome.com.tw/upload/images/20231003/20150980FRgX7zrnqU.png
如果是java呢
https://ithelp.ithome.com.tw/upload/images/20231003/20150980V1NYbaIPay.png
從上面的對照表可以明顯地感受到,Go就是一個面向併發而生的語言了,因此在應用結構的設計上 GO的慣例就是優先考慮併發設計

來一個例子

你在一家餐廳,這家餐廳有一個(非併發)或數個(併發)服務生和一個廚師

package main

import (
	"fmt"
	"sync"
	"time"
)

// 廚師的功能
func chef(order int) {
	fmt.Printf("廚師開始製作訂單 #%d\n", order)
	time.Sleep(2 * time.Second) // 廚師製作食物需要2秒
	fmt.Printf("廚師完成訂單 #%d\n", order)
}

// 服務生的功能
func waiter(order int, wg *sync.WaitGroup) {
	fmt.Printf("服務生接收到訂單 #%d\n", order)
	chef(order) // 服務生將訂單交給廚師
	if wg != nil {
		wg.Done()
	}
}

如果是併發版本的話

func main() {
	var wg sync.WaitGroup

	// 三位顧客同時下訂單
	wg.Add(3)
	go waiter(1, &wg)
	go waiter(2, &wg)
	go waiter(3, &wg)

	wg.Wait()
	fmt.Println("所有訂單都已完成!")
}

執行後,輸出為

❯ go run main.go
服務生接收到訂單 #1
廚師開始製作訂單 #1
服務生接收到訂單 #3
廚師開始製作訂單 #3
服務生接收到訂單 #2
廚師開始製作訂單 #2
廚師完成訂單 #2
廚師完成訂單 #1
廚師完成訂單 #3
所有訂單都已完成!

非併發版本:

func main() {
	// 三位顧客依次下訂單
	waiter(1, nil)
	waiter(2, nil)
	waiter(3, nil)

	fmt.Println("所有訂單都已完成!")
}

執行後,輸出為

❯ go run main.go
服務生接收到訂單 #1
廚師開始製作訂單 #1
廚師完成訂單 #1
服務生接收到訂單 #2
廚師開始製作訂單 #2
廚師完成訂單 #2
服務生接收到訂單 #3
廚師開始製作訂單 #3
廚師完成訂單 #3
所有訂單都已完成!

不知道你有沒有感受到併發的威力,希望以上的解說可以讓你能理解


上一篇
2023鐵人賽Day 17 Go 方法集合決定介面實現
下一篇
2023鐵人賽Day 19 面向併發,Go 語言借鏡 CSP 模型的一場大師級的併發設計之旅
系列文
就是個Go,我也可以啦!GOGO30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言