AVt天堂网 手机版,亚洲va久久久噜噜噜久久4399,天天综合亚洲色在线精品,亚洲一级Av无码毛片久久精品

當前位置:首頁 > 科技  > 軟件

理解 Go 調度器并探索其工作原理

來源: 責編: 時間:2023-10-25 15:47:58 249觀看
導讀一、Go:它是什么?除非你一直生活在石頭下,否則你可能已經聽說過 Golang。Golang 或 Go 是 Google 在21世紀初開發的一種編程語言。其最有趣的特性之一是它通過使用 goroutines 來支持并發,這些 goroutines 就像輕量級的線

一、Go:它是什么?

除非你一直生活在石頭下,否則你可能已經聽說過 Golang。Golang 或 Go 是 Google 在21世紀初開發的一種編程語言。其最有趣的特性之一是它通過使用 goroutines 來支持并發,這些 goroutines 就像輕量級的線程。Goroutines 比實際的線程要便宜得多,甚至每秒可以調度數百萬的 goroutines。NTH28資訊網——每日最新資訊28at.com

但 Go 是如何實現這一令人難以置信的壯舉的呢?它是如何提供這種能力的?讓我們看看 Golang 調度器在背后是如何工作的。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

二、前提條件

在我們深入探討之前,有一些前提條件我必須談論。如果你已經對它們非常熟悉,可以跳過它們。NTH28資訊網——每日最新資訊28at.com

1.系統調用

系統調用是向內核的接口。就像 web 服務為用戶暴露一個 REST API 接口一樣。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

系統調用為 Linux 內核提供了一個接口。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

為了更多地了解這些系統調用,讓我們寫一點代碼!你可能首先問的問題是,選擇哪種語言?NTH28資訊網——每日最新資訊28at.com

答案有點復雜,但讓我們先了解如何在 C 語言中做,然后再花一些時間思考如何在其他語言中執行相同的操作。NTH28資訊網——每日最新資訊28at.com

現在,讓我們嘗試 stat 系統調用。該調用非常簡單。它接受一個文件路徑并返回有關該文件的大量信息。現在,我們將打印返回的幾個值,即文件的所有者和文件的大小(以字節為單位)。NTH28資訊網——每日最新資訊28at.com

#include<stdio.h>#include<sys/stat.h>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;}

輸出是相當容易預測的,如下所示:NTH28資訊網——每日最新資訊28at.com

uid = 765filesize = 11

所有的語言在內部都調用相同的系統調用,因為你只能使用這些系統調用與內核進行交互。例如,看一些 NodeJS 代碼這里,你可以看到它是如何聲明文件路徑的。他們圍繞這些系統調用做了很多工作,為開發者提供了更簡單的接口,但在底層,它們使用內核提供的相同的系統調用。NTH28資訊網——每日最新資訊28at.com

2.線程

我相信大多數人在生活中某個時候都聽說過線程,但你真的知道它們是什么嗎?它們是如何工作的?NTH28資訊網——每日最新資訊28at.com

我會盡快為你解釋。NTH28資訊網——每日最新資訊28at.com

你可能已經讀到過有多個核心的處理器。這些核心中的每一個都像一個可以獨立工作的獨立處理單元。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

線程是基于這一點的抽象,我們可以在我們的程序中“創建”線程,并在每個線程上運行代碼,然后由內核調度在單個核心上運行。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

所以,在任何時候,單個核心都在運行一個執行線程。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

我們如何創建線程?通過系統調用!NTH28資訊網——每日最新資訊28at.com

那為什么不直接使用核心呢?為什么我們不直接寫new Core()而是通過new Thread()創建線程?因為在操作系統中可能有多個程序正在運行,每個程序可能都想并行執行代碼。由于核心數量有限,每個程序都必須知道有多少可用的核心,如果一個程序占用了所有的核心,它基本上可以完全阻塞操作系統。操作系統可能還需要執行比任何單個程序更重要或優先級更高的工作,所以需要一層抽象。NTH28資訊網——每日最新資訊28at.com

