go語言協程 併發

協程

目前比較流行的併發模式

  • 多進程 資源開銷最大 好處 進程間 互不影響。 系統開銷大 所有進程都是由內核管理的。
  • 多線程 多線程在大部分操作系統上面都屬於系統層面的併發模式。 比多進程的開銷小, 但是總體開銷大
  • 基於 回調的非阻塞 異步IO
  • 協程 本質是用戶態線程 不需要操作系統來進行搶佔式調度, 且是在真正 的實現中寄存於線程中。因此線程開銷很小, 有效的提高線程任務的併發性, 而避免多線程的缺點。

消息傳遞系統 :
對線程間共享的狀態的各種操作都被封裝在線程之間傳遞的消息中, 這通常要求: 發送消息時候對狀態進行復制, 並且在消息傳遞的邊界上交出這個狀態的所有權。
從邏輯看這個操作和共享內存系統中執行的原子操作相同, 這從物理上來看很不一樣。 由於需要執行復制操作, 所以大多數消息傳遞的實現在性能上並不 優越, 但是線程中的狀態管理工作通常會變得更爲簡單。

goroutine

go 在語言層面支持協程(輕量級線程) goroutine .
Go 語言標準庫提供的所有系統調用操作(包括所有的IO同步操作)都會出讓cpu 給其他的goroutine. 這讓輕量級的線程 的切換管理不 依賴於系統進程 也不依賴cpu 的核心數量。

goroutine 是go 語言中輕量級線程的實現, 由Go 運行時(runtime) 管理。

goroutine 不是線程, 他是對線程的多路複用。 所以協程的數量可以比線程數量多得多。一個goroutine 啓動時候只需要非常小的棧並且這個棧可以按需拓展和縮小(Go1.4 中 goroutine啓動棧的大小僅爲2kb)

當一個goroutine 被阻塞的時候, 他也會阻塞所複用操作系統線程,而運行時環境 runtime 則會把位於被阻塞線程上其他 goroutine 移動到其他未阻塞的線程上繼續運行。

代碼看看如何使用

package main

import (
	"fmt"
	"sync"
	"time"
)

func add(x, y int, wg *sync.WaitGroup) {	
	z := x + y
	fmt.Println(z)
}
func main() {
	for i := 0; i < 10; i++ {
		go add(i, i, &wg)
	}
}

運行之後什麼都沒有打印
程序從main 函數開始 當main()函數返回時候, 程序退出, 且程序並不等其他goroutine(非主goroutine) 結束。
上面代碼主函數啓動了是個 goroutine 飯後返回, 這是程序退出了, 而被啓動執行 Add()的goroutine沒來得及執行, 所以程序沒有任何輸出。

解決方法之一 就是前面文章提到的 sync.WatiGroup() 函數的使用 計數器

package main

import (
	"fmt"
	"sync"
	"time"
)

func add(x, y int, wg *sync.WaitGroup) {
	time.Sleep(5 * time.Second)
	z := x + y
	fmt.Println(z)
	wg.Done()
}
func main() {
	wg := sync.WaitGroup{}
	wg.Add(10)
	t1 := time.Now().Unix()
	for i := 0; i < 10; i++ {
		go add(i, i, &wg)
	}
	wg.Wait()
	t2 := time.Now().Unix()
	fmt.Println("sleep: ", t2 - t1)
}

t1 t2 的目的之一就是 檢驗 協程之間是不是相互影響的 打印是5 說明是並行執行的 互不影響

併發通信模型

在工程上最常見的併發通信模型:
共享數據 和消息
共享數據:
多個併發單元分別保持對同一數據的引用, 實現對該數據的共享。
被共享的數據可能有多種形式, 比如 內存數據塊, 磁盤文件, 網絡數據等。 在實際中最常用的是內存, 也就是通常說的共享內存。

消息機制
go 是以消息機制 而不非共享內存作爲通信方式
消息機制認爲每個併發單元都是自包含的, 獨立的個體 並且都有自己的變量,並在不同的併發單元間這些變量不共享。
每個併發單元的輸入和輸出只有一種, 那就是消息。 這點有點類似於進程, 每個進程不被別的進程打擾 他只做好自己的工作就好了。 不同進程間靠消息來通信, 他們不會共享內存。

Go 語言 提供的消息通信機制被稱爲channel

不要通過共享內存來通信 而應該通過通信來共享內存

channel

