Go語言 | CSP併發模型與Goroutine的基本使用

TechFlow2019發表於2020-08-20

今天是golang專題的第13篇文章,我們一起來聊聊golang當中的併發與Goroutine。

在之前的文章當中我們介紹完了golang當中常用的使用方法和規範,在接下來的文章當中和大家聊聊golang的核心競爭力之一,併發模型與Goroutine

我們都知道併發是提升資源利用率最基礎的手段,尤其是當今大資料時代,流量對於一家網際網路企業的重要性不言而喻。串流顯然是不行的,尤其是對於web後端這種流量的直接載體。併發是一定的,問題在於怎麼執行併發。常見的併發方式有三種,分別是多程式、多執行緒和協程。

併發實現模型

多程式

在之前的文章當中我們曾經介紹過,程式是作業系統資源分配的最小單元。所以多程式是在作業系統層面的併發模型,因為所有的程式都是有作業系統的核心管理的。所以每個程式之間是獨立的,每一個程式都會有自己單獨的記憶體空間以及上下文資訊,一個程式掛了不會影響其他程式的執行。這個也是多程式最大的優點,但是它的缺點也很明顯。

最大的缺點就是開銷很大,建立、銷燬程式的開銷是最高的,遠遠高於建立、銷燬執行緒。並且由於程式之間互相獨立,導致程式之間通訊也是一個比較棘手的問題,程式之間共享記憶體也非常不方便。因為這些弊端使得在大多數場景當中使用多程式都不是一個很好的做法。

多執行緒

多執行緒是目前最流行的併發場景的解決方案,由於執行緒更加輕量級,建立和銷燬的成本都很低。並且執行緒之間通訊以及共享記憶體非常方便,和多程式相比開銷要小得多。

但是多執行緒也有缺點,一個缺點也是開銷。雖然執行緒的開銷要比程式小得多,但是如果建立和銷燬頻繁的話仍然是不小的負擔。針對這個問題誕生了執行緒池這種設計。建立一大批執行緒放入執行緒池當中,需要用的時候拿出來使用,用完了再放回,回收和領用代替了建立和銷燬兩個操作,大大提升了效能。另外一個問題是資源的共享,由於執行緒之間資源共享更加頻繁,所以在一些場景當中我們需要加上鎖等設計,避免併發帶來的資料紊亂。以及需要避免死鎖等問題。

協程

也叫做輕量級執行緒,本質上仍然是執行緒。相比於多執行緒和多程式來說,協程要小眾得多,相信很多同學可能都沒有聽說過。和多執行緒最大的區別在於,協程的排程不是基於作業系統的而是基於程式的。

也就是說協程更像是程式裡的函式,但是在執行的過程當中可以隨時掛起、隨時繼續。

我們舉個例子,比如這裡有兩個函式:

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

如果我們在一個執行緒內執行A和B這兩個函式,要麼先執行A再執行B要麼先執行B再執行A。輸出的結果是確定的,但如果我們用寫成來執行A和B,有可能A函式執行了一半剛輸出了一條語句的時候就轉而去執行B,B輸出了一條又再回到A繼續執行。不管執行的過程當中發生了幾次中斷和繼續,在作業系統當中執行的執行緒都沒有發生變化。也就是說這是程式級的排程。

那麼和多執行緒相比,我們建立、銷燬執行緒的開銷就完全沒有了,整個過程變得非常靈活。但是缺點是由於是程式級別的排程,所以需要程式語言自身的支援,如果語言本身不支援,就很難使用了。目前原生就支援協程的語言並不多,顯然golang就是其中一個。

共享記憶體與CSP

我們常見的多執行緒模型一般是通過共享記憶體實現的,但是共享記憶體就會有很多問題。比如資源搶佔的問題、一致性問題等等。為了解決這些問題,我們需要引入多執行緒鎖、原子操作等等限制來保證程式執行結果的正確性。

除了共享記憶體模型之外,還有一個經典模型就是CSP模型。CSP模型其實並不新,發表已經好幾十年了。CSP的英文全稱是Communicating Sequential Processes,翻譯過來的意思是通訊順序程式。CSP描述了併發系統中的互動模式,是一種面向併發的語言的源頭。

Golang只使用了CSP當中關於Process/Channel的部分。簡單來說Process對映Goroutine,Channel對映Channel。Goroutine即Golang當中的協程,Goroutine之間沒有任何耦合,可以完全併發執行。Channel用於給Goroutine傳遞訊息,保持資料同步。雖然Goroutine之間沒有耦合,但是它們與Channel依然存在耦合。

整個Goroutine和Channel的結構有些類似於生產消費者模式,多個執行緒之間通過佇列共享資料,從而保持執行緒之間獨立。這裡不過多深入,我們大概有一個印象即可。

Goroutine

Goroutine即golang當中的協程,這也是golang這門語言的核心精髓所在。正是因為Goroutine,所以golang才叫做golang,所以人們才選擇golang。

相比於Java、Python等多執行緒的複雜的使用體驗而言,golang當中的Goroutine的使用非常簡單,簡單到爆表。只需要一個關鍵字就夠了,那就是go。所以你們應該明白為什麼golang叫做Go語言不叫別的名字了吧?

比如我們有一個函式:

func Add(x, y int) int{
    z := x + y
    fmt.Println(z)
}

我們希望啟動一個goroutine去執行它, 應該怎麼辦?很簡單,只需要一行程式碼:

go Add(34)

我們還可以用go關鍵字來使用goroutine來執行一個匿名函式:

go func(x, y int) {
    fmt.Println(x + y)
}(34)

需要注意的是,當我們使用go關鍵字的時候,是不能獲取返回值的。也就是說z := go Add(3, 4)是違法的。乍看起來似乎不合理,但是道理其實是很簡單的。如果我們希望一個變數承接一個函式的返回值,說明這裡的邏輯是序列的,那麼我們使用goroutine的意義是什麼?所以這裡看似不合理,其實是設計者下了心思的。

總結

關於併發模型與goroutine的基本原理就介紹到這裡了,goroutine的使用方法我們已經瞭解了,但是還有很多問題沒有解決。比如多個goroutine之間怎麼通訊,我們如何知道goroutine的執行狀態,當我們建立多個goroutine的時候,我們怎麼知道goroutine都結束沒有?

關於這些問題,我們將會在之後的文章當中給大家分享,敬請期待。

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

- END -

{{uploading-image-935546.png(uploading...)}}

相關文章