3.Goroutines

正如我上面解釋的,goroutines類似于輕量級的線程,它們可以并發運行并且可以在單獨的線程中運行。NTH28資訊網——每日最新資訊28at.com

在繼續之前,沒有真正需要了解Golang,但我認為至少在繼續之前,看一下一個非常簡單的goroutine的實現是有意義的。NTH28資訊網——每日最新資訊28at.com

package mainimport ( "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()}

輸出,你可能猜到了,是連續打印的單詞“HELLO”和“WORLD”。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

既然用go關鍵字標記的兩個函數調用都是并行運行的。NTH28資訊網——每日最新資訊28at.com

你的直覺可能會讓你認為這些 goroutines 只是并行運行在 CPU 上的單獨線程,并由操作系統管理,但這不是真的。盡管現在每次調用都可以是單個線程,但我們很快會發現這并不實際。NTH28資訊網——每日最新資訊28at.com

三、設計我們自己的調度器

讓我們討論 Go 調度器,就好像我們正在創建自己的調度器一樣。我知道這個任務可能看起來很艱巨,但本質上,我們有一些工作單元,比如說一些函數,我們需要將它們調度到有限的線程上。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

所以,為了更正式地描述這一點,函數是單一的工作單位。它是執行某些處理的代碼行的序列。現在,讓我們不要用像 async/await 這樣的東西來混淆自己,我們說一個函數只包含非常基本的代碼。也許像這樣,NTH28資訊網——每日最新資訊28at.com

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

它應該在線程上運行,這些線程在內部在處理器的核心上進行多路復用。核心是有限的,但線程可以是無限的。管理這些線程并在有限的CPU核心上運行它們是操作系統調度器的工作,所以我們不需要關心核心。我們可以簡單地將線程視為實現并行性的單位。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

我們確實有一套系統調用,我們可以運行它們在操作系統上創建線程。系統調用往往是昂貴的,所以我們需要確保我們不經常創建/刪除線程。NTH28資訊網——每日最新資訊28at.com

我們調度器的工作很簡單,將這些函數或 goroutines 運行在線程上。我們的調度器可以在任何時候被賦予新的函數/goroutines,它將必須在線程上調度它們。NTH28資訊網——每日最新資訊28at.com

在下一部分,讓我們繼續明確所有的需求。NTH28資訊網——每日最新資訊28at.com

需求

讓我們列出我們調度器中想要的一些優先事項,這與 Golang 所設置的優先級相似,這些將影響我們在設計調度器時所做的設計決策。NTH28資訊網——每日最新資訊28at.com

(1) 輕量級 goroutinesNTH28資訊網——每日最新資訊28at.com

我們希望我們的 goroutines 非常輕量級。我們希望能夠處理每秒調度的數百萬 goroutines。這意味著我們想要做的所有事情,如創建一個 goroutine 或在線程上切換一個 goroutine 必須非常快。NTH28資訊網——每日最新資訊28at.com

(2) 處理系統調用和 I/ONTH28資訊網——每日最新資訊28at.com

系統調用可能難以處理,但我們仍然希望為我們的函數提供能夠進行系統調用和執行 I/O 操作的能力。這意味著某個函數可以打開并讀取一個文件,例如。系統調用有點棘手,因為一旦開始了系統調用,我們就看不到它何時結束或需要多長時間。在某種意義上,我們不能再控制或透明地看到函數了。當我們開始設計我們的調度器時,這個問題會出現。NTH28資訊網——每日最新資訊28at.com

(3) 并行NTH28資訊網——每日最新資訊28at.com

我們當然希望同時運行多個 goroutines。記住,我們不需要擔心核心,我們只需考慮如何將這些函數多路復用到線程上。NTH28資訊網——每日最新資訊28at.com

(4) 公平NTH28資訊網——每日最新資訊28at.com

我們希望一個系統確保運行在其中的 goroutines 是公平的。這意味著一個 goroutine 不應該阻塞其他 goroutines 很長時間。公平可能有點難以客觀定義,但其思路是每個 goroutine 都應該在線程上獲得公平的執行時間。這似乎是合乎邏輯的,因為沒有定義某種優先級系統的要求,沒有理由給予任何這些函數優先權。NTH28資訊網——每日最新資訊28at.com

(5) 避免過度訂閱NTH28資訊網——每日最新資訊28at.com

重要的是要控制任何程序將使用的線程數。當確保我們不會無謂地獲取操作系統級別的線程并阻塞機器上運行的其他進程時,這可能會很有用。因此,我們應該嘗試限制我們使用的線程數量。如果我們超出這個限制,我們將過度訂閱,為了對系統上運行的其他進程公平,我們會認為這是一件很糟糕的事情,并盡量避免它。NTH28資訊網——每日最新資訊28at.com

四、第1部分:調度 Goroutines

1.1:1 映射

一個非常簡單的系統是我們為每個 goroutine 創建一個線程。這意味著我們在 goroutines 和線程之間有一個1:1的映射。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

因此,每當用戶嘗試創建一個 goroutine,我們的調度器創建一個線程,該線程開始執行 goroutine。NTH28資訊網——每日最新資訊28at.com

這導致了很多問題:NTH28資訊網——每日最新資訊28at.com

  • 我們的 goroutines 不再輕量級,創建一個線程需要一個系統調用,這可能需要任意的時間。而且,一個線程有它自己的堆棧、寄存器和其他變量。如果你想運行幾十個線程,這是可以的,但當你想每秒運行數百萬 goroutines 時,這是不切實際的。
  • 如果我們為每個 goroutine 創建一個線程,而我們的用戶可以創建他們想要的任何數量的 goroutines,那么我們就過度訂閱了操作系統資源。我們希望能夠控制我們使用的操作系統線程的數量。

2.M:N 映射

這里的邏輯步驟是使用 M:N 映射。這意味著M個 goroutines 需要映射到N個線程。因此,用戶可以創建他們想要的任意數量的 goroutines,但我們對一組有限的線程有控制權,并且我們將他們的 goroutines 調度到一個線程上一段時間。我們也可以暫停/恢復線程上運行的 goroutines。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

這樣做有點復雜,因為我們現在需要了解如何將一個 goroutine/function 映射到一個線程上。我們可能只在每個線程上運行一個函數很短的時間,所以我們需要做一些繁重的工作以確保我們得到正確的邏輯。NTH28資訊網——每日最新資訊28at.com

一個好的比喻可能是餐館。餐廳通常會有不同數量的侍者和廚師。侍者的數量取決于餐廳的客人或桌子的數量,而廚師的數量取決于訂單的數量和烹飪所需的時間。NTH28資訊網——每日最新資訊28at.com

在這個比喻中,我們可以把函數想象成侍者,線程想象成廚師。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

為每個侍者提供一個廚師并不真正有意義,因為也許一個廚師可以很快地烹飪食物,也許餐廳在高峰時間有很多客人,但在慢時,也許需要更少的侍者。所以為了正確管理這家餐廳,你需要某種系統來將M個侍者分配給N個廚師。這正是我們必須在調度器中做的事情!NTH28資訊網——每日最新資訊28at.com

為了進一步延續這個比喻,也許一個簡單的系統是有一個訂單隊列,每個訂單都有一個數字,廚師們一個接一個地選擇數字。這也將是我們調度器的起點!NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

3.一個基本的系統 — 全局運行隊列

讓我們首先設計一個基本的系統,并對其進行迭代。NTH28資訊網——每日最新資訊28at.com

我們設置一個全局的先入先出的運行隊列,其中包含需要運行的一組 goroutines。每個線程從這個隊列中選擇一個 goroutine 并執行它。當它執行完一個 goroutine 后,它再次選擇一個新的 goroutine 并繼續反復進行相同的過程。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

為了更好地理解,讓我們編寫一些基本的偽代碼,描述每個線程將執行的內容。NTH28資訊網——每日最新資訊28at.com

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();    }  }}

