亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

今天的文章我首先說一下之前文章里的思考題的解決思路,我會給出完整可運(yùn)行的代碼。之后通過觀察程序的運(yùn)行結(jié)果里的現(xiàn)象簡單介紹 Go 語言的調(diào)度器是如何對 goroutine 進(jìn)行調(diào)度的。

回答之前的問題

先來回顧一下上周文章里思考題的題目:

假設(shè)有一個超長的切片,切片的元素類型為int,切片中的元素為亂序排列。限時(shí)5秒,使用多個goroutine查找切片中是否存在給定值,在找到目標(biāo)值或者超時(shí)后立刻結(jié)束所有g(shù)oroutine的執(zhí)行。

比如切片為:[23, 32, 78, 43, 76, 65, 345, 762, ...... 915, 86],查找的目標(biāo)值為345,如果切片中存在目標(biāo)值程序輸出:"Found it!"并且立即取消仍在執(zhí)行查找任務(wù)的 goroutine 。如果在超時(shí)時(shí)間未找到目標(biāo)值程序輸出:"Timeout! Not Found",同時(shí)立即取消仍在執(zhí)行查找任務(wù)的 goroutine 。

首先題目里提到了 在找到目標(biāo)值或者超時(shí)后立刻結(jié)束所有g(shù)oroutine的執(zhí)行 ,完成這兩個功能需要借助計(jì)時(shí)器、通道和 context 才行。我能想到的第一點(diǎn)就是要用 context.WithCancel 創(chuàng)建一個上下文對象傳遞給每個執(zhí)行任務(wù)的 goroutine ,外部在滿足條件后(找到目標(biāo)值或者已超時(shí))通過調(diào)用上下文的取消函數(shù)來通知所有 goroutine 停止工作。

func main() {
    timer := time.NewTimer(time.Second * 5)
    ctx, cancel := context.WithCancel(context.Background())
    resultChan := make(chan bool)
  ......
    select {
    case <-timer.C:
        fmt.Fprintln(os.Stderr, "Timeout! Not Found")
        cancel()
    case <- resultChan:
        fmt.Fprintf(os.Stdout, "Found it!n")
        cancel()
    }
}

執(zhí)行任務(wù)的 goroutine 們?nèi)绻业侥繕?biāo)值后需要通知外部等待任務(wù)執(zhí)行的主 goroutine ,這個工作是典型的應(yīng)用通道的場景,上面代碼也已經(jīng)看到了,我們創(chuàng)建了一個接收查找結(jié)果的通道,接下來要做的就是把它和上下文對象一起傳遞給執(zhí)行任務(wù)的 goroutine 。

func SearchTarget(ctx context.Context, data []int, target int, resultChan chan bool) {
    for _, v := range data {
        select {
        case <- ctx.Done():
            fmt.Fprintf(os.Stdout, "Task cancelded! n")
            return
        default:
        }
        // 模擬一個耗時(shí)查找,這里只是比對值,真實(shí)開發(fā)中可以是其他操作
        fmt.Fprintf(os.Stdout, "v: %d n", v)
        time.Sleep(time.Millisecond * 1500)
        if target == v {
            resultChan <- true
            return
        }
    }

}

在執(zhí)行查找任務(wù)的 goroutine 里接收上下文的取消信號,為了不阻塞查找任務(wù),我們使用了 select 語句加 default 的組合:

select {
case <- ctx.Done():
    fmt.Fprintf(os.Stdout, "Task cancelded! n")
    return
default:
}

在 goroutine 里面如果找到了目標(biāo)值,則會通過發(fā)送一個 true 值給 resultChan ,讓外面等待的主 goroutine 收到一個已經(jīng)找到目標(biāo)值的信號。

resultChan <- true

這樣通過上下文的 Done 通道和 resultChan 通道, goroutine 們就能相互通信了。

Go 語言中最常見的、也是經(jīng)常被人提及的設(shè)計(jì)模式 — 不要通過共享內(nèi)存的方式進(jìn)行通信,而是應(yīng)該通過通信的方式共享內(nèi)存

完整的源代碼如下:

package main

import (
    "context"
    "fmt"
    "os"
    "time"
)

