日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
理解Go調(diào)度器并探索其工作原理

一、Go:它是什么?

除非你一直生活在石頭下,否則你可能已經(jīng)聽(tīng)說(shuō)過(guò) Golang。Golang 或 Go 是 Google 在21世紀(jì)初開(kāi)發(fā)的一種編程語(yǔ)言。其最有趣的特性之一是它通過(guò)使用 goroutines 來(lái)支持并發(fā),這些 goroutines 就像輕量級(jí)的線(xiàn)程。Goroutines 比實(shí)際的線(xiàn)程要便宜得多,甚至每秒可以調(diào)度數(shù)百萬(wàn)的 goroutines。

成都創(chuàng)新互聯(lián)是專(zhuān)業(yè)的舟山網(wǎng)站建設(shè)公司,舟山接單;提供成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站,網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專(zhuān)業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行舟山網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專(zhuān)業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專(zhuān)業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!

但 Go 是如何實(shí)現(xiàn)這一令人難以置信的壯舉的呢?它是如何提供這種能力的?讓我們看看 Golang 調(diào)度器在背后是如何工作的。

二、前提條件

在我們深入探討之前,有一些前提條件我必須談?wù)?。如果你已?jīng)對(duì)它們非常熟悉,可以跳過(guò)它們。

1.系統(tǒng)調(diào)用

系統(tǒng)調(diào)用是向內(nèi)核的接口。就像 web 服務(wù)為用戶(hù)暴露一個(gè) REST API 接口一樣。

系統(tǒng)調(diào)用為 Linux 內(nèi)核提供了一個(gè)接口。

為了更多地了解這些系統(tǒng)調(diào)用,讓我們寫(xiě)一點(diǎn)代碼!你可能首先問(wèn)的問(wèn)題是,選擇哪種語(yǔ)言?

答案有點(diǎn)復(fù)雜,但讓我們先了解如何在 C 語(yǔ)言中做,然后再花一些時(shí)間思考如何在其他語(yǔ)言中執(zhí)行相同的操作。

現(xiàn)在,讓我們嘗試 stat 系統(tǒng)調(diào)用。該調(diào)用非常簡(jiǎn)單。它接受一個(gè)文件路徑并返回有關(guān)該文件的大量信息。現(xiàn)在,我們將打印返回的幾個(gè)值,即文件的所有者和文件的大?。ㄒ宰止?jié)為單位)。

#include
#include
int main()
{
  //pointer to stat struct
  struct stat sfile;
  //stat system call
  stat("myfile", &sfile);  //accessing some data returned from stat
  printf("uid = %o\nfileszie = %lld\n", sfile.st_uid, sfile.st_size);  return 0;
}

輸出是相當(dāng)容易預(yù)測(cè)的,如下所示:

uid = 765
filesize = 11

所有的語(yǔ)言在內(nèi)部都調(diào)用相同的系統(tǒng)調(diào)用,因?yàn)槟阒荒苁褂眠@些系統(tǒng)調(diào)用與內(nèi)核進(jìn)行交互。例如,看一些 NodeJS 代碼這里,你可以看到它是如何聲明文件路徑的。他們圍繞這些系統(tǒng)調(diào)用做了很多工作,為開(kāi)發(fā)者提供了更簡(jiǎn)單的接口,但在底層,它們使用內(nèi)核提供的相同的系統(tǒng)調(diào)用。

2.線(xiàn)程

我相信大多數(shù)人在生活中某個(gè)時(shí)候都聽(tīng)說(shuō)過(guò)線(xiàn)程,但你真的知道它們是什么嗎?它們是如何工作的?

我會(huì)盡快為你解釋。

你可能已經(jīng)讀到過(guò)有多個(gè)核心的處理器。這些核心中的每一個(gè)都像一個(gè)可以獨(dú)立工作的獨(dú)立處理單元。

線(xiàn)程是基于這一點(diǎn)的抽象,我們可以在我們的程序中“創(chuàng)建”線(xiàn)程,并在每個(gè)線(xiàn)程上運(yùn)行代碼,然后由內(nèi)核調(diào)度在單個(gè)核心上運(yùn)行。

所以,在任何時(shí)候,單個(gè)核心都在運(yùn)行一個(gè)執(zhí)行線(xiàn)程。

我們?nèi)绾蝿?chuàng)建線(xiàn)程?通過(guò)系統(tǒng)調(diào)用!

