進程、線程、協程
進程:進程是系統進行資源分配的基本單元,有獨立的內存空間
線程:線程是cpu調度和分派的基本單位,線程依附于進程存在,每個線程共享父進程的資源
協程:協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制,協程切換只需要保存任務的上下文,沒有內核的開銷
協程與多線程相比,其優勢體現在:協程的執行效率極高。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
線程上下文切換
由于中斷處理,多任務處理,用戶態切換等等原因會導致CPU從一個線程切換到另一個線程,切換過程中要保存一個線程的狀態切換到另一個線程的狀態。
而這個上下文切換代價極高
為了解決傳統的多線程問題Golang引入了Goroutine
Goroutine非常輕量:
- 上下文切換代價小:Goroutine上下文切換只涉及到三個寄存器(PC/SP/DX)的值的修改;而線程的上下文切換則需要設計模式轉換(從用戶態切換到內核態)、以及16個寄存器、PC、SP等寄存器的刷新
- 內存占用少:線程棧空間通常是2m,而goroutine棧空間最小2k(可最大擴展到1G)
Go并發調度的GMP模型
在操作系統提供的內核線程之上,Go搭建了一個特有的兩級線程模型。goroutine機制實現了M : N的線程模型
,goroutine機制是協程(coroutine)的一種實現,golang內置的調度器,可以讓多核CPU中每個CPU執行一個協程。
Go的調度器是如何工作的
Go語言中支撐整個scheduler實現的主要有4個重要結構,分別是M、G、P、Sched, 前三個定義在runtime.h中,Sched定義在proc.c中。
- Sched結構就是調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
- M結構是Machine,系統線程,它由操作系統管理的,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息。
- P結構是Processor,處理器,它的主要用途就是用來執行goroutine的,它維護了一個goroutine隊列,即runqueue。Processor是讓我們從N:1調度到M:N調度的重要部分。
- G是goroutine實現的核心結構,它包含了棧,指令指針,以及其他對調度goroutine很重要的信息,例如其阻塞的channel。
Processor的數量是在啟動時被設置為環境變量GOMAXPROCS的值,或者通過運行時調用函數GOMAXPROCS()進行設置。Processor數量固定意味著任意時刻只有GOMAXPROCS個線程在運行go代碼。
我們分別用三角形,矩形和圓形表示Machine Processor和Goroutine。
在單核處理器的場景下,所有goroutine運行在同一個M系統線程中,每一個M系統線程維護一個Processor,任何時刻,一個Processor中只有一個goroutine,其他goroutine在runqueue中等待。一個goroutine運行完自己的時間片后,讓出上下文,回到runqueue中。 多核處理器的場景下,為了運行goroutines,每個M系統線程會持有一個Processor。
Go運行時系統中的調度器的主要職責就是將G公平合理的安排到多個M上去執行
在正常情況下,scheduler會按照上面的流程進行調度,但是為了充分利用線程的計算資源,Go調度器采取以下幾種策略
任務竊取
為了提高Go并行處理能力,提高整體效率,當每個p之間的G任務不均衡時,調度器允許從GRQ(全局運行隊列)LRQ(本地運行隊列)中獲取G
減少阻塞
當正在運行的goroutine阻塞的時候,例如進行系統調用,會再創建一個系統線程(M1),當前的M線程放棄了它的Processor,P轉到新的線程中去運行
- 由于原子操作、互斥量、通道操作導致的阻塞,調度器將當前阻塞Goroutine切換出去,重新調度LRQ上的其他Goroutine
- 由于網絡請求阻塞 Go提供了網絡輪詢器(NetPoller)來處理網絡請求和I/O操作的問題,通過NetPoller來進行網絡系統調用,調度器可以防止Goroutine在進行這些操作時阻塞M。可以讓M執行P中的LRQ的其他Goroutine和不需要創建M1,有助于減少操作系統上的負載。