func main() {
    timer := time.NewTimer(time.Second * 5)
    data := []int{1, 2, 3, 10, 999, 8, 345, 7, 98, 33, 66, 77, 88, 68, 96}
    dataLen := len(data)
    size := 3
    target := 345
    ctx, cancel := context.WithCancel(context.Background())
    resultChan := make(chan bool)
    for i := 0; i < dataLen; i += size {
        end := i + size
        if end >= dataLen {
            end = dataLen - 1
        }
        go SearchTarget(ctx, data[i:end], target, resultChan)
    }
    select {
    case <-timer.C:
        fmt.Fprintln(os.Stderr, "Timeout! Not Found")
        cancel()
    case <- resultChan:
        fmt.Fprintf(os.Stdout, "Found it!n")
        cancel()
    }

    time.Sleep(time.Second * 2)
}

func SearchTarget(ctx context.Context, data []int, target int, resultChan chan bool) {
    for _, v := range data {
        select {
        case <- ctx.Done():
            fmt.Fprintf(os.Stdout, "Task cancelded! n")
            return
        default:
        }
        // 模擬一個耗時(shí)查找,這里只是比對值,真實(shí)開發(fā)中可以是其他操作
        fmt.Fprintf(os.Stdout, "v: %d n", v)
        time.Sleep(time.Millisecond * 1500)
        if target == v {
            resultChan <- true
            return
        }
    }

}

為了打印演示結(jié)果所以加了幾處 time.Sleep ,這個程序更多的是提供思路框架,所以細(xì)節(jié)的地方?jīng)]有考慮。有幾位讀者把他們的答案發(fā)給了我,其中有一位的提供的答案在代碼實(shí)現(xiàn)上考慮的更全面,這個我們放到文末再說。

上面程序的執(zhí)行結(jié)果如下:

v: 1 
v: 88 
v: 33 
v: 10 
v: 345 
Found it!
v: 2 
v: 999 
Task cancelded! 
v: 68 
Task cancelded! 
Task cancelded!

因?yàn)槭遣l(fā)程序所以每次打印的結(jié)果的順序是不一樣的,這個你們可以自己試驗(yàn)一下。而且也并不是先開啟的 goroutine 就一定會先執(zhí)行,主要還是看調(diào)度器先調(diào)度哪個。

Go語言調(diào)度器

所有應(yīng)用程序都是運(yùn)行在操作系統(tǒng)上,真正用來干活(計(jì)算)的是 CPU 。所以談到 Go 語言調(diào)度器,我們也繞不開操作系統(tǒng)、進(jìn)程與線程這些概念。線程是操作系統(tǒng)調(diào)度時(shí)的最基本單元,而 linux 在調(diào)度器并不區(qū)分進(jìn)程和線程的調(diào)度,它們在不同操作系統(tǒng)上也有不同的實(shí)現(xiàn),但是在大多數(shù)的實(shí)現(xiàn)中線程都屬于進(jìn)程。

多個線程可以屬于同一個進(jìn)程并共享內(nèi)存空間。因?yàn)槎嗑€程不需要創(chuàng)建新的虛擬內(nèi)存空間,所以它們也不需要內(nèi)存管理單元處理上下文的切換,線程之間的通信也正是基于共享的內(nèi)存進(jìn)行的,與重量級的進(jìn)程相比,線程顯得比較輕量。

雖然線程比較輕量,但是在調(diào)度時(shí)也有比較大的額外開銷。每個線程會都占用 1 兆以上的內(nèi)存空間,在對線程進(jìn)行切換時(shí)不止會消耗較多的內(nèi)存,恢復(fù)寄存器中的內(nèi)容還需要向操作系統(tǒng)申請或者銷毀對應(yīng)的資源。

大量的線程出現(xiàn)了新的問題

  • 高內(nèi)存占用
  • 調(diào)度的CPU高消耗

然后工程師們就發(fā)現(xiàn),其實(shí)一個線程分為"內(nèi)核態(tài)"線程和"用戶態(tài)"線程。

一個 用戶態(tài)線程 必須要綁定一個 內(nèi)核態(tài)線程 ,但是CPU并不知道有 用戶態(tài)線程 的存在,它只知道它運(yùn)行的是一個 內(nèi)核態(tài)線程 (Linux的PCB進(jìn)程控制塊)。這樣,我們再去細(xì)化分類,內(nèi)核線程依然叫線程(thread),用戶線程叫協(xié)程(co-routine)。既然一個協(xié)程可以綁定一個線程,那么也可以通過實(shí)現(xiàn)協(xié)程調(diào)度器把多個協(xié)程與一個或者多個線程進(jìn)行綁定。

