怎么從MPG線程模型理解Go語言的并發(fā)程序

今天就跟大家聊聊有關(guān)怎么從MPG線程模型理解Go語言的并發(fā)程序,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供怒江州網(wǎng)站建設(shè)、怒江州做網(wǎng)站、怒江州網(wǎng)站設(shè)計、怒江州網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、怒江州企業(yè)網(wǎng)站模板建站服務(wù),十余年怒江州做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。

我們都知道計算機的核心為 CPU,它是計算機的運算和控制核心,承載了所有的計算任務(wù)。最近半個世紀以來,由于半導(dǎo)體技術(shù)的高速發(fā)展,集成電路中晶體管的數(shù)量也在大幅度增長,這大大提升了 CPU 的性能。著名的摩爾定律——“集成電路芯片上所集成的電路的數(shù)目,每隔18個月就翻一番”,描述的就是該種情形。

過于密集的晶體管雖然提高了 CPU 的處理性能,但也帶來了單個芯片發(fā)熱過高和成本過高的問題,與此同時,受限于材料技術(shù)的發(fā)展,芯片中晶體管數(shù)量密度的增加速度已經(jīng)放緩。也就是說,程序已經(jīng)無法簡單地依賴硬件的提升而提升運行速度。這時,多核 CPU 的出現(xiàn)讓我們看到了提升程序運行速度的另一個方向:將程序的執(zhí)行過程分為多個可并行或并發(fā)執(zhí)行的步驟,讓它們分別在不同的 CPU 核心中同時執(zhí)行,最后將各部分的執(zhí)行結(jié)果進行合并得到最終結(jié)果。

并行和并發(fā)是計算機程序執(zhí)行的常見概念,它們的區(qū)別在于:

  • 并行,指兩個或多個程序在同一個時刻執(zhí)行;

  • 并發(fā),指兩個或多個程序在同一個時間段內(nèi)執(zhí)行。

并行執(zhí)行的程序,無論從宏觀還是微觀的角度觀察,同一時刻內(nèi)都有多個程序在 CPU 中執(zhí)行。這就要求 CPU 提供多核計算能力,多個程序被分配到 CPU 的不同的核中被同時執(zhí)行。

并發(fā)執(zhí)行的程序,僅需要在宏觀角度觀察到多個程序在 CPU 中同時執(zhí)行。即使是單核 CPU 也可以通過分時復(fù)用的方式,給多個程序分配一定的執(zhí)行時間片,讓它們在 CPU 上被快速輪換執(zhí)行,從而在宏觀上模擬出多個程序同時執(zhí)行的效果。但從微觀角度來看,這些程序其實是在 CPU 中被串行執(zhí)行。

Go 的 MPG 線程模型

Go 被認為是一門高性能并發(fā)語言,得益于它在原生態(tài)支持協(xié)程并發(fā)。這里我們首先了解進程、線程和協(xié)程這三者的聯(lián)系和區(qū)別。

在多道程序系統(tǒng)中,進程是一個具有獨立功能的程序關(guān)于某個數(shù)據(jù)集合的一次動態(tài)執(zhí)行過程,是操作系統(tǒng)進行資源分配和調(diào)度的基本單位,是應(yīng)用程序運行的載體。

線程則是程序執(zhí)行過程中一個單一的順序控制流程,是 CPU 調(diào)度和分派的基本單位。線程是比進程更小的獨立運行基本單位,一個進程中可以擁有一個或者以上的線程,這些線程共享進程所持有的資源,在 CPU 中被調(diào)度執(zhí)行,共同完成進程的執(zhí)行任務(wù)。

在 Linux 系統(tǒng)中,根據(jù)資源訪問權(quán)限的不同,操作系統(tǒng)會把內(nèi)存空間分為內(nèi)核空間和用戶空間:內(nèi)核空間的代碼能夠直接訪問計算機的底層資源,如 CPU 資源、I/O 資源等,為用戶空間的代碼提供計算機底層資源訪問能力;用戶空間為上層應(yīng)用程序的活動空間,無法直接訪問計算機底層資源,需要借助“系統(tǒng)調(diào)用”“庫函數(shù)”等方式調(diào)用內(nèi)核空間提供的資源。

