1. 程式人生 > >Golang併發原理及GPM排程策略(一)

Golang併發原理及GPM排程策略(一)

其實從一開始瞭解到go的goroutine概念就應該想到,其實go應該就是在核心級執行緒的基礎上做了一層邏輯上的虛擬執行緒(使用者級執行緒)+ 執行緒排程系統,如此分析以後,goroutine也就不再那麼神祕了。

併發≠並行

假如我們有一段CPU密集型任務,我們建立2000個gorountine是否真的可以將其效能提高2000倍,答案必然是不能,因為我們只是進行了2000次的併發(concurrency),而並沒有真正做到並行(parallelism)。

併發其實所指的是我們的程式執行邏輯,傳統單執行緒應用的程式邏輯是順序執行的,在任何時刻,程式只能處理同一個邏輯,而併發指的是,我們同時執行多個獨立的程式邏輯,若干個程式邏輯在執行時可以是同時進行的(但並不代表同時進行處理)。實際上,不論我們併發多少個程式邏輯,若我們僅僅將其執行在一個單核單執行緒的CPU上,都不能讓你的程式在效能上有所提升,因為最終所有任務都排隊等待CPU資源(時間片)。

而並行才能讓我們的程式真正的同時處理多個任務,但並行並不是程式語言能夠帶我們的特性,他需要硬體支援。上面說到單核CPU所有資源都要等待同一個CPU的資源,那麼其實我們只要將CPU增多就能真正的讓我們實現並行。我們可以使用多核CPU或用多臺伺服器組成服務叢集,均可實現真正的並行,能夠並行處理的任務數量也就是我們的CPU數量。

引用Rob Pikie大神在PPT《Concurrency is not Parallelism》中的一段總結,大意就是併發不同於並行,但併發也許可以讓程式實現並行化。

Concurrency vs. parallelism
Concurrency is about dealing with lots of things at once.

Parallelism is about doing lots of things at once.

Not the same, but related.

Concurrency is about structure, parallelism is about execution.

Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.

CPU密集&I/O密集

那麼是不是說我們在單核CPU的機器上使用程式語言所提供的多執行緒就沒有意義了呢?

如果說我們的程式屬於CPU密集型,使用併發程式設計,可能確實無法提升我們的程式效能,甚至可能因為大量計算資源花在了建立執行緒本身,導致程式效能進一步下降。

但不同的是,如果說我們的程式屬於IO密集型,當你在進行程式壓測的過程中可能發現CPU佔用率很低,但效能卻到了瓶頸,原因是程式將大量的時間花在了等待IO的過程中,如果我們可以在等待IO的時候繼續執行其他的程式邏輯即可提高CPU利用率,從而提高我們的程式效能,這時併發程式設計的好處就出來了,例如Python因為GIL的存在實際上並不能實現真正的並行,但他的多執行緒依舊在IO密集型的程式中依舊有種很重要的意義。

Goroutine(Golang Coroutine)

上面說到了使用多核CPU實現並行處理,使應用在多核cpu實現並行處理的方案主要是多程序與多執行緒兩種方式,多程序模型相對簡單,但是有著資源開銷大及程序間通訊成本高的問題。多執行緒模型相對複雜,會有死鎖,執行緒安全,模型複雜等問題,但卻因為資源開銷及易於管理等優點適用於對於效能要求較高的應用。

Golang採用的是多執行緒模型,更詳細的說他是一個兩級執行緒模型,但它對系統執行緒(核心級執行緒)進行了封裝,暴露了一個輕量級的協程goroutine(使用者級執行緒)供使用者使用,而使用者級執行緒到核心級執行緒的排程由golang的runtime負責,排程邏輯對外透明。

goroutine的優勢在於上下文切換在完全使用者態進行,無需像執行緒一樣頻繁在使用者態與核心態之間切換,節約了資源消耗。

同時,啟動一個gorountine非常簡單,而且寫法很cool~

go function()

僅僅需要在呼叫函式時在前面加上關鍵字go即可建立一個goroutine並建立其上下文物件。

G·P·M

G(Goroutine) :我們所說的協程,為使用者級的輕量級執行緒,每個Goroutine物件中的sched儲存著其上下文資訊

M(Machine) :對核心級執行緒的封裝,數量對應真實的CPU數(真正幹活的物件)

P(Processor) :即為G和M的排程物件,用來排程G和M之間的關聯關係,其數量可通過GOMAXPROCS()來設定,預設為核心數

每個Processor物件都擁有一個LRQ(Local Run Queue),未分配的Goroutine物件儲存在GRQ(Global Run Queue )中,等待分配給某一個P的LRQ中,每個LRQ裡面包含若干個使用者建立的Goroutine物件,同時Processor作為橋樑對Machine和Goroutine進行了解耦,也就是說Goroutine如果想要使用Machine需要繫結一個Processor才行,上圖中共有兩個M和兩個P也就是說我們可以同時並行處理兩個goroutine。

這一篇主要講解了一些並行與併發的區別Golang併發模型的優勢,下一篇會詳細說明GPM模型的排程策略。