go併發研究

基礎概述

go語言的併發同步模型來自叫作通訊順序進程的範型(Communicating Sequential Processes,CSP),經過goroutine 之間傳遞數據而不是對數據加鎖進行同步訪問java

Do not communicate by sharing memory; instead, share memory by communicating.算法

普通的線程併發模型,就是像Java、C++、或者Python,他們線程間通訊都是經過共享內存的方式來進行的。很是典型的方式就是,在訪問共享數據(例如數組、Map、或者某個結構體或對象)的時候,經過鎖來訪問,所以,在不少時候,衍生出一種方便操做的數據結構,叫作「線程安全的數據結構」。例如Java提供的包"java.util.concurrent"中的數據結構。Go中也實現了傳統的線程併發模型。數組

進程和線程

運行一個程序的時候,就會開啓一個進程,這個進程就至關於包括了這個程序全部資源集合的容器安全

進程資源包括但不限於如下3塊內容 內存,句柄,線程bash

具體概覽以下 數據結構

線程調度

操做系統調度器會決定哪一個線程會得到CPU的運行,調度CPU時間片的分配 單核CPU系統下,超線程運行的話,CPU同一時間只會進行一個線程的運行,因此多線程會致使CPU不停地進行切換,多核CPU就能夠意味着並行多線程

若是程序是CPU密集型,則併發並不會提升性能,甚至由於屢次建立線程致使效率更低,可是若是是IO密集型,則併發操做可讓線程再等待io的時候執行其餘邏輯來提升程序效率併發

線程調度的幾個方式函數

  1. 搶佔式調度

能夠設置線程的優先級,優先級高的能夠先佔用CPU性能

  1. 分時調度

各個線程輪流進行CPU的使用權,且平均分配CPU的時間片

goroutine特色

--- example 01 ---
func main() {
	runtime.GOMAXPROCS(1)

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		for count := 0; count < 3; count++ {
			for char := 'a'; char < 'a'+26; char++ {
				fmt.Printf("%c", char)
			}
		}
	}()
	go func() {
		defer wg.Done()
		for count := 0; count < 3; count++ {
			for char := 'A'; char < 'A'+26; char++ {
				fmt.Printf("%c", char)
			}
		}
	}()

	wg.Wait()
}

goroutine 執行順序
會是:ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
複製代碼

解析: 多個goroutine 至關於一個個待執行的G任務,調度器在處理G任務的時候每每會將最後的G 提到next待處理的任務中,因此最後的G任務會最早進行,其餘的就按照順序進行,可是會存在P空閒的時候

基於調度器的內部算法,一個正在運行的goroutine在工做結束前,能夠被中止並從新調度,調度器會中止當前正運行的goroutine,並給其餘可運行的goroutine運行的機會

以下

func main() {
	runtime.GOMAXPROCS(1)
	wg.Add(2)
	go printPrime("A")
	go printPrime("B")
	wg.Wait()
}

func printPrime(prefix string) {
	defer wg.Done()

next:
	for outer := 2; outer < 5000; outer++ {
		for inner := 2; inner < outer; inner++ {
			if outer%inner == 0 {
				continue next
			}
		}
		fmt.Printf("%s :%d\n", prefix, outer)
	}
}

複製代碼

會發現 AB 兩個goroutine 會交替計算

線程實現模型

M   表明內核線程
P   表明一個執行GO代碼片斷的必須資源(上下文環境)
G   表明一個Go代碼片斷
複製代碼
M
一個M表明一個內核線程,建立一個M 都是由於沒有足夠的M 來關聯P並運行其中可運行的G
在垃圾回收的時候也會致使M的建立,
建立後,系統會對他進行一番初始化,初始化自身的棧空間以及信號處理,在起始函數執行完畢後,當前M會與之預聯的P完成關聯。
單個M的最大數量是10000 能夠經過修改setmaxThreads 參數來調整最大數量
複製代碼
P
經過runtime.GOMAXPROCS 能夠設定P的最大數量,目前上限值是256,設定這個值以後,系統會重整全局P列表,該列表包含了當前運行時系統建立的全部P,與M相似,系統會有一個空閒的P列表,當P不與任何一個M關聯是時,就會將其放進空閒P列表中(固然這時候的G隊列必須爲空)
P除了一個可運行的G列表以外,還有一個自由的G列表,這個列表存儲的是已完成的G任務,爲了提升複用性,啓用1個G的時候,會先去自由G列表中獲取一個現成的G,只有自由列表中獲取不到的時候,纔會從新申請一個新的G
複製代碼
G
GO編譯器會將go語句變成對內部函數newproc的調用,並將參數傳遞給這個函數,與P M 相同,運行時也持有一個G的全局列表,初始化流程包括關聯go函數以及設置G的狀態跟id
會當即放入P的runnext 字段中, 這個字段存放優先級最高的G,若是runnext已經有了值,則
會放入起可運行隊列中
複製代碼
核心元素的容器
包括全局,空閒M,P,G列表
實現和操縱Go的線程實現模型的內部程序籠統稱爲 運行時系統,能夠稱爲調度器,一個Go程序只會存在一個調度器實例
複製代碼