同樣,線程也可以分為內(nèi)核線程和用戶線程。內(nèi)核線程由操作系統(tǒng)管理和調(diào)度,是內(nèi)核調(diào)度實體,它能夠直接操作計算機底層資源,可以充分利用 CPU 多核并行計算的優(yōu)勢,但是線程切換時需要 CPU 切換到內(nèi)核態(tài),存在一定的開銷,可創(chuàng)建的線程數(shù)量也受到操作系統(tǒng)的限制。用戶線程由用戶空間的代碼創(chuàng)建、管理和調(diào)度,無法被操作系統(tǒng)感知。用戶線程的數(shù)據(jù)保存在用戶空間中,切換時無須切換到內(nèi)核態(tài),切換開銷小且高效,可創(chuàng)建的線程數(shù)量理論上只與內(nèi)存大小相關(guān)。

協(xié)程是一種用戶線程,屬于輕量級線程。協(xié)程的調(diào)度,完全由用戶空間的代碼控制;協(xié)程擁有自己的寄存器上下文和棧,并存儲在用戶空間;協(xié)程切換時無須切換到內(nèi)核態(tài)訪問內(nèi)核空間,切換速度極快。但這也給開發(fā)人員帶來較大的技術(shù)挑戰(zhàn):開發(fā)人員需要在用戶空間處理協(xié)程切換時上下文信息的保存和恢復(fù)、??臻g大小的管理等問題。

Go 是為數(shù)不多在語言層次實現(xiàn)協(xié)程并發(fā)的語言,它采用了一種特殊的兩級線程模型:MPG 線程模型(如下圖)。

怎么從MPG線程模型理解Go語言的并發(fā)程序

MPG 線程模型

  • M,即 machine,相當于內(nèi)核線程在 Go 進程中的映射,它與內(nèi)核線程一一對應(yīng),代表真正執(zhí)行計算的資源。在 M 的生命周期內(nèi),它只會與一個內(nèi)核線程關(guān)聯(lián)。

  • P,即 processor,代表 Go 代碼片段執(zhí)行所需的上下文環(huán)境。M 和 P 的結(jié)合能夠為 G 提供有效的運行環(huán)境,它們之間的結(jié)合關(guān)系不是固定的。P 的最大數(shù)量決定了 Go 程序的并發(fā)規(guī)模,由 runtime.GOMAXPROCS 變量決定。

  • G,即 goroutine,是一種輕量級的用戶線程,是對代碼片段的封裝,擁有執(zhí)行時的棧、狀態(tài)和代碼片段等信息。

在實際執(zhí)行過程中,M 和 P 共同為 G 提供有效的運行環(huán)境(如下圖),多個可執(zhí)行的 G 順序掛載在 P 的可執(zhí)行 G 隊列下面,等待調(diào)度和執(zhí)行。當 G 中存在一些 I/O 系統(tǒng)調(diào)用阻塞了 M時,P 將會斷開與 M 的聯(lián)系,從調(diào)度器空閑 M 隊列中獲取一個 M 或者創(chuàng)建一個新的 M 組合執(zhí)行, 保證 P 中可執(zhí)行 G 隊列中其他 G 得到執(zhí)行,且由于程序中并行執(zhí)行的 M 數(shù)量沒變,保證了程序 CPU 的高利用率。

怎么從MPG線程模型理解Go語言的并發(fā)程序

M 和 P 結(jié)合示意圖