那為什么不直接使用核心呢?為什么我們不直接寫(xiě)new Core()而是通過(guò)new Thread()創(chuàng)建線(xiàn)程?因?yàn)樵诓僮飨到y(tǒng)中可能有多個(gè)程序正在運(yùn)行,每個(gè)程序可能都想并行執(zhí)行代碼。由于核心數(shù)量有限,每個(gè)程序都必須知道有多少可用的核心,如果一個(gè)程序占用了所有的核心,它基本上可以完全阻塞操作系統(tǒng)。操作系統(tǒng)可能還需要執(zhí)行比任何單個(gè)程序更重要或優(yōu)先級(jí)更高的工作,所以需要一層抽象。

3.Goroutines

正如我上面解釋的,goroutines類(lèi)似于輕量級(jí)的線(xiàn)程,它們可以并發(fā)運(yùn)行并且可以在單獨(dú)的線(xiàn)程中運(yùn)行。

在繼續(xù)之前,沒(méi)有真正需要了解Golang,但我認(rèn)為至少在繼續(xù)之前,看一下一個(gè)非常簡(jiǎn)單的goroutine的實(shí)現(xiàn)是有意義的。

package main

import (
 "fmt"
 "sync"
 "time"
)

func printForever(s string) {
 // This is an infinite loop that prints the string s forever
 for {
  fmt.Println(s)
  time.Sleep(time.Millisecond)
 }
}

func main() {
 var wg sync.WaitGroup
 
 // A waitgroup is just a way to wait for goroutines to finish
 wg.Add(2)

 // Any function executed with the "go" keyword will run as a goroutine
 go printForever("HELLO")
 go printForever("WORLD")
 
 // This line of code blocks until both goroutines are finished
 wg.Wait()
}

輸出,你可能猜到了,是連續(xù)打印的單詞“HELLO”和“WORLD”。

既然用go關(guān)鍵字標(biāo)記的兩個(gè)函數(shù)調(diào)用都是并行運(yùn)行的。

你的直覺(jué)可能會(huì)讓你認(rèn)為這些 goroutines 只是并行運(yùn)行在 CPU 上的單獨(dú)線(xiàn)程,并由操作系統(tǒng)管理,但這不是真的。盡管現(xiàn)在每次調(diào)用都可以是單個(gè)線(xiàn)程,但我們很快會(huì)發(fā)現(xiàn)這并不實(shí)際。

三、設(shè)計(jì)我們自己的調(diào)度器

讓我們討論 Go 調(diào)度器,就好像我們正在創(chuàng)建自己的調(diào)度器一樣。我知道這個(gè)任務(wù)可能看起來(lái)很艱巨,但本質(zhì)上,我們有一些工作單元,比如說(shuō)一些函數(shù),我們需要將它們調(diào)度到有限的線(xiàn)程上。

所以,為了更正式地描述這一點(diǎn),函數(shù)是單一的工作單位。它是執(zhí)行某些處理的代碼行的序列。現(xiàn)在,讓我們不要用像 async/await 這樣的東西來(lái)混淆自己,我們說(shuō)一個(gè)函數(shù)只包含非?;镜拇a。也許像這樣,

int add(int a, int b) {
  int sum = a + b;
  cout << "Calculated the sum: " << sum;
  return sum;
}

它應(yīng)該在線(xiàn)程上運(yùn)行,這些線(xiàn)程在內(nèi)部在處理器的核心上進(jìn)行多路復(fù)用。核心是有限的,但線(xiàn)程可以是無(wú)限的。管理這些線(xiàn)程并在有限的CPU核心上運(yùn)行它們是操作系統(tǒng)調(diào)度器的工作,所以我們不需要關(guān)心核心。我們可以簡(jiǎn)單地將線(xiàn)程視為實(shí)現(xiàn)并行性的單位。

我們確實(shí)有一套系統(tǒng)調(diào)用,我們可以運(yùn)行它們?cè)诓僮飨到y(tǒng)上創(chuàng)建線(xiàn)程。系統(tǒng)調(diào)用往往是昂貴的,所以我們需要確保我們不經(jīng)常創(chuàng)建/刪除線(xiàn)程。

我們調(diào)度器的工作很簡(jiǎn)單,將這些函數(shù)或 goroutines 運(yùn)行在線(xiàn)程上。我們的調(diào)度器可以在任何時(shí)候被賦予新的函數(shù)/goroutines,它將必須在線(xiàn)程上調(diào)度它們。

