天天看點

16.1Go語言幹貨-并發程式設計之goroutine

1.并發與并行

并發:同一

時間段

執行多個任務

并行:同一

時間點

執行多個任務

  1. Go語言中的并發通過

    goroutine

    實作。
  2. goroutine

    屬于使用者态的線程(協程),支援千萬級的并發。
  3. goroutine

    是由Go語言的

    timerun

    排程完成的,線程是由作業系統排程完成的。
  4. Go語言中使用

    channel

    在多個

    goroutine

    間進行通訊。
  5. goroutine

    channel

    是Go語言秉承了CSP并發模式的基礎實作的。

2.goroutine

2.1 使用goroutine

在Go語言中使用goroutine非常簡單,隻需要在調用函數的時候在前面加上

go

關鍵字,就可以為一個函數建立有個

goroutine

一個

goroutine

必須對應一個函數,可以建立多個

goroutine

去執行相同的函數。

2.2 啟動單個

goroutine

啟動

goroutine

隻需要在調用的函數(匿名函數或者普通函數)前面加一個關鍵字

go

舉個栗子:

package main

import (
	"fmt"
	"time")


func nowTime() {
	nowtime := time.Now()
	fmt.Println(nowtime.Format("2006-01-02 15:04:06"))
}
func main() {
	go nowTime()
	fmt.Println("this is main function")
	// time.Sleep(10)
}

           

當我們不執行

time.Sleep()

時,結果隻列印了

this is main function

這是為什麼?

  1. 在程式啟動時,Go語言會為

    main()

    建立一個預設的

    goroutine

  2. main

    函數(主函數)執行完畢的時候該主函數的

    goroutine

    也就結束了。這時建立的分支函數,可能還沒有執行完畢也被強制結束了。
  3. 建立新的

    goroutine

    需要一些時間的。
  4. 是以加上一個

    time.Sleep()

    就可以等待分支函數執行完畢。

2.3 啟動多個

goroutine

啟用多個

goroutine

舉個栗子:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func f1(n int64) {
	fmt.Println(n)
	defer wg.Done()
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go f1(int64(i))
	}

	fmt.Println("main")
	wg.Wait()
}

           

2.4 sync.WaitGroup

第一個栗子我們使用的

time.Sleep()

用來阻塞主函數,第二個栗子使用的是

sync.WaitGroup

,二者由什麼差別呢?

使用time.Sleep()将這個阻塞給寫死了,我們不知道子函數執行需要多少時間,然後定死了一個時間。這個有很大的弊端,阻塞時間給多個浪費工作效率,阻塞時間給少了可能子函數還沒有執行完成。

Go語言提供了一個解決方案,

sync.WaitGroup

是一個結構體,在他的内部維護了一個計數器,使用

.Add(1)

時給計數器加1,使用

.Done()

給計數器減1。最後使用

.Wait()

來判斷計數器是否歸零。

方法名 功能
(wg * WaitGroup) Add(delta int) 計數器+delta
(wg *WaitGroup) Done() 計數器-1
(wg *WaitGroup) Wait() 阻塞直到計數器變為0

3.

goroutine

與線程

3.1 可增長的棧

作業系統一般都有固定的棧記憶體,通常為2MB,一個

goroutine

的棧在其生命周期開始時隻有很小的棧(2KB)。

goroutine

的棧不是固定的,可以增大也可以縮小。最大限制為1GB。

在Go語言中一次建立十萬左右的

goroutine

是可以的。

3.2

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排程方面的性能。

3.3 GOMAXPROCS

1.Go運作時的排程器使用GOMAXPROCS參數來确定需要使用多少個OS線程來同時執行Go代碼。

2.預設值是機器上的CPU核心數。例如在一個8核心的機器上,排程器會把Go代碼同時排程到8個OS線程上(GOMAXPROCS是m:n排程中的n)。

3.Go語言中可以通過runtime.GOMAXPROCS()函數設定目前程式并發時占用的CPU邏輯核心數。

4.Go1.5版本之前,預設使用的是單核心執行。

5.Go1.5版本之後,預設使用全部的CPU邏輯核心數。

我們可以通過将任務配置設定到不同的CPU邏輯核心上實作并行的效果

兩個任務隻有一個邏輯核心,此時是做完一個任務再做另一個任務。

舉個栗子

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func f1() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f1", i)
	}
}

func f2() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f2", i)
	}
}

func main() {
	wg.Add(2)
	runtime.GOMAXPROCS(1)
	go f1()
	go f2()
	fmt.Println("main")
	wg.Wait()
}
`
main
f2 0
f2 1
f2 2
f2 3
f2 4
f2 5
f2 6
f2 7
f2 8
f2 9
f1 0
f1 1
f1 2
f1 3
f1 4
f1 5
f1 6
f1 7
f1 8
f1 9
`
           

将邏輯核心數設為2,此時兩個任務并行執行,代碼如下。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func f1() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f1", i)
	}
}

func f2() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f2", i)
	}
}

func main() {
	wg.Add(2)
	runtime.GOMAXPROCS(12)
	go f1()
	go f2()
	fmt.Println("main")
	wg.Wait()
}

           

1.一個作業系統線程對應多個使用者态

goroutine

2.go程式可以同時使用多個作業系統線程

3.作業系統線程與

goroutine

是多對多關系,即

m:n

go