channel 是go 語言在語言級別提供的goroutine 間的通信方式
我們可以使用channel 在兩個或者多個goroutine 之間傳遞消息。 channel 是進程間的通信方式, 因此通過channel 傳遞對象的過程和調用函數時的參數傳遞行爲比較一致。比如也可以傳遞指針。 如果需要跨進程通信, 建議用分佈式的方式 比如使用 socket 或者http 等通訊協議

channel 是類型相關的 也就是說一個channel 只能傳遞一種類型的值, 這個類型需要再聲明channel 的時候指定。

channel 聲明

var chanName chan ElementType  ( var  ch chan int)
var m map[string] chan bool

定義channel

ch := make(chan int)

寫入:
將一個數據寫入(發送)至channel : ch <- value

像channel 寫入數據嚐嚐會導致程序阻塞, 直到其他的goroutine從這個channel中讀出數據

讀出:
value := <- ch

如果channel 之前沒有寫入數據, 那麼從channel 中讀取數據 也會得導致程序阻塞, 直到channel 中被寫入數據爲止。

select

通過調用 select() 函數來監控一系列的文件句柄,一旦其中一個文件句柄發生了IO 操作, 該select() 調用就會被返回。
這個機制現在被用來實現高併發的socket 服務器程序。 go語言在遠層面支持 select 關鍵字 用於處理異步io 問題

select 的用法 和switch 語言非常類似, 由select 開啓一個新的選擇塊 每個選擇條件由case 語句來描述。 但是select 有很多限制:
最大的限制就是每個case 語句後面必須有一個IO 操作

select {
	case <- chan1:
		// 如果chan1 成功讀取到數據 則處理case
	case chan2 <- 1
		// 如果chan2 成功寫入數據 則處理case
	default:
		// 如果上面都沒有成功 怎進入default 處理流程
}

緩衝機制

之前示範的都是 不帶緩衝的channel 這種做法對於傳遞單個數據的場景可以接受, 但是對於需要持續傳輸大量數據的場景就有些不合適

channel 帶上緩衝 達到消息隊列的效果

c := make(chan int, 1024)

在緩衝區還沒有填寫 完之前不會阻塞

從緩衝區 讀取數據 可以 使用range 關鍵字來讀取

for i := range c {
	fmt.Println("Received:", i)
}

超時機制

在併發機制中 最需要處理的問題就是 超時問題

在像channel 寫數據時 發現channel 已滿或者從channel 視圖讀取數據時候發現channel 爲空。 如果不正確處理這些情況很可能導致整個goroutine 鎖死

Go 語言沒有提供直接的超時處理機制, 可以利用select 機制 。select 的一個特點是隻要其中一個select已經完成, 程序就會繼續往下執行 而不會考慮其他case 的情況

timeout := make(chan bool, 1)
	go func() {
		time.Sleep(1e9) // 等待1秒鐘
		timeout <- true
}()
// 然後我們把timeout這個channel利用起來
select {
	case <-ch:
	// 從ch中讀取到數據
	case <-timeout:
	// 一直沒有從ch中讀取到數據,但從timeout中讀取到了數據
	}

這樣使用select 機制可以避免永久等待問題 因爲程序會在timeout中獲取一個數據後繼續執行無論對ch 的讀取 是否處在等待狀態 從而達成一秒超時的效果

這種寫法要被合理的利用起來 從而有效的提高代碼質量

channel 的傳遞

channel 可別傳遞

管道上是一種使用非常廣泛的一種設計模式, 比如再處理數據時候, 可以採用管道設計, 這樣就比較容易以插件的方式增加數據的處理流程。
可以利用可傳遞性來實現管道

type PipeData struct {
	value int
	handler func(int) int
	next chan int
}

流式處理數據

func handle(queue chan *PipeData) {
	for data := range queue {
		data.next <- data.handler(data.value)
	}
}

單向channel

不存在 真正的單向channel 單向的都是阻塞的更本沒有辦法使用的。

所謂的單向 是對channel 的使用限制
channel 支持被傳遞 還支持類型轉換

ch4 := make(chan int )
ch5 := <- chan int (ch4) // ch5 是單向的的讀取channel
ch6 := chan <- int(ch4) // ch6 是一個單向的寫入 channel

基於ch4 通過類型轉化初始化了兩個單向channel : 單向讀的ch5 和單向寫的ch6

單向channel 的聲明