在下一部分,讓我們繼續(xù)明確所有的需求。

需求

讓我們列出我們調(diào)度器中想要的一些優(yōu)先事項(xiàng),這與 Golang 所設(shè)置的優(yōu)先級(jí)相似,這些將影響我們?cè)谠O(shè)計(jì)調(diào)度器時(shí)所做的設(shè)計(jì)決策。

(1) 輕量級(jí) goroutines

我們希望我們的 goroutines 非常輕量級(jí)。我們希望能夠處理每秒調(diào)度的數(shù)百萬(wàn) goroutines。這意味著我們想要做的所有事情,如創(chuàng)建一個(gè) goroutine 或在線(xiàn)程上切換一個(gè) goroutine 必須非常快。

(2) 處理系統(tǒng)調(diào)用和 I/O

系統(tǒng)調(diào)用可能難以處理,但我們?nèi)匀幌M麨槲覀兊暮瘮?shù)提供能夠進(jìn)行系統(tǒng)調(diào)用和執(zhí)行 I/O 操作的能力。這意味著某個(gè)函數(shù)可以打開(kāi)并讀取一個(gè)文件,例如。系統(tǒng)調(diào)用有點(diǎn)棘手,因?yàn)橐坏╅_(kāi)始了系統(tǒng)調(diào)用,我們就看不到它何時(shí)結(jié)束或需要多長(zhǎng)時(shí)間。在某種意義上,我們不能再控制或透明地看到函數(shù)了。當(dāng)我們開(kāi)始設(shè)計(jì)我們的調(diào)度器時(shí),這個(gè)問(wèn)題會(huì)出現(xiàn)。

(3) 并行

我們當(dāng)然希望同時(shí)運(yùn)行多個(gè) goroutines。記住,我們不需要擔(dān)心核心,我們只需考慮如何將這些函數(shù)多路復(fù)用到線(xiàn)程上。

(4) 公平

我們希望一個(gè)系統(tǒng)確保運(yùn)行在其中的 goroutines 是公平的。這意味著一個(gè) goroutine 不應(yīng)該阻塞其他 goroutines 很長(zhǎng)時(shí)間。公平可能有點(diǎn)難以客觀定義,但其思路是每個(gè) goroutine 都應(yīng)該在線(xiàn)程上獲得公平的執(zhí)行時(shí)間。這似乎是合乎邏輯的,因?yàn)闆](méi)有定義某種優(yōu)先級(jí)系統(tǒng)的要求,沒(méi)有理由給予任何這些函數(shù)優(yōu)先權(quán)。

(5) 避免過(guò)度訂閱

重要的是要控制任何程序?qū)⑹褂玫木€(xiàn)程數(shù)。當(dāng)確保我們不會(huì)無(wú)謂地獲取操作系統(tǒng)級(jí)別的線(xiàn)程并阻塞機(jī)器上運(yùn)行的其他進(jìn)程時(shí),這可能會(huì)很有用。因此,我們應(yīng)該嘗試限制我們使用的線(xiàn)程數(shù)量。如果我們超出這個(gè)限制,我們將過(guò)度訂閱,為了對(duì)系統(tǒng)上運(yùn)行的其他進(jìn)程公平,我們會(huì)認(rèn)為這是一件很糟糕的事情,并盡量避免它。

四、第1部分:調(diào)度 Goroutines

1.1:1 映射

一個(gè)非常簡(jiǎn)單的系統(tǒng)是我們?yōu)槊總€(gè) goroutine 創(chuàng)建一個(gè)線(xiàn)程。這意味著我們?cè)?goroutines 和線(xiàn)程之間有一個(gè)1:1的映射。

因此,每當(dāng)用戶(hù)嘗試創(chuàng)建一個(gè) goroutine,我們的調(diào)度器創(chuàng)建一個(gè)線(xiàn)程,該線(xiàn)程開(kāi)始執(zhí)行 goroutine。

