進程:程序啓動時(好比qq),操做系統位程序開啓一個進程。能夠把它看作是操做系統進行資源分配和調度的一個容器,裏面包含了該應用程序用到的全部資源。算法
線程:是一個獨立的執行空間,用來被系統調度來運行程序代碼。好比我下載文件,操做系統調度會安排到合適的cpu上進行執行,而且不必定是該程序進程所在的cpu。這個調度咱們不用關心。編程
也就是一個進程能夠有好多個線程,線程用來執行具體的任務。每一個進程的初始線程叫作主線程,因此進程至少有一個線程。安全
上圖,系統的調度器來調度線程在合適的cpu上運行。網絡
併發:多線程或多協程在一核cpu上運行就是所謂的併發,都是cpu經過切換時間片給人一種併發的感受。多線程
並行:是真正意義上的併發,就是多核cpu同時去處理多個線程,互不干擾,並行處理。併發
併發不是並行:並行是讓不一樣的代碼片斷同時在不一樣的cpu上執行,利用多核的優點。併發是經過很是快切換時間片來實現「同時」運行。並行的關鍵是同時作不少的事情,而併發是指同時管理不少事情,這些事情可能只作了一半就暫停作別的任務。函數
總結:併發優於並行,能夠有效利用資源。go語言能夠利用多核,經過goroutine 高效併發。atom
go語言經過一個叫作goroutine的東西進行併發執行,每個goroutine就是一個獨立的工做單元,能夠執行咱們的程序代碼。goroutine是相似協程(coroutine)的東西,能夠理解爲一個更輕量的線程。goroutine的具體使用後面再講,目前咱們來看一下go語言是如何經過goroutine實現併發的。spa
目前能夠理解爲:goroutine是個執行代碼的獨立工做單元,須要將它放到合適的線程和cpu上進行執行。這就須要go語言的邏輯處理器和調度器。操作系統
go語言中支撐整個scheduler實現的主要有4個重要結構,分別是M、G、P、Sched。
這裏借用一張圖來看下:
注意:go1.5以後爲每一個cpu建立一個邏輯處理器。
咱們從一個goroutine被建立到goroutine被執行的過程來看一下go是如何實現調度的。
(1)建立一個goroutine,它會放在全局運行隊列中,等待調度器調度
(2)調度器將這個goroutine 分配給一個邏輯處理器A,將它放到了這個邏輯處理器的本地隊列中,這個goroutine就會等待邏輯處理器A執行它
(3)每一個邏輯處理器默認綁定了一個線程,它是在線程中去執行本身本地隊列中的goroutine。
(4)邏輯處理器和原來的線程分離,調度器從新建立一個線程和這個邏輯處理器綁定。這時候邏輯處理器在新的線程上繼續執行本地運行隊列的其餘goroutine。 同時,阻塞的goroutine隨着線程分離,從本地隊列移除。
(5)那個阻塞的goroutine和分離的線程會繼續阻塞,等待系統調用的返回。一旦執行完成並返回,這個goroutine就會從新放回到原來邏輯處理器的本地隊列。
(6)以前的線程目前沒有goroutine了,可是它會被保存,以備以後使用。
調度器對能夠建立的邏輯處理器的數量沒有限制,但語言運行時默認限制每一個程序最多建立 10 000 個線程。這個
限制值能夠經過調用 runtime/debug 包的 SetMaxThreads 方法來更改。
小結:
概念 | 說明 |
---|---|
進程 | 一個程序對應的資源容器 |
線程 | 一個獨立的執行空間,一個進程能夠有多個線程 |
goroutine | 一樣是獨立的執行空間,可是一個線程能夠有多個goroutine |
邏輯處理器 | 綁定一個線程,運行goroutine |
調度器 | 將goroutine分配到合適的邏輯處理器 |
全局運行隊列 | 全部剛建立的goroutine都在這 |
本地運行隊列 | 邏輯處理器的goroutine隊列 |
因此,咱們能夠利用多核cpu,調度器建立多個邏輯處理器,而後每一個邏輯處理器能夠綁定一個線程去運行多個goroutine。這樣咱們就充分利用了多核資源實現併發處理,比單純的多線程更加優秀,高效,省資源。
注意:上面第(4)(5)步,若是goroutine 是在執行網絡io的操做,這個goroutine就不必定就回到這個邏輯處理器了。它實際上會先從邏輯處理器分離,移到集成了網絡輪詢器的運行時 ,一旦該輪詢器指示某個網絡讀或者寫操做已經就緒,對應的 goroutine 就會從新分配到邏輯處理器上來完成操做 。
看到這咱們重溫下併發和並行:
go語言實現併發,建立多個goroutine,調度器會將goroutine分配到邏輯處理器的本地運行隊列,邏輯處理器去運行goroutine。若是隻有一個邏輯處理器,只會實現併發,不會實現並行。
要實現並行,就須要多個邏輯處理器,在不一樣的cpu上,而後調度器會平等的將goroutine分配到每一個邏輯處理器,這樣多個線程多個goroutine就實現了並行和併發。 至於這些算法怎麼調度,咱們根本不須要關心,咱們只要記住goroutine是咱們進行併發編程的一個獨立單元就能夠了。
goroutine實際上是官方實現的超級「輕量線程池」。每一個實例4~5kb的佔內存佔用,更加輕量。只需在函數調⽤語句前添加 go 關鍵字,就可建立併發執⾏單元。開發⼈員⽆需瞭解任何執⾏細節,調度器會⾃動將其安排到合適的系統線程上執⾏。
package main import ( "fmt" "time" ) func main() { //經過go 關鍵字 +匿名函數就能夠開啓一個goroutine go func() { fmt.Println("Hello, World!") }() //因爲main函數也是一個goroutine,若是不讓線程等待,那麼main方法執行完,就退出了,還來不及打印helloworld time.Sleep(1 * time.Second) }
WaitGroup可以一直等到全部的goroutine執行完成,而且阻塞主線程的執行,直到全部的goroutine執行完成。
WaitGroup總共有三個方法:Add(delta int),Done(),Wait()。簡單的說一下這三個方法的做用。
方法名 | 說明 |
---|---|
Add | 添加或者減小等待goroutine的數量; |
Done | 至關於Add(-1),減小一個須要等待的goroutine數量 |
Wait | 進行等待,需等待的goroutine數量爲0 |
WaitGroup用於線程同步,WaitGroup等待一組線程集合完成,纔會繼續向下執行。 主線程(goroutine)調用Add來設置等待的線程(goroutine)數量。 而後每一個線程(goroutine)運行,並在完成後調用Done。 同時,Wait用來阻塞,直到全部線程(goroutine)完成纔會向下執行。
package main import ( "fmt" "runtime" "sync" ) func main() { runtime.GOMAXPROCS(1)//只使用1個物理處理器 var wg sync.WaitGroup wg.Add(2) //添加須要等待goroutine數量 fmt.Println("開啓兩個goroutine") go func() { defer wg.Done()//函數結束時通知main函數執行完畢 for i:=0;i<1000;i++{ fmt.Println("A:",i) } }() go func() { defer wg.Done()//函數結束時通知main函數執行完畢 for i := 0; i < 1000; i++ { fmt.Println("B:",i) } }() fmt.Println("等待goroutine運行") wg.Wait() fmt.Println("程序結束") }
從上面的代碼咱們能夠看到,咱們用sync包下的waitgroup 來進行線程等待,避免main函數執行完來不及執行goroutine就退出的狀況。waitgroup詳情下面會講。咱們用go關鍵字+匿名函數 開啓了兩個goroutine,來併發的去打印,可是由於這個1000個打印速度太快了,還沒來得及切換goroutine就第一個就已經打印完了,因此你可能會輸出,A打印完了纔打印B這種順序輸出。咱們能夠增長一下打印時間,就能看到他們是併發打印的了。好比加個time.Sleep(time.Millisecond) 。 這段代碼中有一個runtime.GOMAXPROCS(1),這是go語言能夠指定程序運行使用的cpu核數,咱們能夠設置多個,來實現並行。
通常來講,經過runtime.GOMAXPROCS(runtime.NumCPU())
能夠設置本機邏輯CPU的數量,不是物理CPU,好比一個雙核CPU,帶有超線程技術,則會被認爲是4個邏輯CPU。 runtime.Gosched () 可讓出底層線程,讓其餘goroutine 使用,runtime.Goexit 將當即終止當前goroutine 執行
runtime 小結:
runtime.GOMAXPROCS() //設置使用的邏輯處理器數量 runtime.NumCPU() //本地邏輯cpu的數量 runtime.Gosched() // 將當前goroutine的線程讓給別的goroutine,本身進入運行隊列等待 runtime.Goexit() //當即終止當前goroutine 運行 runtime.GOROOT() //獲取go的根目錄 runtime.GOOS // 獲取操做系統信息
sync.WaitGroup 類型的變量是一個值類型,若是在函數間進行傳遞,是值傳遞,這樣執行Done()和 wait()方法就不是同一個 WaitGroup了,就會出現死鎖,因此傳遞時必須傳遞指針。 代碼以下:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(wg *sync.WaitGroup, i int) { fmt.Printf("i=>%d\n", i) wg.Done() }(&wg, i) //這裏要傳指針就對了 } wg.Wait() }
若是兩個或者多個 goroutine 在沒有互相同步的狀況下,訪問某個共享的資源,並試圖同時讀和寫這個資源,就處於相互競爭的狀態,這種狀況被稱做競爭狀態(race condition)。 咱們要作的是:同一時刻只能有一個 goroutine 對共享資源進行讀和寫操做 。
package main import ( "fmt" "runtime" "sync" ) var ( count int wg sync.WaitGroup ) func main(){ wg.Add(2) //開啓兩個goroutine go incCount(1) go incCount(2) wg.Wait() fmt.Println("最終結果:",count) } //執行兩次 count++ func incCount(id int) { defer wg.Done() for i:=0;i<2;i++{ value:=count runtime.Gosched() value++ count=value } } //輸出 最終結果: 2
從上面能夠看出,咱們開啓兩個goroutine,每一個goroutine,都執行了兩個value++並賦值給count,也就是說最終的結果應該是4,可是如今確是2。 毫無疑問,在對count 進行讀寫的時候,兩個goroutine進行了資源競爭,而且沒有同步。
程序運行就像圖中所示,兩個goroutine在進行切換的時候,並無同步count的數量,而且他們相互覆蓋了對方,致使各自有通常的工做白作了。
go run -race goDemo.go// -race go自帶的競爭監測命令,能夠查看哪一行哪些方法有資源競爭。
================== WARNING: DATA RACE Read at 0x0000005fa2d0 by goroutine 7: main.incCount() D:/gopath/src/awesomeProject/goroutine/godemo.go:23 +0x76 Previous write at 0x0000005fa2d0 by goroutine 6: main.incCount() D:/gopath/src/awesomeProject/goroutine/godemo.go:26 +0x97 Goroutine 7 (running) created at: main.main() D:/gopath/src/awesomeProject/goroutine/godemo.go:16 +0x90 Goroutine 6 (finished) created at: main.main() D:/gopath/src/awesomeProject/goroutine/godemo.go:15 +0x6f ================== 最終結果: 4 Found 1 data race(s) exit status 66
如何解決資源競爭和線程同步,這就有兩類,一類是傳統的方式——加鎖,另外一類是go語言有的經過chanel,採用csp模型,即經過通訊去共享內存,而不是經過共享內存而通訊。
咱們要實現同一時間只能有一個goroutine對共享資源進行讀寫操做,go語言提供了傳統的解決方案,atomic和sync 包。 另外一種方式是使用channel,下一篇單獨講。
atomic 包提供了一些函數來保證對資源的讀寫安全。好比LoadInt32 和 StoreInt32兩個函數,一個讀取int32類型的值,一個寫入int32類型的值。還有AddInt32()同步整型加法等,以下:
package main import ( "fmt" "runtime" "sync" "sync/atomic" ) var ( count int32 wg sync.WaitGroup ) func main(){ wg.Add(2) go incCount(1) go incCount(2) wg.Wait() fmt.Println("最終結果:",count) } func incCount(id int) { defer wg.Done() for i:=0;i<2;i++{ // 安全地對 count 加 1 atomic.AddInt32(&count, 1) runtime.Gosched() } }
這時候執行結果是4,讀寫安全了。atomic雖然能夠解決資源競爭問題,可是比較都是比較簡單的,支持的數據類型也有限。因此,sync 提供了互斥鎖來解決。
sync包裏提供了一種互斥型的鎖,可讓咱們本身靈活的控制哪些代碼,同時只能有一個goroutine訪問,被sync互斥鎖控制的這段代碼範圍,被稱之爲臨界區,臨界區的代碼,同一時間,只能有一個goroutine訪問。代碼以下:
package main import ( "fmt" "runtime" "sync" ) var ( count int32 wg sync.WaitGroup mutex sync.Mutex //聲明 mutex 互斥鎖變量 ) func main() { wg.Add(2) //2個等待的goroutine go incCount() go incCount() wg.Wait() fmt.Println(count) } func incCount() { defer wg.Done() for i := 0; i < 2; i++ { mutex.Lock() //臨界區開始位置 value := count runtime.Gosched() value++ count = value mutex.Unlock()//臨界區結束位置 } }
咱們仍是使用 sync.WaitGroup 來進行等待兩個goroutine都執行完再推出main函數。 重點看咱們還聲明瞭一個
mutext sync.Mutex
這個互斥鎖,經過mutex.Lock()
加鎖, mutex.Unlock()
解鎖。它將中間的代碼塊造成一個臨界區,因此,這段代碼塊同時只能有一個goroutine進行操做,因此,goroutine1 將count賦值給value,讓出線程,此時goroutine2也沒法進入臨界區的代碼,等待goroutine1 執行完臨界區的代碼,goroutine2再進行執行。這樣就保證了資源的讀寫安全。
固然goroutine 同步還有更好,更簡單的方式,使用channel。即所謂的:經過通訊來共享內存,而不是經過共享內存來通訊。