重要的是要記住,每一個線程都在并行地運行相同的代碼。NTH28資訊網——每日最新資訊28at.com

這個系統中的一個問題是,每個線程都在訪問相同的共享變量,即globalRunQueue。所以,一個線程,比如說 ThreadA,檢查隊列是否為空,但在它能夠從隊列中獲取 goroutine 之前,另一個線程訪問了隊列并選擇了 goroutine。NTH28資訊網——每日最新資訊28at.com

我們需要一種方法來確保任何時候只有一個線程可以訪問運行隊列。NTH28資訊網——每日最新資訊28at.com

4.互斥鎖

解決這個問題的一種方法是引入一個互斥鎖。互斥鎖只是一個鎖,每個線程在訪問隊列之前都必須獲取它。只有線程擁有互斥鎖時,它才能執行操作。完成后,它可以釋放互斥鎖,供其他線程獲取。NTH28資訊網——每日最新資訊28at.com

把它想象成一個鎖,確保只有一個線程可以訪問全局運行隊列。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

繼續我們的偽代碼:NTH28資訊網——每日最新資訊28at.com

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();  }}

現在,下一個問題是每個線程都必須等待獲取一個互斥鎖來對運行隊列執行任何操作。在未來,我們也可能增加暫停 goroutines、恢復 goroutines 等功能。如果所有操作都需要互斥鎖,那么它可能成為我們系統的瓶頸。NTH28資訊網——每日最新資訊28at.com