這導(dǎo)致了很多問(wèn)題:

  • 我們的 goroutines 不再輕量級(jí),創(chuàng)建一個(gè)線(xiàn)程需要一個(gè)系統(tǒng)調(diào)用,這可能需要任意的時(shí)間。而且,一個(gè)線(xiàn)程有它自己的堆棧、寄存器和其他變量。如果你想運(yùn)行幾十個(gè)線(xiàn)程,這是可以的,但當(dāng)你想每秒運(yùn)行數(shù)百萬(wàn) goroutines 時(shí),這是不切實(shí)際的。
  • 如果我們?yōu)槊總€(gè) goroutine 創(chuàng)建一個(gè)線(xiàn)程,而我們的用戶(hù)可以創(chuàng)建他們想要的任何數(shù)量的 goroutines,那么我們就過(guò)度訂閱了操作系統(tǒng)資源。我們希望能夠控制我們使用的操作系統(tǒng)線(xiàn)程的數(shù)量。

2.M:N 映射

這里的邏輯步驟是使用 M:N 映射。這意味著M個(gè) goroutines 需要映射到N個(gè)線(xiàn)程。因此,用戶(hù)可以創(chuàng)建他們想要的任意數(shù)量的 goroutines,但我們對(duì)一組有限的線(xiàn)程有控制權(quán),并且我們將他們的 goroutines 調(diào)度到一個(gè)線(xiàn)程上一段時(shí)間。我們也可以暫停/恢復(fù)線(xiàn)程上運(yùn)行的 goroutines。

這樣做有點(diǎn)復(fù)雜,因?yàn)槲覀儸F(xiàn)在需要了解如何將一個(gè) goroutine/function 映射到一個(gè)線(xiàn)程上。我們可能只在每個(gè)線(xiàn)程上運(yùn)行一個(gè)函數(shù)很短的時(shí)間,所以我們需要做一些繁重的工作以確保我們得到正確的邏輯。

一個(gè)好的比喻可能是餐館。餐廳通常會(huì)有不同數(shù)量的侍者和廚師。侍者的數(shù)量取決于餐廳的客人或桌子的數(shù)量,而廚師的數(shù)量取決于訂單的數(shù)量和烹飪所需的時(shí)間。

在這個(gè)比喻中,我們可以把函數(shù)想象成侍者,線(xiàn)程想象成廚師。

為每個(gè)侍者提供一個(gè)廚師并不真正有意義,因?yàn)橐苍S一個(gè)廚師可以很快地烹飪食物,也許餐廳在高峰時(shí)間有很多客人,但在慢時(shí),也許需要更少的侍者。所以為了正確管理這家餐廳,你需要某種系統(tǒng)來(lái)將M個(gè)侍者分配給N個(gè)廚師。這正是我們必須在調(diào)度器中做的事情!

為了進(jìn)一步延續(xù)這個(gè)比喻,也許一個(gè)簡(jiǎn)單的系統(tǒng)是有一個(gè)訂單隊(duì)列,每個(gè)訂單都有一個(gè)數(shù)字,廚師們一個(gè)接一個(gè)地選擇數(shù)字。這也將是我們調(diào)度器的起點(diǎn)!

3.一個(gè)基本的系統(tǒng) — 全局運(yùn)行隊(duì)列

讓我們首先設(shè)計(jì)一個(gè)基本的系統(tǒng),并對(duì)其進(jìn)行迭代。

我們?cè)O(shè)置一個(gè)全局的先入先出的運(yùn)行隊(duì)列,其中包含需要運(yùn)行的一組 goroutines。每個(gè)線(xiàn)程從這個(gè)隊(duì)列中選擇一個(gè) goroutine 并執(zhí)行它。當(dāng)它執(zhí)行完一個(gè) goroutine 后,它再次選擇一個(gè)新的 goroutine 并繼續(xù)反復(fù)進(jìn)行相同的過(guò)程。

為了更好地理解,讓我們編寫(xiě)一些基本的偽代碼,描述每個(gè)線(xiàn)程將執(zhí)行的內(nèi)容。

void runThread() {
  while (true) {
    // Check if there is an empty goroutine
    bool isEmpty = globalRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = globalRunQueue.getNextGoroutine();
      g();
    }
  }
}

重要的是要記住,每一個(gè)線(xiàn)程都在并行地運(yùn)行相同的代碼。

這個(gè)系統(tǒng)中的一個(gè)問(wèn)題是,每個(gè)線(xiàn)程都在訪(fǎng)問(wèn)相同的共享變量,即globalRunQueue。所以,一個(gè)線(xiàn)程,比如說(shuō) ThreadA,檢查隊(duì)列是否為空,但在它能夠從隊(duì)列中獲取 goroutine 之前,另一個(gè)線(xiàn)程訪(fǎng)問(wèn)了隊(duì)列并選擇了 goroutine。

