并發
順序執行:按照事先計劃好的順序,執行完一個操作后,再執行下一個操作。
順序執行效率不高的原因:
- 每個操作由多個步驟組成,每個步驟所需要的時間長短不一,有些步驟可能相當耗時。
- 顧客點菜需要時間,后廚做菜也需要時間,可否利用這些時間為更多顧客提供服務呢。
優化目標:減少不必要的閑置和等待,最大化處理機時間,提高工作效率
- 當一個操作執行到某個相當耗時的步驟時,轉而執行其它操作中相對不太耗時的步驟。
- 待這些非耗時步驟完成后,之前那個耗時的步驟也完成了,再繼續回到前一個操作中。
并發執行:沒有固定的執行順序,不等一個操作執行完,即開始下一個操作 。
并發與并行
并發:一個行為主體同時執行多個操作。
并行:多個行為主體同時執行多個操作。
瀏覽器中的并發
啟用瀏覽器開發人員工具,打開任意可訪問頁面,可以看到瀏覽器并不是依次發出每一個請求,而是同時發出很多請求,以盡快渲染頁面的每個組成部分。這樣做的好處是頁面的整體加載速度在用戶看來非常之快。
阻塞與非阻塞
在實際編程中,有些函數的執行速度很快,對于調用者而言幾乎瞬間就返回了,這樣的函數稱為非阻塞函數。
但另一些函數的執行速度則可能非常緩慢,在調用者看來從調用到返回需要經歷非常漫長的等待,甚至可能是永久的等待,這樣的函數稱為阻塞函數。
- 在順序模式中,阻塞的操作會導致其后的操作長期或永遠得不到執行,降低程序的性能。
- 在并發模式中,阻塞的操作會和其它操作分屬于不同的執行過程,快不必等慢,高性能。
// 順序執行
// 在順序模式中,阻塞的操作過程會導致其后的操作永遠或長期得不到執行,降低程序的性能
package main
import ("fmt""time"
)
func proc(ch rune, ms time.Duration) {for { // 死循環,模擬阻塞fmt.Printf("%c", ch)time.Sleep(ms * time.Millisecond)}
}
func main() {proc('-', 100)proc('+', 500)
}
// 打印輸出:
// -------------------------
通過goroutine并發處理
Go語言通過Goroutine處理并發,為了使某個函數在獨立的"線程"中執行,只需在調用該函數的時候使用關鍵字go。
- go proc('-', 100)
將任何阻塞函數放在關鍵字go的后面執行:
- 立即啟動一個獨立的"子線程",并在該"子線程"中執行阻塞函數中的代碼。
- 與此同時"父線程"從go中立即返回,并不等待阻塞函數返回,即"子線程"結束。
- "父線程"在"子線程"執行阻塞函數的同時,執行該語句下面的操作。
- go下面的操作和go后面的函數分別運行在父子兩個獨立"線程"中。
- 阻塞函數執行完畢返回,"子線程"結束。
// 并發執行
// 在并發模式中,阻塞的操作過程運行于獨立的"線程"之中,不會影響其它操作的執行,提高了程序的性能
package main
import ("fmt""time"
)
func proc(ch rune, ms time.Duration) {for {fmt.Printf("%c", ch)time.Sleep(ms * time.Millisecond)}
}
func main() {go proc('-', 100) // 每100ms,打印-proc('+', 500) // 每500ms,打印+
}
// 打印輸出:
// +-----+-----+-----+-----+
Goroutine與線程
Goroutine常被稱作輕量級線程或邏輯線程,它和真正的線程還是有區別的。
線程 | Goroutine | |
調度 開銷 | 線程由操作系統內核調度,每隔幾毫秒,會有一個硬件時鐘中斷發送到CPU,CPU會調用一個調度器內核函數。該函數暫停當前正在運行的線程,把它的寄存器信息保存到內存中,查看線程列表并決定接下來運行哪一個線程,再從內存中恢復此線程的寄存器信息并執行之。這種線程調度需要一個完整的上下文切換,即保存一個線程的狀態到內存,再從內存恢復另一個線程的狀態,同時還要不斷更新調度器的數據結構。某種意義上講,這種操作還是相當耗時的。 | Go語言程序運行時自帶一個調度器,這個調度器使用一個稱為一個M:N的調度技術,即將M個Goroutine調度到N個線程中,Go的調度器不由硬件時鐘定期觸發,而由特定的Go語言結構觸發,也不需要在用戶態和內核態之間來回切換,所以調度一個Goroutine比調度一個線程的開銷要小得多。 |
棧空間 | 每個線程都有一個固定大小的棧內存,通常是2M字節,棧內存用于保存函數的參數、局部變量和返回地址。 | Goroutine的棧內存是動態的,開始只有2K字節,而后隨著程序的運行,再根據實際需要增大或縮小,最大可以到1G字節。 |
線程 標識 | 在大部分支持線程的操作系統中,每個線程都有一個唯一標識,通常是一個整數或者結構體,通過該標識可以為每個線程創建獨立的全局存儲空間,謂之線程局部存儲。 | Goroutine沒有提供可被程序員訪問的唯一標識,它是一種純函數的理念。Go語言認為線程局部存儲的濫用會導致一種不健康的超距作用,即函數的行為不僅取決于它的參數,還與執行它的線程有關。 |