5.全局和本地運行隊列

為了解決單一互斥鎖的問題,我們為每個線程提供了它自己的本地運行隊列。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

每個 goroutine 在創建時開始在全局運行隊列中執行,但在某個時候由不同的系統分配給一個本地運行隊列。每個線程大多與它自己的本地運行隊列進行交互,選擇一個 goroutine 并執行它。這樣,我們把大部分工作轉移到了每個線程的本地運行隊列。由于本地運行隊列只被一個線程訪問,所以它甚至不需要互斥鎖。NTH28資訊網——每日最新資訊28at.com

我們現在不再需要互斥鎖,我們的代碼變得更簡單了。NTH28資訊網——每日最新資訊28at.com

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();    }  }}

我們需要明白的是,我們仍然有一個帶有互斥鎖的全局運行隊列,但我們可以大大減少對全局運行隊列的調用,因為一個單獨的線程主要是從其自己的本地運行隊列中取出 goroutines。它可能偶爾從全局運行隊列中獲取,比如當其自己的本地運行隊列為空時,但那是一個罕見的情況(如果你感興趣,這里是找到一個正在運行的 goroutine 執行的代碼,你可以清楚地看到,如果它在本地運行隊列中找不到一個 goroutine,它將會輪詢全局運行隊列)。NTH28資訊網——每日最新資訊28at.com

6.工作竊取

我們可以為我們的系統添加的另一個有趣的特性是“工作竊取”的概念。每當線程的本地運行隊列為空時,它可以嘗試從其他本地運行隊列中竊取工作。這也有助于平衡 goroutines 的負載。NTH28資訊網——每日最新資訊28at.com

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();      }    }  }}

在這個系統中,即使一個線程執行一個 goroutine 需要很長時間,它的本地運行隊列中的其他 goroutines 也不會被餓死。最終,另一個線程會“竊取”這些 goroutines 并執行它們。NTH28資訊網——每日最新資訊28at.com

7.處理系統調用

如我之前所提到的,系統調用可能有點難以處理,因為它們可能需要相當長的時間。當一個 goroutine 執行一個系統調用,比如從一個文件中讀取數據,它可能需要很長時間才能返回。我們甚至不知道它是否會返回,或者操作系統在幕后到底在做什么。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

在操作系統返回之前,該線程不會被賦予任何新的工作,基本上是處于睡眠狀態。我們可能會遇到一個情況,即分配給我們程序的所有線程都只是等待系統調用完成。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

我們如何解決這個問題呢?在進行系統調用之前,我們創建一個新的線程。由于我們是在編寫語言,以及編寫打開文件的函數,我們可以在打開文件之前簡單地添加一行來創建一個新線程。新線程創建后,當前線程進入睡眠狀態,直到系統調用完成。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

