在今天的學習中,我們簡要瞭解了Python的控制流程,特別是if-else判斷和迴圈操作。作為有著Java開發經驗的程式員,我們跳過了一些基礎概念,著重探討if判斷和迴圈的靈活運用。Python的縮進寫法和與Java的一些語法區別都是需要註意的地方。在編寫程式時,if嵌套和迴圈是基礎結構,而設計模式... ...
概述
Go
併發模型獨樹一幟,簡潔、高效。Go
語言最小執行單位稱為協程(goroutine
),運行時可以創建成千萬上個協程,這在Java、C等線程模型中是不可想象的,併發模型是Go
的招牌能力之一。很多文章描述協程是輕量級的線程,並不准確,兩者在底層有本質區別。線程是由操作系統維護,以Linux為例,系統調用創建線程,並由操作系統調度執行,在內核空間管理、與進程共用PCB對象、共用堆空間、獨立調用棧和寄存器,是操作系統最小的調度對象,軟中斷觸發操作系統切換調度。協程是由Go
運行時維護,與操作系統線程不是對等關係,多個協程簡共用堆棧空間,在用戶空間維護,由Go
運行時自行調度。不依賴系統中斷可以做了非常輕量級。
調度可簡單理解就是如何安排任務,合理高效的調度任務,可顯著提升性能和降低複雜度。以Linux網路Io模型為例,經過多年的發展也就出現五種模型(阻塞 I/O、非阻塞 I/O、多路復用 I/O、信號驅動 I/O、非同步 I/O)。傳輸層不變、TCP/IP協議棧不變、應用層協議不變、操作系統不變、硬體配置不變,不同Io模型性能差別非常大,這就是調度的威力。操作系統對線程的調度是自閉環的,不提供用戶側的控制介面,並行線程數與CPU數一致,線程切換是很重的操作,沒有優化空間,完全寄托於操作系統進程管理能力。協程運行線上程之上,由go
運行時維護,創建、同步、銷毀、調度等,全部用戶空間完成,可以做到和函數棧調用一樣輕量級。在Go
底層與操作系統交互還是線程模型,從操作系統視角根本看不到協程的存在,並行線程數也沒有改變,複雜度也並沒有降低,只是從用戶側轉移到了Go
運行時,總有人要負重前行。go
併發模型並沒有提升性能,更大作用是降低併發編程難度,降低開發人員心智。
Go
的調度模型有專用名詞:GPM
- G,表示協程,用戶通過
go
指令創建,數量不受限制 - P,類似CPU,內部維護了隊列,G只有加入到P隊列後才能被調度,數量由
Go
自己維護,可通過GOMAXPROCS
指定數量 - M,OS線程抽象,負責調度任務,和某個P綁定,從P的中不斷取出G,切換堆棧並執行,數量不可指定,由Go Runtime調整
基本使用
一如既往的簡潔,使用go
指令就可以絲滑的創建一個協程,新協程將會由go
運行時調度。
func main() {
go func() {
fmt.Println("hello world")
}()
}
註意,上面代碼大概率無法正常工作,不能列印出字元串。匿名函數在新協程中調度執行,main
函數在主協程繼續執行,兩者協程會併發執行。這引出了協程重要特性,go
主協程有特權,當主協程執行完畢就退出程式,不管是否存在用戶協程。main
執行結束退出程式,此時匿名函數還沒來得及列印字元串。Java主線程也有類似的特性,但是開放了daemon
屬性可控制,Go
則沒有提供控制API。
要順利列印字出字元串,主協程需要等待用戶協程執行結束,本質是協程之間協調問題。
func main() {
go func() {
fmt.Println("hello world")
}()
time.Sleep(time.Second) // 主協程睡眠1秒
}
主協程睡眠了1秒,好像很可以工作了,但這是很low的解決方法,極度不靠譜。只要涉及併發編程,就繞不開同步機制,這是併發編程的核心內容,也是併發編程的複雜度所在,獨立章節介紹。
另一個方法可以阻塞主協程,等待通知後繼續執行
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
fmt.Println("hello world")
}()
wg.Wait() // 進入等待
}
程式會鎖死並panic
崩潰退出。主線程進入了等待,卻沒有收到通知,go
運行時可以發現死鎖狀態,類似邏輯在Java中不會退出,將永遠阻塞,因為通知底層依賴操作系統中斷機制,Java編譯器無法識別死鎖問題。而go
在用戶空間調度,由自己處理調度、同步,大部分死鎖問題在編譯時候就可以發現。這也可以看出兩種調度模型的區別,不過JDK20也支持了虛擬線程,與go
協程類似在空間實現調度。
修正後代碼如下
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
fmt.Println("hello world")
wg.Done() // 通知
}()
wg.Wait() // 進入等待
}
使用管道也可以實現通知
func main() {
notice := make(chan bool)
go func() {
fmt.Println("hello")
notice <- true
}()
<-notice // 讀取時堵塞,直到讀取成功
}
當然也可以使用具名函數啟動協程
func working() {
fmt.Println("hello")
}
go working()
方法啟動協程
type Person struct {
Name string
}
func (p *Person) GetName() {
fmt.Println(p.Name)
}
p := &Person{Name: "name"}
go p.GetName() // 啟動協程
在協程執行的函數返回值將被丟棄,無法接收。如果需要返回值,只能使用一些特殊的方法
使用管道接收
func working(resultChannel chan int) {
....
resultChannel<- res; // 將結果寫入管道
}
使用指針接收,調用函數時候傳入指針,把結果寫入指針指向的記憶體,這種叫傳入傳出參數,在C語言中比較常見
func working(data *int) {
....
*data = res // 結果寫入指針指向記憶體
}
與其他語言一樣,Go
沒有提供主動中斷協程的API,大多數使用chan
+select
實現優雅退出,需要小心處理,容易出現協程泄漏問題。另外任何協程中出現panic
,整個程式會崩潰,可根據情況按需捕獲
func working() {
defer func() {
if err := recover(); err != nil { // 捕獲錯誤,程式不會panic
fmt.Println("error:", err)
}
}()
... // 業務邏輯
}
go working() // 啟動協程