我們需要一種方法來(lái)確保任何時(shí)候只有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)運(yùn)行隊(duì)列。

4.互斥鎖

解決這個(gè)問(wèn)題的一種方法是引入一個(gè)互斥鎖?;コ怄i只是一個(gè)鎖,每個(gè)線(xiàn)程在訪(fǎng)問(wèn)隊(duì)列之前都必須獲取它。只有線(xiàn)程擁有互斥鎖時(shí),它才能執(zhí)行操作。完成后,它可以釋放互斥鎖,供其他線(xiàn)程獲取。

把它想象成一個(gè)鎖,確保只有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)全局運(yùn)行隊(duì)列。

繼續(xù)我們的偽代碼:

void runThread() {
  while (true) {
    // First acquire the mutex
    // This call would block execution until the mutex is free
    mutex m = globalRunQueue.getMutex();

    // Check if there is an empty goroutine
    bool isEmpty = globalRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = globalRunQueue.getNextGoroutine();
      g();
    }
    
    // Release the mutex
    m.release();
  }
}

現(xiàn)在,下一個(gè)問(wèn)題是每個(gè)線(xiàn)程都必須等待獲取一個(gè)互斥鎖來(lái)對(duì)運(yùn)行隊(duì)列執(zhí)行任何操作。在未來(lái),我們也可能增加暫停 goroutines、恢復(fù) goroutines 等功能。如果所有操作都需要互斥鎖,那么它可能成為我們系統(tǒng)的瓶頸。

5.全局和本地運(yùn)行隊(duì)列

為了解決單一互斥鎖的問(wèn)題,我們?yōu)槊總€(gè)線(xiàn)程提供了它自己的本地運(yùn)行隊(duì)列。

每個(gè) goroutine 在創(chuàng)建時(shí)開(kāi)始在全局運(yùn)行隊(duì)列中執(zhí)行,但在某個(gè)時(shí)候由不同的系統(tǒng)分配給一個(gè)本地運(yùn)行隊(duì)列。每個(gè)線(xiàn)程大多與它自己的本地運(yùn)行隊(duì)列進(jìn)行交互,選擇一個(gè) goroutine 并執(zhí)行它。這樣,我們把大部分工作轉(zhuǎn)移到了每個(gè)線(xiàn)程的本地運(yùn)行隊(duì)列。由于本地運(yùn)行隊(duì)列只被一個(gè)線(xiàn)程訪(fǎng)問(wèn),所以它甚至不需要互斥鎖。

我們現(xiàn)在不再需要互斥鎖,我們的代碼變得更簡(jiǎn)單了。

void runThread() {
  while (true) {
    // Check if there is an empty goroutine
    bool isEmpty = this.localRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = this.localRunQueue.getNextGoroutine();
      g();
    }
  }
}

我們需要明白的是,我們?nèi)匀挥幸粋€(gè)帶有互斥鎖的全局運(yùn)行隊(duì)列,但我們可以大大減少對(duì)全局運(yùn)行隊(duì)列的調(diào)用,因?yàn)橐粋€(gè)單獨(dú)的線(xiàn)程主要是從其自己的本地運(yùn)行隊(duì)列中取出 goroutines。它可能偶爾從全局運(yùn)行隊(duì)列中獲取,比如當(dāng)其自己的本地運(yùn)行隊(duì)列為空時(shí),但那是一個(gè)罕見(jiàn)的情況(如果你感興趣,這里是找到一個(gè)正在運(yùn)行的 goroutine 執(zhí)行的代碼,你可以清楚地看到,如果它在本地運(yùn)行隊(duì)列中找不到一個(gè) goroutine,它將會(huì)輪詢(xún)?nèi)诌\(yùn)行隊(duì)列)。

6.工作竊取

我們可以為我們的系統(tǒng)添加的另一個(gè)有趣的特性是“工作竊取”的概念。每當(dāng)線(xiàn)程的本地運(yùn)行隊(duì)列為空時(shí),它可以嘗試從其他本地運(yùn)行隊(duì)列中竊取工作。這也有助于平衡 goroutines 的負(fù)載。