調度器

兩級線程模型一部分調度惹怒會有操做系統內核以外的程序承擔,這就是調度器

主goroutine的運做

源碼目錄:src/runtime/proc.go

  1. 設定每個goroutine 的棧空間最大尺寸,32位位250MB 64位爲1GB
  2. 主goroutine 會在當前M的g0上執行系統檢測任務
  3. 鎖住主線程
  4. 檢查當前M是否爲runtime.m0,不是的話拋出異常
  5. 開啓gc goroutine (gcenable)
  6. 執行main包的init函數
  7. 解鎖主線程
  8. 執行main函數
  9. 若是有競爭檢測的話,則初始化競爭檢測
  10. 當其餘協程有panicdefer 的話,重試屢次確保panicdefer爲0是才退出,此時須要執行go park函數,gopark函數用於協程切換

競爭狀態

若是兩個或者多個goroutine訪問某個共享的資源,並試圖同時讀寫該資源,這種狀況就成爲競爭狀態,咱們隊一個共享資源的讀寫必須是原子化的,也就是同一時刻只能有一個goroutine對共享資源進行讀與寫操做

func main() {
	wg1.Add(2)
	go initCounter(1)
	go initCounter(2)

	wg1.Wait()
	fmt.Println("final counter:", counter)
}

func initCounter(id int) {
	defer wg1.Done()
	for count := 0; count < 2; count++ {
		value := counter
		runtime.Gosched()
		value++

		counter = value

		fmt.Println("get id is", id)
	}
}
複製代碼

會發現 最終counter 的值在2-4之間來回返回,就是由於2個goroutine對同一個變量進行賦值,每一個goroutine 都會覆蓋另外一個goroutine 的工做,細節以下:

goroutine A 對 counter 變量賦值完,將其保存在本身的副本中,另外一個goroutine 執行時,不會對剛剛A改變後的counter 值繼續操做,而是從新開始執行,這樣就會致使 goroutineB 覆蓋了A的操做,致使最終的counter值不肯定

runtime.Gosched()函數是用來強制將goroutine 衝當前線程中退出,給其餘goroutine處理

咱們可使用競爭檢測來發現代碼中是否存在這樣的問題

go build -race list09.go
./list09

如圖能夠看出,代碼中存在競爭問題

如何鎖住共享資源

處理方式:對共享資源加鎖,go包中 atomic以及sync包都提供了不少這樣的解決方案

atomic:

原子操做即執行過程當中不能被中斷的操做,原子操做僅會由一個獨立的CPU指令表明和完成,
只有這樣才能在併發環境保證原子操做的絕對安全

mutex 是由操做系統決定的,而atomic是由底層硬件決定的,CPU指令集中,有一些指令是封裝進atomic的,
這些指令在執行過程當中是不能被中斷的因此能保證併發安全
複製代碼

mutex:

互斥鎖在代碼中建立一個臨界區,保證同一時間只有一個goroutine 能夠執行這個臨界區代碼
複製代碼
func main() {
	wg2.Add(2)
	go incCounter(1)
	go incCounter(2)
	wg2.Wait()
	fmt.Printf("final counter %d\n", counter1)
}

func incCounter(id int) {
	defer wg2.Done()

	for count := 0; count < 2; count++ {
		mutex.Lock()
		value := counter1
		runtime.Gosched()
		value++
		counter1 = value

		mutex.Unlock()
	}
	fmt.Println("id is ", id)
}
使用競爭檢測後 ,也沒有問題
複製代碼

通道

當一個資源在goroutine中共享時,通道在goroutine 之間架起管道,提供確保同步交換數據的機制, 申明時,須要指定將要被共享的數據類型

無緩衝

func main() {
	//申明無緩衝通道
	court := make(chan int)

	wg3.Add(2)

	go player("james", court)
	go player("lina", court)

	court <- 1

	wg3.Wait()
}

func player(playerName string, court chan int) {
	defer wg3.Done()
	for {
		ball, ok := <-court
		if !ok {
			fmt.Printf("Player %s win\n", playerName)
			return
		}
		n := rand.Intn(100)
		if n%12 == 0 {
			fmt.Printf("Player %s miss\n", playerName)
			close(court)
			return
		}

		fmt.Printf("Player %s Hit %d\n", playerName, ball)
		ball++
		court <- ball
	}
}

複製代碼

有緩衝

當通道關閉後,goroutine 依舊能夠從通道中接收數據,但不能從通道中發送數據,從一個已關閉而且沒有數據的通道
裏獲取數據,總會當即返回,一個通道類型的零值
複製代碼
相關文章
相關標籤/搜索