從技術上講,這并不是過度訂閱,因為當前線程正在休眠,因此不會占用操作系統的資源。NTH28資訊網——每日最新資訊28at.com

但這意味著我們可能有很多我們不使用的休眠線程。NTH28資訊網——每日最新資訊28at.com

再次說,這并不是過度訂閱,但它為我們帶來了一個不同的問題。由于我們為每個線程提供資源(本地運行隊列),如果我們有很多線程,我們將為每個線程分配內存。NTH28資訊網——每日最新資訊28at.com

此外,我們尚未深入探討的系統的其他部分也可能會出現一些問題,例如將 goroutines 分配給本地運行隊列的系統可能會將一個 goroutine 分配給一個當前被系統調用阻塞的線程。NTH28資訊網——每日最新資訊28at.com

此外,工作竊取也可能變得有點繁瑣。如果我們有很多線程,工作竊取將需要檢查很多本地運行隊列。NTH28資訊網——每日最新資訊28at.com

8.處理器

為了解決這個問題,我們增加了另一層抽象,稱為處理器。NTH28資訊網——每日最新資訊28at.com

每個線程都會獲取一個處理器,該處理器包含執行 Go 代碼所需的變量。所以我們將本地運行隊列以及我們可能擁有的任何其他變量(如緩存等)移動到處理器中。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

當一個線程在系統調用上被阻塞時,它將釋放處理器,另一個線程將獲取它并從中斷的地方繼續。NTH28資訊網——每日最新資訊28at.com

NTH28資訊網——每日最新資訊28at.com

這并不顯著地改變了我們的偽代碼,除了我們現在在使用本地運行隊列時使用了一個處理器。NTH28資訊網——每日最新資訊28at.com

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.搶占

為了確保沒有單一的 goroutine 長時間占用一個線程,我們可以添加一個非常簡單的機制,如果一個 goroutine 的執行時間超過了一定的時間段,比如說10ms,那么它會被預先暫停。然后我們將它加到運行隊列的尾部。NTH28資訊網——每日最新資訊28at.com

2.全局運行隊列饑餓

有一種可能性是,goroutines 不斷地在全局運行隊列中被填充,而每個處理器都繼續在其自己的本地運行隊列上工作。這可能導致全局運行隊列中的 goroutines 基本上餓死。NTH28資訊網——每日最新資訊28at.com

這個問題的簡單解決方案是什么呢?讓我們偶爾檢查全局運行隊列,而不是本地運行隊列。NTH28資訊網——每日最新資訊28at.com

其實在代碼中理解這一點非常容易。NTH28資訊網——每日最新資訊28at.com

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();      }    }  }}

六、結論

這是我嘗試簡要描述 Golang 并發是如何工作的。我可能做了一些簡化,但這大致是并發模型的方向。歡迎查看 Go 源代碼中的 findRunnable 函數的實際代碼這里,現在你應該能夠理解它的大部分內容。NTH28資訊網——每日最新資訊28at.com

我們確實跳過了一些概念,比如網絡輪詢器,但這篇文章已經變得非常長,我不想在這樣一個預計10-15分鐘的閱讀中塞入太多的信息。NTH28資訊網——每日最新資訊28at.com

我選擇這個主題的原因是,我一直認為并發和并行這樣的概念更偏理論而不是實際,我發現自己難以理解它內部是如何工作的。在我看來,重要的是要認識到,歸根結底,這些看似非常困難且像魔法一樣的底層概念只不過是代碼片段,理解它們以及導致它們的決策不應該比理解任何其他代碼更難。NTH28資訊網——每日最新資訊28at.com

本文鏈接:http://www.tebozhan.com/showinfo-26-14804-0.html理解 Go 調度器并探索其工作原理

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 什么是std::string_view:現代C++中的輕量級字符串引用?

下一篇: Java服務總在半夜掛,背后的真相竟然是...

標簽:
  • 熱門焦點
Top