Go:goroutine使用、調度、runtime包

目錄

併發編程前言

進程和線程

併發和並行

協程和線程

Goroutine

使用goroutine

啓動多個goroutine

goroutine與線程

可增長的棧

goroutine調度

runtime包

runtime.Gosched()

runtime.Goexit()

runtime.GOMAXPROCS


併發編程前言

進程和線程

    A. 進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。
    B. 線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
    C.一個進程可以創建和撤銷多個線程;同一個進程中的多個線程之間可以併發執行。

併發和並行

    A. 多線程程序在一個核的cpu上運行,就是併發。
    B. 多線程程序在多個核的cpu上運行,就是並行。

協程和線程

協程​​​​​
協程
協程
  • 協程:獨立的棧空間,共享堆空間,調度由用戶自己控制,本質上有點類似於用戶級線程,這些用戶級線程的調度也是自己實現的。
  • 線程:一個線程上可以跑多個協程,協程是輕量級的線程。
  • goroutine 只是由官方實現的超級"線程池"。
  • 每個實力4~5KB的棧內存佔用和由於實現機制而大幅減少的創建和銷燬開銷是go高併發的根本原因
  • 併發不是並行:
    • 併發主要由切換時間片來實現"同時"運行,並行則是直接利用多核實現多線程的運行,go可以設置使用核數,以發揮多核計算機的能力。
    • goroutine 奉行通過通信來共享內存,而不是共享內存來通信

Goroutine

goroutine

Go語言中的goroutine就是這樣一種機制,goroutine的概念類似於線程,但 goroutine是由Go的運行時(runtime)調度和管理的。Go程序會智能地將 goroutine 中的任務合理地分配給每個CPU。Go語言之所以被稱爲現代化的編程語言,就是因爲它在語言層面已經內置了調度和上下文切換的機制。

在Go語言編程中你不需要去自己寫進程、線程、協程,你的技能包裏只有一個技能–goroutine,當你需要讓某個任務併發執行的時候,你只需要把這個任務包裝成一個函數,開啓一個goroutine去執行這個函數就可以了,就是這麼簡單粗暴。

Go程序會爲main()函數創建一個默認的goroutine

goroutine

使用goroutine

Go語言中使用goroutine非常簡單,只需要在調用函數的時候在前面加上go關鍵字,就可以爲一個函數創建一個goroutine。

一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。

啓動多個goroutine

goroutine切換點

在Go語言中實現併發就是這樣簡單,我們還可以啓動多個goroutine。讓我們再來一個例子: (這裏使用了sync.WaitGroup來實現goroutine的同步)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine結束就登記-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {

    for i := 0; i < 10; i++ {
        wg.Add(1) // 啓動一個goroutine就登記+1
        go hello(i)
    }
    wg.Wait() // 等待所有登記的goroutine都結束
}

多次執行上面的代碼,會發現每次打印的數字的順序都不一致。這是因爲10個goroutine是併發執行的,而goroutine的調度是隨機的。

注意

  • 如果主協程退出了,其他任務還執行嗎(運行下面的代碼測試一下吧)
package main

import (
	"fmt"
	"time"
)

func main() {
	// 合起來寫
	go func() {
		i := 0
		for {
			i++
			fmt.Printf("new goroutine: i = %d\n", i)
			time.Sleep(time.Second)
		}
	}()
	i := 0
	for {
		i++
		fmt.Printf("main goroutine: i = %d\n", i)
		time.Sleep(time.Second)
		if i == 2 {
			break
		}
	}
}

// 返回結果
//main goroutine: i = 1
//new goroutine: i = 1
//new goroutine: i = 2
//main goroutine: i = 2

goroutine與線程

可增長的棧

OS線程(操作系統線程)一般都有固定的棧內存(通常爲2MB),一個goroutine的棧在其生命週期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。

goroutine調度

GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別於操作系統調度OS線程。

  • 1.G很好理解,就是個goroutine的,裏面除了存放本goroutine信息外 還有與所在P的綁定等信息
  • 2.P管理着一組goroutine隊列,P裏面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當自己的隊列消費完了就去全局隊列裏取,如果全局隊列裏也消費完了會去其他P的隊列裏搶任務。
  • 3.M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關係, 一個groutine最終是要放到M上執行的;

P與M一般也是一一對應的。他們關係是: P管理着一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認爲其已經死掉時 回收舊的M。

P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之後默認爲物理線程數。 在併發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。

單從線程調度講,Go語言相比起其他語言的優勢在於OS線程是由OS內核來調度的,goroutine則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱爲m:n調度的技術(複用/調度m個goroutine到n個OS線程)。 其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護着一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。

runtime包

runtime.Gosched()

讓出CPU時間片,重新等待安排任務

package main

import (
	"fmt"
	"runtime"
)

func main() {
	go func(s string) {
		for i := 0; i < 2; i++ {
			fmt.Println(s)
		}
	}("world")
	// 主協程
	for i := 0; i < 2; i++ {
		// 切一下,再次分配任務
		runtime.Gosched()
		fmt.Println("hello")
	}
}

// 返回結果
//world
//world
//hello
//hello

runtime.Goexit()

退出當前協程

package main

import (
	"fmt"
	"runtime"
)

func main() {
	go func() {
		defer fmt.Println("A.defer")
		func() {
			defer fmt.Println("B.defer")
			// 結束協程
			runtime.Goexit()
			defer fmt.Println("C.defer")
			fmt.Println("B")
		}()
		fmt.Println("A")
	}()
	select {}
}

// 返回結果
//B.defer
//A.defer
//fatal error: all goroutines are asleep - deadlock!
//
//goroutine 1 [select (no cases)]:
//main.main()

runtime.GOMAXPROCS

  • Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼默認值是機器上的CPU核心數。例如在一個8核心的機器上,調度器會把Go代碼同時調度到8個OS線程上(GOMAXPROCS是m:n調度中的n)。
  • Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序併發時佔用的CPU邏輯核心數。
  • Go1.5版本之前,默認使用的是單核心執行。Go1.5版本之後,默認使用全部的CPU邏輯核心數。

我們可以通過將任務分配到不同的CPU邏輯核心上實現並行的效果,這裏舉個例子:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}

// 返回結果
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9

兩個任務只有一個邏輯核心,此時是做完一個任務再做另一個任務。 將邏輯核心數設爲2,此時兩個任務並行執行,代碼如下。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(2)
	go a()
	go b()
	time.Sleep(time.Second)
}

// 返回結果
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//
//Process finished with exit code 0

Go語言中的操作系統線程和goroutine的關係:

  • 1.一個操作系統線程對應用戶態多個goroutine。

  • 2.go程序可以同時使用多個操作系統線程。

  • 3.goroutine和OS線程是多對多的關係,即m:n。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章