void runThread() {
  while (true) {
    // Check if there is an empty goroutine
    bool isEmpty = this.localRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = this.localRunQueue.getNextGoroutine();
      g();
    }
    else {
      // Steal work from other local run queues
      for (int i = 0; i < localRunQueueCount; i += 1) {
        // Check if there is an empty goroutine in this local run queue
        bool isEmpty = localRunQueues[i].empty();
        
        // If not, steal the next goroutine, and run it
        goroutine g = localRunQueues[i].getNextGoroutine();
        g();
      }
    }
  }
}

在這個(gè)系統(tǒng)中,即使一個(gè)線(xiàn)程執(zhí)行一個(gè) goroutine 需要很長(zhǎng)時(shí)間,它的本地運(yùn)行隊(duì)列中的其他 goroutines 也不會(huì)被餓死。最終,另一個(gè)線(xiàn)程會(huì)“竊取”這些 goroutines 并執(zhí)行它們。

7.處理系統(tǒng)調(diào)用

如我之前所提到的,系統(tǒng)調(diào)用可能有點(diǎn)難以處理,因?yàn)樗鼈兛赡苄枰喈?dāng)長(zhǎng)的時(shí)間。當(dāng)一個(gè) goroutine 執(zhí)行一個(gè)系統(tǒng)調(diào)用,比如從一個(gè)文件中讀取數(shù)據(jù),它可能需要很長(zhǎng)時(shí)間才能返回。我們甚至不知道它是否會(huì)返回,或者操作系統(tǒng)在幕后到底在做什么。

在操作系統(tǒng)返回之前,該線(xiàn)程不會(huì)被賦予任何新的工作,基本上是處于睡眠狀態(tài)。我們可能會(huì)遇到一個(gè)情況,即分配給我們程序的所有線(xiàn)程都只是等待系統(tǒng)調(diào)用完成。

我們?nèi)绾谓鉀Q這個(gè)問(wèn)題呢?在進(jìn)行系統(tǒng)調(diào)用之前,我們創(chuàng)建一個(gè)新的線(xiàn)程。由于我們是在編寫(xiě)語(yǔ)言,以及編寫(xiě)打開(kāi)文件的函數(shù),我們可以在打開(kāi)文件之前簡(jiǎn)單地添加一行來(lái)創(chuàng)建一個(gè)新線(xiàn)程。新線(xiàn)程創(chuàng)建后,當(dāng)前線(xiàn)程進(jìn)入睡眠狀態(tài),直到系統(tǒng)調(diào)用完成。

從技術(shù)上講,這并不是過(guò)度訂閱,因?yàn)楫?dāng)前線(xiàn)程正在休眠,因此不會(huì)占用操作系統(tǒng)的資源。

但這意味著我們可能有很多我們不使用的休眠線(xiàn)程。

再次說(shuō),這并不是過(guò)度訂閱,但它為我們帶來(lái)了一個(gè)不同的問(wèn)題。由于我們?yōu)槊總€(gè)線(xiàn)程提供資源(本地運(yùn)行隊(duì)列),如果我們有很多線(xiàn)程,我們將為每個(gè)線(xiàn)程分配內(nèi)存。

此外,我們尚未深入探討的系統(tǒng)的其他部分也可能會(huì)出現(xiàn)一些問(wèn)題,例如將 goroutines 分配給本地運(yùn)行隊(duì)列的系統(tǒng)可能會(huì)將一個(gè) goroutine 分配給一個(gè)當(dāng)前被系統(tǒng)調(diào)用阻塞的線(xiàn)程。

此外,工作竊取也可能變得有點(diǎn)繁瑣。如果我們有很多線(xiàn)程,工作竊取將需要檢查很多本地運(yùn)行隊(duì)列。

8.處理器

為了解決這個(gè)問(wèn)題,我們?cè)黾恿肆硪粚映橄?,稱(chēng)為處理器。

每個(gè)線(xiàn)程都會(huì)獲取一個(gè)處理器,該處理器包含執(zhí)行 Go 代碼所需的變量。所以我們將本地運(yùn)行隊(duì)列以及我們可能擁有的任何其他變量(如緩存等)移動(dòng)到處理器中。

當(dāng)一個(gè)線(xiàn)程在系統(tǒng)調(diào)用上被阻塞時(shí),它將釋放處理器,另一個(gè)線(xiàn)程將獲取它并從中斷的地方繼續(xù)。

