日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Go?并發編程協程及調度機制詳情_Golang

作者:宇宙之一粟 ? 更新時間: 2022-11-02 編程語言

前言:

協程(coroutine)是 Go 語言最大的特色之一,goroutine 的實現其實是通過協程。

協程的概念

協程一詞最早出現在 1963 年發表的論文中,該論文的作者為美國計算機科學家 Melvin E.Conway。著名的康威定律:“設計系統的架構受制于產生這些設計的組織的溝通結構。” 也是這個作者。

協程是一種用戶態的輕量級線程,可以想成一個線程里面可以有多個協程,而協程的調度完全由用戶控制,協程也會有自己的 registers、context、stack 等等,并且由協程的調度器來控制說目前由哪個協程執行,哪個協程要被 block 住。

而相對于 Thread 及 Process 的調度,則是由 CPU 內核去進行調度,因此操作系統其實會有所謂許多的調度算法,并且可以進行搶占式調度,可以主動搶奪執行的控制權。

反之,協程是不行的,只能進行非搶占式的調度。 可以理解成,如果 coroutine 被 block 住,則會在用戶態直接切換另外一個 coroutine 給此 thread 繼續執行,這樣其他 coroutine 就不會被 block 住,讓資源能夠有效的被利用,借此實現 Concurrent 的概念。

協程與線程

線程?是 CPU 調度的基本單位,多個線程可以通過共享進程的資源,通過共享內存等方式來進行線程間通信。

協程?可理解為輕量級線程,與線程相比,協程不受操作系統系統調度,協程調度由用戶應用程序提供,協程調度器按照調度策略把協程調度到線程中運行。

  • 協程只需花幾 KB 就可以被創立,線程則需要幾 MB 的內存才能創立
  • 切換開銷方面,協程遠遠低于線程,切換的速度也因此大幅提升

goroutine 的誕生

Golang 語言的 goroutine 其實就是協程,特別的是在語言層面直接原生支持創立協程,并在 runtime、系統調用等多方面對 goroutine 調度進行封裝及處理。

相對于 Java 的建立線程,操作系統是會直接建立一個線程與其對應,而多個線程的間互相切換需要通過內核線程來進行,會有較大的上下文切換開銷,造成的資源耗費,而 goroutine 是在代碼上直接實現切換,不需要經過內核線程。

goroutine 的優勢:

  • 與線程相比, goroutine 非常便宜,可以根據應用程序的需求自動分配, 但在線程的大小通常是固定的
  • 使用 goroutine 訪問共享內存的時候 透過?channel?可以避免競態條件的發生

比如,我們計算一個數字的質數,可以寫出如下的代碼:

package main

import (
	"fmt"
	"time"
)

func main() {
	num := 300000
	start := time.Now()
	for i := 1; i <= num; i++ {
		if isPrime(i) {
			fmt.Println(i)
		}
	}
	end := time.Now()
	fmt.Println(end.Unix()-start.Unix(), "seconds")
}
func isPrime(num int) bool {
	if num == 1 {
		return false
	} else if num == 2 {
		return true
	} else {
		for i := 2; i < num; i++ {
			if num%i == 0 {
				return false
			}
		}
		return true
	}
}

上面的代碼用?num := 300000?來測試,也就是從?1~300000 之間來看那些數字會是質數,如果是質數的話就把質數輸出,最后看到最終花費了 37 秒。運行結果如下:

使用 goroutine 加快速度

package main

import (
	"fmt"
	"time"
)

func main() {
	num := 300000
	start := time.Now()
	for i := 1; i <= num; i++ {
		go findPrimes(i)
	}

	end := time.Now()
	time.Sleep(5 * time.Second)
	fmt.Println(end.Unix()-start.Unix(), "seconds")
}

func findPrimes(num int) {
	if num == 1 {
		return
	} else if num == 2 {
		fmt.Println(num)
	} else {
		for i := 2; i < num; i++ {
			if num%i == 0 {
				return
			}
		}
		fmt.Println(num)
	}
}

go findPrimes?這條語句就可以開啟一個 goroutine,因此以主程序來說這樣等于是開啟 300000 個 goroutine 來各自判斷自己拿到 num 是不是質數這樣。

用?time. Sleep?來休息五秒來讓 main 主程序不要被關閉,否則由于開啟 goroutine 之后代碼會繼續往下執行,如果沒做 sleep 的話會導致主程序關閉,主程序一關閉 goroutine 就跟著關閉了,我們就看不出效果了。

這邊運行之后會發現輸出的質數出現并不是從小到大的,這是因為這些 goroutine 是一起做事情的,所以誰先做完誰就先輸出這樣。

運行結果如下,最后花費了大概 11 秒:

goroutine 的機制原理

理解 goroutine 機制的原理,關鍵是理解 Go 語言是如何實現調度器模型的。

計算機科學領域的任何問題都可以通過添加間接中間層來解決。GPM 模型就是這一理論的實踐者。

Go 語言中支撐整個調度器實現的主要有 4 個重要結構,分別是 machine(簡稱 M )、goroutine(簡稱 G )、processor(簡稱 P )、Scheduler(簡稱 Sched), 前三個定義在?runtime.h?中,Sched 定義在?proc.c?中。

  • Sched 結構就是調度器,它維護有存儲 M 和 G 的隊列以及調度器的一些狀態信息等
  • M 結構是 Machine,系統線程,它由操作系統管理和調度的,goroutine 就是跑在 M 之上的; M 是一個很大的結構,里面維護小對象內存 cache(mcache)、當前執行的 goroutine、隨機數發生器等等非常多的信息。
  • P 結構是 Processor,處理器,它的主要用途就是用來執行 goroutine 的,它維護了一個 goroutine 隊列,即 runqueue。 Processor 是讓我們從 N:1 調度到 M:N 調度的重要部分。
  • G 是 goroutine 實現的核心結構,它包含了棧,指令指針,以及其他對調度 goroutine 很重要的信息,例如其阻塞的 channel。

我們分別用三角形,矩形和圓形表示 Machine Processor 和 Goroutine:

在單核處理器的場景下,所有 goroutine 運行在同一個 M 系統線程中,每一個 M 系統線程維護一個 Processor,任何時刻,一個 Processor 中只有一個 goroutine,其他 goroutine 在 runqueue 中等待。

一個 goroutine 運行完自己的時間片后,讓出上下文,回到 runqueue 中。 多核處理器的場景下,為了運行 goroutines,每個 M 系統線程會持有一個 Processor 。

可以看到 Go 的并發用起來非常簡單,用了一個語法糖將內部復雜的實現結結實實的包裝了起來。

其內部可以用下面這張圖來概述:

在單核處理器的場景下,所有 goroutine 運行在同一個 M 系統線程中,每一個 M 系統線程維護一個 Processor,任何時刻,一個 Processor 中只有一個 goroutine,其他 goroutine 在 runqueue 中等待。一個 goroutine 運行完自己的時間片后,讓出上下文,回到 runqueue 中。 多核處理器的場景下,為了運行 goroutines,每個 M 系統線程會持有一個 Processor 。

在正常情況下,scheduler 會按照上面的流程進行調度,但是線程會發生阻塞等情況,看一下goroutine對線程阻塞等的處理。

原文鏈接:https://juejin.cn/post/7140291758201503774

欄目分類
最近更新