當 G 中系統(tǒng)調(diào)用執(zhí)行結(jié)束返回時,M 會為 G 捕獲一個 P 上下文,如果捕獲失敗,就把 G 放到全局可執(zhí)行 G 隊列等待其他 P 的獲取。新創(chuàng)建的 G 會被放置到全局可執(zhí)行 G 隊列中,等待調(diào)度器分發(fā)到合適的 P 的可執(zhí)行 G 隊列中。M 和 P 結(jié)合后,會從 P 的可執(zhí)行 G 隊列中無鎖獲取 G 執(zhí)行。當 P 的可執(zhí)行 G 隊列為空時,P 才會加鎖從全局可執(zhí)行 G 隊列獲取 G。當全局可執(zhí)行 G 隊列中也沒有 G 時,P 會嘗試從其他 P 的可執(zhí)行 G 隊列中“剽竊”G 執(zhí)行。

goroutine 和 channel

并發(fā)程序中的多個線程同時在 CPU 執(zhí)行,由于資源之間的相互依賴和競態(tài)條件,需要一定的并發(fā)模型協(xié)作不同線程之間的任務(wù)執(zhí)行。Go 中倡導(dǎo)使用CSP 并發(fā)模型來控制線程之間的任務(wù)協(xié)作,CSP 倡導(dǎo)使用通信的方式來進行線程之間的內(nèi)存共享。

Go是通過 goroutine 和 channel 來實現(xiàn) CSP 并發(fā)模型的:

  • goroutine,即協(xié)程,Go 中的并發(fā)實體,是一種輕量級的用戶線程,是消息的發(fā)送和接收方;

  • channel,即通道, goroutine 使用通道發(fā)送和接收消息。

CSP并發(fā)模型類似常用的同步隊列,它更加關(guān)注消息的傳輸方式,解耦了發(fā)送消息的 goroutine 和接收消息的 goroutine,channel 可以獨立創(chuàng)建和存取,在不同的 goroutine 中傳遞使用。

使用關(guān)鍵字 go 即可使用 goroutine 并發(fā)執(zhí)行代碼片段,形式如下:

go expression

而 channel 作為一種引用類型,聲明時需要指定傳輸數(shù)據(jù)類型,聲明形式如下:

var name chan T // 雙向 channel
var name chan <- T // 只能發(fā)送消息的 channel
var name T <- chan // 只能接收消息的 channel

其中,T 即為 channel 可傳輸?shù)臄?shù)據(jù)類型。channel 作為隊列,遵循消息先進先出的順序,同時保證同一時刻只能有一個 goroutine 發(fā)送或者接收消息。
使用 channel 發(fā)送和接收消息形式如下:

channel <- val // 發(fā)送消息
val := <- channel // 接收消息
val, ok := <- channel // 非阻塞接收消息

goroutine 向已經(jīng)填滿信息的 channel 發(fā)送信息或從沒有數(shù)據(jù)的 channel 接收信息會阻塞自身。goroutine 接收消息時可以使用非阻塞的方式,無論 channel 中是否存在消息都會立即返回,通過 ok 布爾值判斷是否接收成功。

創(chuàng)建一個 channel 需要使用 make 函數(shù)對 channel 進行初始化,形式如下所示:

ch := make(chan T, sizeOfChan)

初始化 channel 時可以指定 channel 的長度,表示 channel 最多可以緩存多少條信息。下面我們通過一個簡單例子演示 goroutine 和 channel 的使用:

package main
import (
"fmt"
"time"
)
//生產(chǎn)者
func Producer(begin, end int, queue chan<- int) {
for i:= begin ; i < end ; i++ {
fmt.Println("produce:", i)
queue <- i
}
}
//消費者
func Consumer(queue <-chan int) {
for val := range queue  { //當前的消費者循環(huán)消費
fmt.Println("consume:", val)
}
}
func main() {
queue := make(chan int)
defer close(queue)
for i := 0; i < 3; i++ {
go Producer(i * 5, (i+1) * 5, queue) //多個生產(chǎn)者
}
go Consumer(queue) //單個消費者
time.Sleep(time.Second) // 避免主 goroutine 結(jié)束程序
}