這并不顯著地改變了我們的偽代碼,除了我們現(xiàn)在在使用本地運(yùn)行隊(duì)列時(shí)使用了一個(gè)處理器。

void runThread() {
  while (true) {
    // Check if there is an empty goroutine
    bool isEmpty = this.processor.localRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = this.processor.localRunQueue.getNextGoroutine();
      g();
    }
    else {
      // Steal work from other local run queues
      for (int i = 0; i < localRunQueueCount; i += 1) {
        // Check if there is an empty goroutine in this local run queue
        bool isEmpty = localRunQueues[i].empty();
        
        // If not, steal the next goroutine, and run it
        goroutine g = localRunQueues[i].getNextGoroutine();
        g();
      }
    }
  }
}

五、第2部分:公平性

1.搶占

為了確保沒(méi)有單一的 goroutine 長(zhǎng)時(shí)間占用一個(gè)線(xiàn)程,我們可以添加一個(gè)非常簡(jiǎn)單的機(jī)制,如果一個(gè) goroutine 的執(zhí)行時(shí)間超過(guò)了一定的時(shí)間段,比如說(shuō)10ms,那么它會(huì)被預(yù)先暫停。然后我們將它加到運(yùn)行隊(duì)列的尾部。

2.全局運(yùn)行隊(duì)列饑餓

有一種可能性是,goroutines 不斷地在全局運(yùn)行隊(duì)列中被填充,而每個(gè)處理器都繼續(xù)在其自己的本地運(yùn)行隊(duì)列上工作。這可能導(dǎo)致全局運(yùn)行隊(duì)列中的 goroutines 基本上餓死。

這個(gè)問(wèn)題的簡(jiǎn)單解決方案是什么呢?讓我們偶爾檢查全局運(yùn)行隊(duì)列,而不是本地運(yùn)行隊(duì)列。

其實(shí)在代碼中理解這一點(diǎn)非常容易。

void runThread() {
  // A simple variable to keep track of the number of goroutines
  // we are running
  int schedTick = 0;

  while (true) {
    // Occasionally poll the global run queue instead of the local run queue
    // 61 is the actual number they use to decide to poll the global run queue!
    // https://github.com/golang/go/blob/master/src/runtime/proc.go#L2753
    if (schedTick % 61 == 0) {
      // Polling the global run queue
      goroutine g = pollGlobalRunQueue()
      if (g != nil) {
        g();
      }
    }

    // Check if there is an empty goroutine
    bool isEmpty = this.processor.localRunQueue.empty();
    
    // If not empty
    if (!isEmpty) {
      goroutine g = this.processor.localRunQueue.getNextGoroutine();
      g();

      // Increment the schedTick variable
      schedTick ++;
    }
    else {
      // Steal work from other local run queues
      for (int i = 0; i < localRunQueueCount; i += 1) {
        // Check if there is an empty goroutine in this local run queue
        bool isEmpty = localRunQueues[i].empty();
        
        // If not, steal the next goroutine, and run it
        goroutine g = localRunQueues[i].getNextGoroutine();
        g();
      }
    }
  }
}

六、結(jié)論

這是我嘗試簡(jiǎn)要描述 Golang 并發(fā)是如何工作的。我可能做了一些簡(jiǎn)化,但這大致是并發(fā)模型的方向。歡迎查看 Go 源代碼中的 findRunnable 函數(shù)的實(shí)際代碼這里,現(xiàn)在你應(yīng)該能夠理解它的大部分內(nèi)容。

我們確實(shí)跳過(guò)了一些概念,比如網(wǎng)絡(luò)輪詢(xún)器,但這篇文章已經(jīng)變得非常長(zhǎng),我不想在這樣一個(gè)預(yù)計(jì)10-15分鐘的閱讀中塞入太多的信息。

我選擇這個(gè)主題的原因是,我一直認(rèn)為并發(fā)和并行這樣的概念更偏理論而不是實(shí)際,我發(fā)現(xiàn)自己難以理解它內(nèi)部是如何工作的。在我看來(lái),重要的是要認(rèn)識(shí)到,歸根結(jié)底,這些看似非常困難且像魔法一樣的底層概念只不過(guò)是代碼片段,理解它們以及導(dǎo)致它們的決策不應(yīng)該比理解任何其他代碼更難。


分享標(biāo)題:理解Go調(diào)度器并探索其工作原理
當(dāng)前URL:http://m.5511xx.com/article/dpeehss.html