您好,登錄后才能下訂單哦!
這篇文章主要講解了“Golang并發編程怎么應用”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Golang并發編程怎么應用”吧!
并發編程是一個很大的主題,這里只提供一些特定于go的重點內容。
在許多環境中,實現對共享變量的正確訪問所需要的微妙之處使并發編程變得困難。Go鼓勵一種不同的方法,在這種方法中,共享值在通道中傳遞,實際上,從不由單獨的執行線程主動共享。在任何給定時間,只有一個goroutine可以訪問該值。根據設計,數據競爭是不可能發生的。為了鼓勵這種思維方式,我們把它簡化為一句口號:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通過共享內存進行通信;相反,通過通信共享內存。
這種方法可能走得太遠。例如,引用計數最好通過在整數變量周圍放置互斥來實現。但是作為一種高級方法,使用通道來控制訪問可以更容易地編寫清晰、正確的程序。
考慮這個模型的一種方法是考慮一個典型的單線程程序運行在一個CPU上。它不需要同步原語。現在運行另一個這樣的實例;它也不需要同步。現在讓這兩個程序通信;如果通信是同步器,則仍然不需要其他同步。例如,Unix管道就完美地符合這個模型。盡管Go的并發方法起源于Hoare的通信順序處理(communication Sequential Processes, CSP),但它也可以被視為Unix管道的類型安全的泛化。
它們之所以被稱為goroutine,是因為現有的術語——線程、協程、進程等等——傳達了不準確的含義。goroutine有一個簡單的模型:它是一個與相同地址空間中的其他goroutine并發執行的函數。它是輕量級的,比分配棧空間的成本高不了多少。而且棧開始時很小,所以它們很便宜,并通過根據需要分配(和釋放)堆存儲來增長。
goroutine被多路復用到多個操作系統線程上,因此如果一個線程阻塞,比如在等待I/O時,其他線程繼續運行。它們的設計隱藏了線程創建和管理的許多復雜性。
在函數或方法調用前加上go
關鍵字以在新的 goroutine 中運行該調用。當調用完成時,goroutine 將無聲地退出。(效果類似于Unix shell的&符號,用于在后臺運行命令。)
go list.Sort() // run list.Sort concurrently; don't wait for it.
function literal
在goroutine調用中很方便。
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // Note the parentheses - must call the function. }
在Go中,函數字面量( function literals )是閉包: 實現確保函數引用的變量只要處于活動狀態就能存活。
與map一樣,通道也使用make
進行分配,結果值作為對底層數據結構的引用。如果提供了可選的整數參數,它將設置通道的緩沖區大小。對于無緩沖通道或同步通道,默認值為0。
ci := make(chan int) // unbuffered channel of integers cj := make(chan int, 0) // unbuffered channel of integers cs := make(chan *os.File, 100) // buffered channel of pointers to Files
無緩沖通道將通信(值的交換)與同步結合起來,確保兩個計算(gorout例程)處于已知狀態。
有很多使用通道的好習語。這是一個開始。在前一節中,我們在后臺啟動了排序。通道可以允許啟動goroutine等待排序完成。
c := make(chan int) // Allocate a channel. // Start the sort in a goroutine; when it completes, signal on the channel. go func() { list.Sort() c <- 1 // Send a signal; value does not matter. }() doSomethingForAWhile() <-c // Wait for sort to finish; discard sent value.
接收者總是阻塞,直到有數據接收。如果通道無緩沖,發送方將阻塞,直到接收方接收到該值。如果通道有緩沖區,發送方只阻塞直到值被復制到緩沖區;如果緩沖區已滿,這意味著需要等待到某個接收器接收到一個值。 (參考3.1)
有緩沖通道可以像信號量(semaphore)一樣使用,例如限制吞吐量。在本例中,傳入的請求被傳遞給handle, handle將一個值發送到通道中,處理請求,然后從通道接收一個值,以便為下一個使用者準備“信號量”。通道緩沖區的容量限制了要處理的同時調用的數量。
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // Wait for active queue to drain. process(r) // May take a long time. <-sem // Done; enable next request to run. } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // Don't wait for handle to finish. } }
一旦MaxOutstanding處理程序正在執行進程,試圖向已充滿的通道緩沖區發送的請求都將阻塞,直到現有的一個處理程序完成并從緩沖區接收。
但是,這種設計有一個問題:Serve
為每個傳入的請求創建一個新的goroutine ,盡管在任何時候, 只有MaxOutstanding
多個可以運行。因此,如果請求來得太快,程序可能會消耗無限的資源。我們可以通過更改Serve
來限制goroutines的創建來解決這個缺陷。這里有一個明顯的解決方案,但要注意它有一個bug,我們隨后會修復:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) // Buggy; see explanation below. <-sem }() } }
bug 在于,在Go for
循環中,循環變量在每次迭代中都被重用,因此req
變量在所有goroutine中共享。這不是我們想要的。我們需要確保每個goroutine的req
是唯一的。這里有一種方法,在goroutine中將req
的值作為參數傳遞給閉包:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <-sem }(req) } }
將此版本與前一個版本進行比較,查看閉包的聲明和運行方式的差異。另一個解決方案是創建一個同名的新變量,如下例所示:
func Serve(queue chan *Request) { for req := range queue { req := req // Create new instance of req for the goroutine. sem <- 1 go func() { process(req) <-sem }() } }
這樣寫似乎有些奇怪
req := req
但在Go 中這樣做是合法的和慣用的。您將得到一個具有相同名稱的新變量,故意在局部掩蓋循環變量,但對每個goroutine都是惟一的。
回到編寫服務器的一般問題,另一種很好地管理資源的方法是啟動固定數量的handle
goroutines ,所有這些handle
goroutines 都從請求通道讀取。goroutine的數量限制了process
同時調用的數量。這個Serve
函數還接受一個通道,它將被告知退出該通道;在啟動goroutines之后,它會阻止從該通道接收。
func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *Request, quit chan bool) { // Start handlers for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // Wait to be told to exit. }
Go語言中的channel具有以下幾個特性:
線程安全
channel是線程安全的,多個協程可以同時讀寫一個channel,而不會發生數據競爭的問題。這是因為Go語言中的channel內部實現了鎖機制,保證了多個協程之間對channel的訪問是安全的。
阻塞式發送和接收
當一個協程向一個channel發送數據時,如果channel已經滿了,發送操作會被阻塞,直到有其他協程從channel中取走了數據。同樣地,當一個協程從一個channel中接收數據時,如果channel中沒有數據可供接收,接收操作會被阻塞,直到有其他協程向channel中發送了數據。這種阻塞式的機制可以保證協程之間的同步和通信。
順序性
通過channel發送的數據是按照發送的順序進行排列的。也就是說,如果協程A先向channel中發送了數據x,而協程B再向channel中發送了數據y,那么從channel中接收數據時,先接收到的一定是x,后接收到的一定是y。
可以關閉
通過關閉channel可以通知其他協程這個channel已經不再使用了。關閉一個channel之后,其他協程仍然可以從中接收數據,但是不能再向其中發送數據了。關閉channel的操作可以避免內存泄漏等問題。
緩沖區大小
channel可以帶有一個緩沖區,用于存儲一定量的數據。如果緩沖區已經滿了,發送操作會被阻塞,直到有其他協程從channel中取走了數據;如果緩沖區已經空了,接收操作會被阻塞,直到有其他協程向channel中發送了數據。緩沖區的大小可以在創建channel時指定,例如:
ch := make(chan int, 10)
會panic的幾種情況
1.向已經關閉的channel發送數據
2.關閉已經關閉的channel
3.關閉未初始化的nil channel
會阻塞的情況:
1.從未初始化 nil
channel中讀數據
2.向未初始化 nil
channel中發數據
3.在沒有讀取的groutine時,向無緩沖channel發數據,
有緩沖區,但緩沖區已滿,發送數據時
4.在沒有數據時,從無緩沖或者有緩沖channel讀數據
返回零值:
從已經關閉的channe接收數據
在使用channel時,應該遵循以下幾個最佳實踐:
避免死鎖
使用channel時應該注意避免死鎖的問題。如果一個協程向一個channel發送數據,但是沒有其他協程從channel中取走數據,那么發送操作就會一直被阻塞,從而導致死鎖。為了避免這種情況,可以使用select語句來同時監聽多個channel,從而避免阻塞。
避免泄漏
在使用channel時應該注意避免內存泄漏的問題。如果一個channel沒有被關閉,而不再使用了,那么其中的數據就無法被釋放,從而導致內存泄漏。為了避免這種情況,可以在協程結束時關閉channel。
避免競爭
在使用channel時應該注意避免數據競爭的問題。如果多個協程同時讀寫一個channel,那么就可能會發生競爭條件,從而導致數據不一致的問題。為了避免這種情況,可以使用鎖機制或者使用單向channel來限制協程的訪問權限。
避免過度使用
在使用channel時應該注意避免過度使用的問題。如果一個程序中使用了大量的channel,那么就可能會導致程序的性能下降。為了避免這種情況,可以使用其他的并發編程機制,例如鎖、條件變量等。
Go最重要的屬性之一是通道是first-class值,可以像其他值一樣分配和傳遞。此屬性的常見用途是實現安全的并行多路解復用。
在上一節的示例中,handle
是請求的理想處理程序,但我們沒有定義它處理的類型。如果該類型包含要在其上回復的通道,則每個客戶機都可以為應答提供自己的路徑。下面是Request
類型的示意圖定義。
type Request struct { args []int f func([]int) int resultChan chan int }
客戶端提供了一個函數及其參數,以及請求對象內用于接收answer的通道。
func sum(a []int) (s int) { for _, v := range a { s += v } return } request := &Request{[]int{3, 4, 5}, sum, make(chan int)} // Send request clientRequests <- request // Wait for response. fmt.Printf("answer: %d\n", <-request.resultChan)
在服務器端,唯一需要更改的是處理程序函數。
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
顯然,要實現它還有很多工作要做,但這段代碼是一個速率受限、并行、非阻塞RPC系統的框架,而且還沒有看到mutex 。
這些思想的另一個應用是跨多個CPU核并行計算。如果計算可以被分解成可以獨立執行的獨立部分,那么它就可以被并行化,并在每個部分完成時用一個通道發出信號。
假設我們有一個昂貴的操作要對一個items的向量執行,并且每個item的操作值是獨立的,就像在這個理想的例子中一樣。
type Vector []float64 // Apply the operation to v[i], v[i+1] ... up to v[n-1]. func (v Vector) DoSome(i, n int, u Vector, c chan int) { for ; i < n; i++ { v[i] += u.Op(v[i]) } c <- 1 // signal that this piece is done }
我們在一個循環中獨立地啟動這些片段,每個CPU一個。它們可以按任何順序完成,但這沒有關系;我們只是在啟動所有的goroutine之后通過排泄通道來計算完成信號。
const numCPU = 4 // number of CPU cores func (v Vector) DoAll(u Vector) { c := make(chan int, numCPU) // Buffering optional but sensible. for i := 0; i < numCPU; i++ { go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c) } // Drain the channel. for i := 0; i < numCPU; i++ { <-c // wait for one task to complete } // All done. }
我們不需要為numCPU
創建一個常量,而是可以詢問運行時哪個值是合適的。函數runtime.NumCPU
返回機器中硬件CPU核數,因此我們可以這樣寫
還有一個函數 runtime.GOMAXPROCS
,它報告(或設置)用戶指定的Go程序可以同時運行的核數。默認值為runtime.NumCPU
,但可以通過設置類似命名的shell環境變量或調用帶有正數的函數來覆蓋。用0調用它只是查詢值。因此,如果我們想要滿足用戶的資源請求,我們應該寫
var numCPU = runtime.GOMAXPROCS(0)
請務必不要混淆并發性(concurrency,將程序構造為獨立執行的組件)和并行性(parallelism, 在多個cpu上并行執行計算以提高效率)這兩個概念。盡管Go的并發特性可以使一些問題很容易構建為并行計算,但Go是一種并發語言,而不是并行語言,并且并不是所有的并行化問題都適合Go的模型。關于區別的討論,請參閱本文章中引用的談話。
并發編程的工具甚至可以使非并發的想法更容易表達。下面是一個從RPC包中抽象出來的示例。客戶端goroutine循環從某個源(可能是網絡)接收數據。為了避免分配和釋放緩沖區,它保留了一個空閑列表,并使用緩沖通道來表示它。如果通道為空,則分配一個新的緩沖區。一旦消息緩沖區準備好了,它就被發送到serverChan
上的服務器。
var freeList = make(chan *Buffer, 100) var serverChan = make(chan *Buffer) func client() { for { var b *Buffer // Grab a buffer if available; allocate if not. select { case b = <-freeList: // Got one; nothing more to do. default: // None free, so allocate a new one. b = new(Buffer) } load(b) // Read next message from the net. serverChan <- b // Send to server. } }
服務器循環從客戶端接收每條消息,處理它,并將緩沖區返回到空閑列表。
func server() { for { b := <-serverChan // Wait for work. process(b) // Reuse buffer if there's room. select { case freeList <- b: // Buffer on free list; nothing more to do. default: // Free list full, just carry on. } } }
客戶端嘗試從freeList
中檢索緩沖區;如果沒有可用的,則分配一個新的。服務器發送給freeList的消息會將b放回空閑列表中,除非空閑列表已滿,在這種情況下,緩沖區將被丟棄在地板上,由垃圾收集器回收。(當沒有其他case
可用時,select
語句中的default
子句將執行,這意味著select
語句永遠不會阻塞。)此實現僅用幾行就構建了一個漏桶列表,依賴于緩沖通道和垃圾收集器進行記賬。
感謝各位的閱讀,以上就是“Golang并發編程怎么應用”的內容了,經過本文的學習后,相信大家對Golang并發編程怎么應用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。