你是否想知道如何應(yīng)對(duì)高并發(fā)?Go語(yǔ)言為你提供了答案!
并發(fā)編程是當(dāng)前軟件領(lǐng)域中不可忽視的一個(gè)關(guān)鍵概念。隨著CPU等硬件的不斷發(fā)展,我們都渴望讓我們的程序運(yùn)行速度更快、更快。而Go語(yǔ)言在語(yǔ)言層面天生支持并發(fā),充分利用現(xiàn)代CPU的多核優(yōu)勢(shì),這也是Go語(yǔ)言能夠廣泛流行的一個(gè)重要原因。
在Java中,要支持高并發(fā)有幾種方案可供選擇。首先,我們可以通過(guò)開(kāi)啟多部署節(jié)點(diǎn)集群來(lái)增加高并發(fā)處理能力,通過(guò)增加機(jī)器硬件來(lái)實(shí)現(xiàn)。其次,我們可以在單節(jié)點(diǎn)上開(kāi)啟多線程來(lái)處理請(qǐng)求。然而,即使在單節(jié)點(diǎn)內(nèi)創(chuàng)建線程也是非常耗費(fèi)資源的。因此,通常情況下我們會(huì)使用線程池來(lái)管理線程的創(chuàng)建和銷毀。然而,有一個(gè)公式你可能會(huì)很熟悉,即核心線程數(shù)等于CPU核數(shù)的一半加一。這意味著我們并不是線程創(chuàng)建得越多,對(duì)于我們的Java程序就越好。
在我們明確了問(wèn)題的痛點(diǎn)之后,我們可以進(jìn)一步探究一下Go語(yǔ)言是如何解決這些問(wèn)題,并且將高并發(fā)作為Go語(yǔ)言的一項(xiàng)特色功能。
goroutine
我們?cè)贘ava中開(kāi)啟線程的方式是直接創(chuàng)建一個(gè)Thread對(duì)象。然而,在Go語(yǔ)言中,如果我們想要實(shí)現(xiàn)異步處理,我們可以使用"go"關(guān)鍵字來(lái)開(kāi)啟一個(gè)goroutine協(xié)程。協(xié)程的最大優(yōu)勢(shì)在于其輕量級(jí),可以輕松創(chuàng)建上百萬(wàn)個(gè)協(xié)程而不會(huì)導(dǎo)致系統(tǒng)資源的耗盡,而線程和進(jìn)程通常最多也不能超過(guò)1萬(wàn)個(gè)。舉個(gè)例子:
go f() // 創(chuàng)建一個(gè)新的 goroutine 運(yùn)行函數(shù)f
在Go語(yǔ)言中,我們可以非常簡(jiǎn)單地使用關(guān)鍵字"go"來(lái)開(kāi)啟一個(gè)協(xié)程,從而實(shí)現(xiàn)異步處理函數(shù)f。只需在函數(shù)f的調(diào)用前面加上"go"關(guān)鍵字,就能使得該函數(shù)在一個(gè)獨(dú)立的協(xié)程中異步執(zhí)行。
不僅可以使用"go"關(guān)鍵字來(lái)開(kāi)啟一個(gè)協(xié)程異步執(zhí)行具名函數(shù),還可以使用"go"關(guān)鍵字來(lái)開(kāi)啟一個(gè)協(xié)程異步執(zhí)行匿名函數(shù)。
go func(){
// ...
}()
今天我們的重點(diǎn)不在這里,而是要討論為什么Go語(yǔ)言適合處理高并發(fā)的情況。我們都知道,操作系統(tǒng)的CPU最小調(diào)度單位是線程,然而Go語(yǔ)言卻使用了協(xié)程的概念。那么問(wèn)題來(lái)了,Go語(yǔ)言是如何將這些協(xié)程交給CPU來(lái)處理的呢?如果無(wú)法將它們交給CPU處理,那么就算再創(chuàng)建多少協(xié)程也無(wú)法運(yùn)行代碼。在這里,我們就需要了解一下Go語(yǔ)言的調(diào)度器,也就是GPM調(diào)度模型。
GPM調(diào)度模型
可以借鑒一下以下圖例,總的來(lái)說(shuō),我們可以像線程池一樣,無(wú)論創(chuàng)建了多少協(xié)程,都需要將它們放入隊(duì)列中。然后,剩下的任務(wù)就交給調(diào)度器來(lái)處理。
圖片
其中:
- G:使用關(guān)鍵字"go"加上一個(gè)函數(shù)調(diào)用可以創(chuàng)建一個(gè)goroutine(簡(jiǎn)稱G)。每次調(diào)用"go f()"都會(huì)創(chuàng)建一個(gè)新的G,其中包含要執(zhí)行的函數(shù)f以及相關(guān)的上下文信息。
- 全局隊(duì)列(Global Queue)是用來(lái)存放等待運(yùn)行的 G(Goroutine)的地方。
- P 是指 goroutine 執(zhí)行所需的物理資源,每個(gè) P 最多可以承載 GOMAXPROCS 個(gè) goroutine 的執(zhí)行。
- P 的本地隊(duì)列是類似于全局隊(duì)列的,它存放了等待運(yùn)行的G,并且數(shù)量限制在256個(gè)以內(nèi)。每當(dāng)新建一個(gè)G時(shí),優(yōu)先將其加入到P的本地隊(duì)列中,如果本地隊(duì)列已滿,則會(huì)批量移動(dòng)部分G到全局隊(duì)列中。
- 為了使線程能夠執(zhí)行任務(wù),需要通過(guò)獲取調(diào)度器(P)來(lái)獲取任務(wù)(G)。線程首先嘗試從調(diào)度器的本地隊(duì)列獲取任務(wù),如果本地隊(duì)列為空,則線程會(huì)嘗試從全局隊(duì)列或其他調(diào)度器的本地隊(duì)列獲取任務(wù)。一旦線程獲取到任務(wù),就會(huì)執(zhí)行任務(wù),并在任務(wù)執(zhí)行完畢后再次從調(diào)度器獲取下一個(gè)任務(wù),持續(xù)重復(fù)這個(gè)過(guò)程。
Goroutine 調(diào)度器和操作系統(tǒng)調(diào)度器通過(guò) M 結(jié)合起來(lái),形成了調(diào)度的基本單位。在這個(gè)結(jié)合中,每個(gè) M 代表一個(gè)內(nèi)核線程,而操作系統(tǒng)調(diào)度器則負(fù)責(zé)將這些內(nèi)核線程分配到 CPU 的核心上進(jìn)行執(zhí)行。
channel
單純地將函數(shù)并發(fā)執(zhí)行是沒(méi)有意義的,因?yàn)楹瘮?shù)與函數(shù)之間需要進(jìn)行數(shù)據(jù)交換,才能真正體現(xiàn)并發(fā)執(zhí)行函數(shù)的意義。
雖然可以利用共享內(nèi)存進(jìn)行數(shù)據(jù)交換,但是在不同的 goroutine 中使用共享內(nèi)存容易導(dǎo)致競(jìng)態(tài)問(wèn)題的出現(xiàn)。為了確保數(shù)據(jù)交換的正確性,許多并發(fā)模型都需要通過(guò)使用互斥量對(duì)內(nèi)存進(jìn)行加鎖來(lái)解決這個(gè)問(wèn)題。然而,這種做法往往會(huì)帶來(lái)性能問(wèn)題,因?yàn)榧渔i操作會(huì)引入額外的開(kāi)銷。
Go語(yǔ)言采用的并發(fā)模型是CSP(Communicating Sequential Processes),這個(gè)模型強(qiáng)調(diào)了通過(guò)通信共享內(nèi)存的方式來(lái)實(shí)現(xiàn)并發(fā),而不是通過(guò)共享內(nèi)存來(lái)實(shí)現(xiàn)通信。這種設(shè)計(jì)理念使得Go語(yǔ)言在處理并發(fā)任務(wù)時(shí)更加高效和安全。
如果說(shuō) goroutine 是Go程序中實(shí)現(xiàn)并發(fā)執(zhí)行的主體,那么channel就是連接這些goroutine之間的紐帶。channel是一種能夠使得一個(gè)goroutine向另一個(gè)goroutine發(fā)送特定值的通信機(jī)制。
Mutex(互斥鎖)在實(shí)現(xiàn)上也是使用了重量級(jí)鎖。與Java的互斥鎖相比,Go語(yǔ)言的Mutex有以下幾點(diǎn)區(qū)別:
內(nèi)存開(kāi)銷:Go語(yǔ)言的Mutex相對(duì)較輕量,使用較少的內(nèi)存。這是因?yàn)镚o語(yǔ)言的Mutex只包含一個(gè)字段,用于表示鎖的狀態(tài),而Java的互斥鎖通常包含更多的字段和數(shù)據(jù)結(jié)構(gòu)。
鎖的語(yǔ)法:在Go語(yǔ)言中,可以使用mutex.Lock()和mutex.Unlock()方法來(lái)手動(dòng)控制鎖的獲取和釋放,這樣可以更靈活地控制鎖的粒度。而在Java中,使用synchronized關(guān)鍵字來(lái)實(shí)現(xiàn)互斥鎖,鎖的粒度相對(duì)固定,只能對(duì)整個(gè)方法或代碼塊進(jìn)行加鎖。
鎖的性能:由于Go語(yǔ)言的Mutex較為輕量,并且采用了更高效的實(shí)現(xiàn)方式,比如以下幾個(gè)方面:
- 自旋鎖:在低并發(fā)的情況下,Go語(yǔ)言的Mutex會(huì)采用自旋鎖的方式。自旋鎖是一種忙等待的鎖,當(dāng)一個(gè)Goroutine嘗試獲取鎖時(shí),如果鎖已經(jīng)被其他Goroutine持有,則該Goroutine會(huì)一直循環(huán)檢查鎖的狀態(tài),直到成功獲取鎖。這種方式避免了線程切換的開(kāi)銷,提高了性能。
- 優(yōu)化的調(diào)度策略:Go語(yǔ)言的調(diào)度器在處理Goroutine的調(diào)度時(shí)會(huì)進(jìn)行優(yōu)化,盡量將鎖的持有者與等待者調(diào)度到同一個(gè)處理器(P)上執(zhí)行,減少線程之間的上下文切換和鎖競(jìng)爭(zhēng)的開(kāi)銷。
- 等待隊(duì)列:當(dāng)一個(gè)Goroutine無(wú)法獲取到Mutex鎖時(shí),它會(huì)進(jìn)入等待隊(duì)列,等待鎖的釋放。Go語(yǔ)言的Mutex的等待隊(duì)列是基于鏈表實(shí)現(xiàn)的,相比Java的互斥鎖使用的等待隊(duì)列,具有更低的內(nèi)存開(kāi)銷和更高的效率。
總結(jié)
并發(fā)編程是當(dāng)前軟件領(lǐng)域中一個(gè)重要的概念。Go語(yǔ)言通過(guò)goroutine和channel的特性,天生支持高并發(fā)處理,充分利用現(xiàn)代CPU的多核優(yōu)勢(shì)。與Java相比,Go語(yǔ)言的協(xié)程更加輕量級(jí),可以輕松創(chuàng)建上百萬(wàn)個(gè)協(xié)程。Go語(yǔ)言的調(diào)度器采用GPM調(diào)度模型,通過(guò)將協(xié)程放入隊(duì)列中,由調(diào)度器分配給CPU處理。此外,Go語(yǔ)言采用CSP模型,通過(guò)channel實(shí)現(xiàn)協(xié)程之間的通信,避免了共享內(nèi)存帶來(lái)的競(jìng)態(tài)問(wèn)題。相比之下,Go語(yǔ)言的Mutex鎖更輕量、靈活,并且具有更高的性能。總的來(lái)說(shuō),Go語(yǔ)言適合處理高并發(fā)的情況,成為了當(dāng)前軟件開(kāi)發(fā)領(lǐng)域的熱門語(yǔ)言之一。