var ch1 chan int // ch1 是一個正常的channel 不是單獨的
var ch2 chan <- float64 // ch2 是單向channel 只用於寫float64
var ch3 <-chan int // ch3 是單向channel 用用於讀取int 數據

目前 我不知道在什麼樣的場景使用

關閉channel

close(ch)

如何判斷一個通道是否關閉呢 ?

x, ok := <- ch

ok 爲false 表示已經關閉

同步

即使成功的使用channel 來通信手段還是避免不了多個goroutine之間數據共享問題, 這裏也有資源鎖方案

同步鎖

sync 包提供了兩種鎖類型:

  • sync.Mutex
  • sync.RWMutex

sync.Mutex 比較簡單但是比較暴力, 當一個goroutine 獲得了 mutex後其他的goroutine就只能乖乖的等到這個goroutine 釋放改mutex.
RWMutex 相對比較友好, 是經典的單寫多讀模型。在讀鎖佔用的情況下會阻塞寫 但是不會阻止讀, 也就是多個goroutine 可以同時獲得讀鎖(調用RLock; 而寫鎖(調用LOCK()方法)), 會阻止任何其他的goroutine(包括讀寫),

從 RWMutex 的實現看, RWMutex 類型其實組合了 Mutex:

type RWMutex struct {
	w Mutex
	writerSem uint32
	readerSem uint32
	readerCount int32
	readerWait int32
}

鎖的典型使用模型

var temp sync.Mutex

func foo(){
	temp.Lock()
	defer temp.Unlock()
}

全局唯一性操作

對於全局只需要運行一次的代碼, 比如全局初始化操作 go語言提供了一個once 類型來保證 全局的唯一性

var a string
var once sync.Once

func setup(){
	a = "hello world"	
}
func doprint(){
	once.Do(setup)
	print(a)
}
func twoprint(){
	go doprint()
	go doprint()
}

Once的DO() 方法可以保證在全局範圍內只調用指定函數一次(這裏值setup() 函數) 而且其他的goroutine 在調用此語句時候 將會先被阻塞, 直至全局唯一的once.Do() 調用結束之後才調用。 once.Do() 還是原子操作。

爲了更好的控制併發過程中的 原子性操作 sync 包還提供了一個 automic 子包, 它提供了對於一些基礎數據類型的原子操作函數 比如

func CompareAndSwapUint64(val *uint64, old, new uint64) (swapped bool)

這個函數提供了比較和交換兩個 uint64類型數據的操作。 這讓操作者無需再爲這樣的操作專門添加Lock 操作。

競爭條件

如果一個程序在執行時候依賴於特定的順序或時序, 但是又無法保證這種順序和時序, 此時就會存在競爭條件、

競爭條件的存在將導致程序的行爲變得飄忽不定而且難以預測。

競爭條件通常出現在哪些需要修改共享資源的併發程序當中。當有兩個 或者多個進程同時去修改一項共享資源的時候, 最先訪問資源的那個進程/ 線程將得到預期的結果, 而其他的進程或者線程則不然。 最終, 因爲程序無法判斷哪個進程或者線程先訪問了資源, 所以它將無法產生一致的行爲。

互斥技術:mutex , 該技術可以將同一時間內訪問臨界區的進程數量限制爲一個。

總結

  • 並行和併發是兩個相輔相成的概念, 但是他們並不相同。 併發指的是兩個或者多個任務在同一時間段內啓動, 運行結束 , 並且這些任務可能會互動, 而並行則是單純的同時運行多個任務。
  • Go web 服務器本身就是併發的, 服務器會把接受到的每條請求都放到獨立的goroutine裏運行。
  • go 通過goroutine 和通道這兩個重要特性直接支持併發。
  • goroutine 用於編寫併發程序, 而通道則是用於爲不同的goroutine 之間提供通信功能。
  • 無緩衝通道都是同步, 嘗試向一個已經包含數據的無緩衝通道推入新的數據會被阻塞; 但是, 有緩衝通道在被填滿之前都是異步的。
  • select 語句可以以先到先服務的方式, 從多個通道選出一個已經準備好執行接受操作的通道。
  • WaitGroup 同樣可以對多個通道進行同步
  • 併發程序的性能一般會比相應的非併發程序要高, 而具體提升多少則取決於所使用的算法
  • 在條件允許的情況下, 併發的web應用將自動的獲取並行帶來的優勢。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章