Go 語言的 goroutine 來自協(xié)程的概念,讓一組可復(fù)用的函數(shù)運(yùn)行在一組線程之上,即使有協(xié)程阻塞,該線程的其他協(xié)程也可以被 runtime 調(diào)度,轉(zhuǎn)移到其他可運(yùn)行的線程上。最關(guān)鍵的是,程序員看不到這些底層的細(xì)節(jié),這就降低了編程的難度,提供了更容易的并發(fā)。

Go 中,協(xié)程被稱為 goroutine ,它非常輕量,一個 goroutine 只占幾KB,并且這幾KB就足夠 goroutine 運(yùn)行完,這就能在有限的內(nèi)存空間內(nèi)支持大量 goroutine ,支持了更多的并發(fā)。雖然一個 goroutine 的棧只占幾KB,但實(shí)際是可伸縮的,如果需要更多內(nèi)存, runtime 會自動為 goroutine 分配。

既然我們知道了 goroutine 和系統(tǒng)線程的關(guān)系,那么最關(guān)鍵的一點(diǎn)就是實(shí)現(xiàn)協(xié)程調(diào)度器了。

Go 目前使用的調(diào)度器是2012年重新設(shè)計(jì)的,因?yàn)橹暗恼{(diào)度器性能存在問題,所以使用4年就被廢棄了。重新設(shè)計(jì)的調(diào)度器使用 G-M-P 模型并一直沿用至今。

并發(fā)問題的解決思路以及Go語言調(diào)度器工作原理

 

調(diào)度器G-M-P模型

  • G — 表示 goroutine,它是一個待執(zhí)行的任務(wù);
  • M — 表示操作系統(tǒng)的線程,它由操作系統(tǒng)的調(diào)度器調(diào)度和管理;
  • P — 表示處理器,它可以被看做運(yùn)行在線程上的本地調(diào)度器;

G

gorotuine 就是 Go 語言調(diào)度器中待執(zhí)行的任務(wù),它在運(yùn)行時(shí)調(diào)度器中的地位與線程在操作系統(tǒng)中差不多,但是它占用了更小的內(nèi)存空間,也降低了上下文切換的開銷。

goroutine 只存在于 Go 語言的運(yùn)行時(shí),它是 Go 語言在用戶態(tài)提供的線程,作為一種粒度更細(xì)的資源調(diào)度單元,如果使用得當(dāng)能夠在高并發(fā)的場景下更高效地利用機(jī)器的 CPU 。

M

Go 語言并發(fā)模型中的 M 是操作系統(tǒng)線程。調(diào)度器最多可以創(chuàng)建 10000 個線程,但是其中大多數(shù)的線程都不會執(zhí)行用戶代碼(可能陷入系統(tǒng)調(diào)用),最多只會有 GOMAXPROCS 個活躍線程能夠正常運(yùn)行。

在默認(rèn)情況下,運(yùn)行時(shí)會將 GOMAXPROCS 設(shè)置成當(dāng)前機(jī)器的核數(shù),我們也可以使用 runtime.GOMAXPROCS 來改變程序中最大的線程數(shù)。一個四核機(jī)器上會創(chuàng)建四個活躍的操作系統(tǒng)線程,每一個線程都對應(yīng)一個運(yùn)行時(shí)中的 runtime.m 結(jié)構(gòu)體。

在大多數(shù)情況下,我們都會使用 Go 的默認(rèn)設(shè)置,也就是活躍線程數(shù)等于 CPU 個數(shù),在這種情況下不會觸發(fā)操作系統(tǒng)的線程調(diào)度和上下文切換,所有的調(diào)度都會發(fā)生在用戶態(tài),由 Go 語言調(diào)度器觸發(fā),能夠減少非常多的額外開銷。

操作系統(tǒng)線程在 Go 語言中會使用私有結(jié)構(gòu)體 runtime.m 來表示

type m struct {
    g0   *g 
    curg *g
    ...
}

其中 g0 是持有調(diào)度棧的 goroutine , curg 是在當(dāng)前線程上運(yùn)行的用戶 goroutine ,用戶 goroutine 執(zhí)行完后,線程切換回 g0 上, g0 會從線程 M 綁定的 P 上的等待隊(duì)列中獲取 goroutine 交給線程。

P

調(diào)度器中的處理器 P 是線程和 goroutine 的中間層,它能提供線程需要的上下文環(huán)境,也會負(fù)責(zé)調(diào)度線程上的等待隊(duì)列,通過處理器 P 的調(diào)度,每一個內(nèi)核線程都能夠執(zhí)行多個 goroutine ,它能在 goroutine 進(jìn)行一些 I/O 操作時(shí)及時(shí)切換,提高線程的利用率。因?yàn)檎{(diào)度器在啟動時(shí)就會創(chuàng)建 GOMAXPROCS 個處理器,所以 Go 語言程序的處理器數(shù)量一定會等于 GOMAXPROCS ,這些處理器會綁定到不同的內(nèi)核線程上并利用線程的計(jì)算資源運(yùn)行 goroutine 。

此外在調(diào)度器里還有一個全局等待隊(duì)列,當(dāng)所有P本地的等待隊(duì)列被占滿后,新創(chuàng)建的 goroutine 會進(jìn)入全局等待隊(duì)列。 P 的本地隊(duì)列為空后, M 也會從全局隊(duì)列中拿一批待執(zhí)行的 goroutine 放到 P 本地的等待隊(duì)列中。

GMP模型圖示

并發(fā)問題的解決思路以及Go語言調(diào)度器工作原理

 

GMP模型圖示

  • 全局隊(duì)列:存放等待運(yùn)行的G。
  • P的本地隊(duì)列:同全局隊(duì)列類似,存放的也是等待運(yùn)行的G,存的數(shù)量有限,不超過256個。新建G時(shí),G優(yōu)先加入到P的本地隊(duì)列,如果隊(duì)列已滿,則會把本地隊(duì)列中一半的G移動到全局隊(duì)列。
  • P列表:所有的P都在程序啟動時(shí)創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS(可配置)個。
  • M:線程想運(yùn)行任務(wù)就得獲取P,從P的本地隊(duì)列獲取G,P隊(duì)列為空時(shí),M也會嘗試從全局隊(duì)列拿一批G放到P的本地隊(duì)列,或從其他P的本地隊(duì)列偷一半放到自己P的本地隊(duì)列。M運(yùn)行G,G執(zhí)行之后,M會從P獲取下一個G,不斷重復(fù)下去。
  • goroutine 調(diào)度器和OS調(diào)度器是通過M結(jié)合起來的,每個M都代表了1個內(nèi)核線程,OS調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到CPU上執(zhí)行。

調(diào)度器的策略

調(diào)度器的一個策略是盡可能的復(fù)用現(xiàn)有的活躍線程,通過以下兩個機(jī)制提高線程的復(fù)用:

  1. work stealing機(jī)制,當(dāng)本線程無可運(yùn)行的G時(shí),嘗試從其他線程綁定的P偷取G,而不是銷毀線程。
  2. hand off機(jī)制,當(dāng)本線程因?yàn)镚進(jìn)行系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。

Go 的運(yùn)行時(shí)并不具備操作系統(tǒng)內(nèi)核級的硬件中斷能力,基于工作竊取的調(diào)度器實(shí)現(xiàn),本質(zhì)上屬于先來先服務(wù)的協(xié)作式調(diào)度,為了解決響應(yīng)時(shí)間可能較高的問題,目前運(yùn)行時(shí)實(shí)現(xiàn)了協(xié)作式調(diào)度和搶占式調(diào)度兩種不同的調(diào)度策略,保證在大部分情況下,不同的 G 能夠獲得均勻的 CPU 時(shí)間片。

協(xié)作式調(diào)度依靠被調(diào)度方主動棄權(quán),系統(tǒng)監(jiān)控到一個 goroutine 運(yùn)行超過10ms會通過 runtime.Gosched 調(diào)用主動讓出執(zhí)行機(jī)會。搶占式調(diào)度則依靠調(diào)度器強(qiáng)制將被調(diào)度方被動中斷。

分享到:
標(biāo)簽:調(diào)度 語言
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定