這是一個簡單的多生產(chǎn)者和單消費的代碼例子,生產(chǎn) goroutine 將生產(chǎn)的數(shù)字通過 channel 發(fā)送給消費 goroutine。上述例子中,消費 goroutine 使用 for:range 從 channel 中循環(huán)接收消息,只有當相應(yīng)的 channel 被內(nèi)置函數(shù) close 后,該循環(huán)才會結(jié)束。channel 在關(guān)閉之后不可以再用于發(fā)送消息,但是可以繼續(xù)用于接收消息,從關(guān)閉的 channel 中接收消息或者正在被阻塞的 goroutine 將會接收零值并返回。還有一個需要注意的點是,main 函數(shù)由主 goroutine 啟動,當主 goroutine 即 main 函數(shù)執(zhí)行結(jié)束,整個 Go 程序也會直接執(zhí)行結(jié)束,無論是否存在其他未執(zhí)行完的 goroutine。

  • select 多路復(fù)用

當需要從多個 channel 中接收消息時,可以使用 Go 提供的 select 關(guān)鍵字,它提供類似多路復(fù)用的能力,使得 goroutine 可以同時等待多個 channel 的讀寫操作。select 的形式與 switch 類似,但是要求 case 語句后面必須為 channel 的收發(fā)操作,一個簡單的例子如下:

package main
import (
"fmt"
"time"
)
func send(ch chan int, begin int )  {
// 循環(huán)向 channel 發(fā)送消息
for i :=begin ; i< begin + 10 ;i++{
ch <- i
}
}
func receive(ch <-chan int)  {
val := <- ch
fmt.Println("receive:", val)
}
func main()  {
ch2 := make(chan int)
ch3 := make(chan int)
go send(ch2, 0)
go receive(ch3)
// 主 goroutine 休眠 1s,保證調(diào)度成功
time.Sleep(time.Second)
for {
select {
case val := <- ch2: // 從 ch2 讀取數(shù)據(jù)
fmt.Printf("get value %d from ch2\n", val)
case ch3 <- 2 : // 使用 ch3 發(fā)送消息
fmt.Println("send value by ch3")
case <-time.After(2 * time.Second): // 超時設(shè)置
fmt.Println("Time out")
return
}
}
}

在上述例子中,我們使用 select 關(guān)鍵字同時從 ch2 中接收數(shù)據(jù)和使用 ch3 發(fā)送數(shù)據(jù),輸出的一種可能結(jié)果為:

get value 0 from ch2
get value 1 from ch2
send value by ch3
receive: 2
get value 2 from ch2
get value 3 from ch2
get value 4 from ch2
get value 5 from ch2
get value 6 from ch2
get value 7 from ch2
get value 8 from ch2
get value 9 from ch2
Time out

由于 ch3 中的消息僅被接收一次,所以僅出現(xiàn)一次“send value by ch3”,后續(xù)消息的發(fā)送將被阻塞。select 語句分別從 3 個 case 中選取返回的 case 進行處理,當有多個 case 語句同時返回時,select 將會隨機選擇一個 case 進行處理。如果 select 語句的最后包含 default 語句,該 select 語句將會變?yōu)榉亲枞停串斊渌械?case 語句都被阻塞無法返回時,select 語句將直接執(zhí)行 default 語句返回結(jié)果。在上述例子中,我們在最后的 case 語句使用了 <-time.After(2 * time.Second) 的方式指定了定時返回的 channel,這是一種有效從阻塞的 channel 中超時返回的小技巧。

  • Context 上下文

當需要在多個 goroutine 中傳遞上下文信息時,可以使用 Context 實現(xiàn)。Context 除了用來傳遞上下文信息,還可以用于傳遞終結(jié)執(zhí)行子任務(wù)的相關(guān)信號,中止多個執(zhí)行子任務(wù)的 goroutine。Context 中提供以下接口:

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • Deadline 方法,返回 Context 被取消的時間,也就是完成工作的截止日期;

  • Done,返回一個 channel,這個channel 會在當前工作完成或者上下文被取消之后關(guān)閉,多次調(diào)用 Done 方法會返回同一個 channel;

  • Err 方法,返回 Context 結(jié)束的原因,它只會在 Done 返回的 channel 被關(guān)閉時才會返回非空的值,如果 Context 被取消,會返回 Canceled 錯誤;如果 Context 超時,會返回 DeadlineExceeded 錯誤。

  • Value 方法,可用于從 Context 中獲取傳遞的鍵值信息。

在 Web 請求的處理過程中,一個請求可能啟動多個 goroutine 協(xié)同工作,這些 goroutine 之間可能需要共享請求的信息,且當請求被取消或者執(zhí)行超時時,該請求對應(yīng)的所有 goroutine 都需要快速結(jié)束,釋放資源。Context 就是為了解決上述場景而開發(fā)的,我們通過下面一個例子來演示:

package main
import (
"context"
"fmt"
"time"
)
const DB_ADDRESS  = "db_address"
const CALCULATE_VALUE  = "calculate_value"
func readDB(ctx context.Context, cost time.Duration)  {
fmt.Println("db address is", ctx.Value(DB_ADDRESS))
select {
case <- time.After(cost): //  模擬數(shù)據(jù)庫讀取
fmt.Println("read data from db")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任務(wù)取消的原因
// 一些清理工作
}
}
func calculate(ctx context.Context, cost time.Duration)  {
fmt.Println("calculate value is", ctx.Value(CALCULATE_VALUE))
select {
case <- time.After(cost): //  模擬數(shù)據(jù)計算
fmt.Println("calculate finish")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任務(wù)取消的原因
// 一些清理工作
}
}
func main()  {
ctx := context.Background(); // 創(chuàng)建一個空的上下文
// 添加上下文信息
ctx = context.WithValue(ctx, DB_ADDRESS, "localhost:10086")
ctx = context.WithValue(ctx, CALCULATE_VALUE, 1234)
// 設(shè)定子 Context 2s 后執(zhí)行超時返回
ctx, cancel := context.WithTimeout(ctx, time.Second * 2)
defer cancel()
// 設(shè)定執(zhí)行時間為 4 s
go readDB(ctx, time.Second * 4)
go calculate(ctx, time.Second * 4)

// 充分執(zhí)行
time.Sleep(time.Second * 5)
}

在上述例子中,我們模擬了一個請求中同時進行數(shù)據(jù)庫訪問和邏輯計算的操作,在請求執(zhí)行超時時,及時關(guān)閉尚未執(zhí)行結(jié)束 goroutine。我們首先通過 context.WithValue 方法為 context 添加上下文信息,Context 在多個 goroutine 中是并發(fā)安全的,可以安全地在多個 goroutine 中對 Context 中的上下文數(shù)據(jù)進行讀取。接著使用 context.WithTimeout 方法設(shè)定了 Context 的超時時間為 2s,并傳遞給 readDB 和 calculate 兩個 goroutine 執(zhí)行子任務(wù)。在 readDB 和 calculate 方法中,使用 select 語句對 Context 的 Done 通道進行監(jiān)控。

由于我們設(shè)定了子 Context 將在 2s 之后超時,所以它將在 2s 之后關(guān)閉 Done 通道;然而預(yù)設(shè)的子任務(wù)執(zhí)行時間為 4s,對應(yīng)的 case 語句尚未返回,執(zhí)行被取消,進入到清理工作的 case 語句中,結(jié)束掉當前的 goroutine 所執(zhí)行的任務(wù)。預(yù)期的輸出結(jié)果如下:

calculate value is 1234
db address is localhost:10086
context deadline exceeded
context deadline exceeded

使用 Context,能夠有效地在一組 goroutine 中傳遞共享值、取消信號、deadline 等信息,及時關(guān)閉不需要的 goroutine。

看完上述內(nèi)容,你們對怎么從MPG線程模型理解Go語言的并發(fā)程序有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。

本文名稱:怎么從MPG線程模型理解Go語言的并發(fā)程序
網(wǎng)頁路徑:http://muchs.cn/article36/jepopg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供電子商務(wù)、網(wǎng)站收錄、外貿(mào)網(wǎng)站建設(shè)、建站公司、App開發(fā)、標簽優(yōu)化

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設(shè)