您好,登錄后才能下訂單哦!
這篇文章主要介紹了Golang中的并發性是什么的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Golang中的并發性是什么文章都會有所收獲,下面我們一起來看看吧。
并發是指在同一時間運行多個事物的能力。你的電腦有一個CPU。一個CPU有幾個線程。每個線程通常一次運行一個程序。當我們通常寫代碼時,這些代碼是按順序運行的,也就是說,每項工作都是背對背運行的。在并發代碼中,這些工作是由線程同時運行的。
一個很好的比喻是對一個家庭廚師的比喻。我還記得我第一次嘗試煮意大利面的時候。我按照菜譜一步步地做。我切了蔬菜,做了醬汁,然后煮了意大利面條,再把兩者混合起來。在這里,每一步都是按順序進行的,所以下一項工作必須等到當前工作完成后才能進行。
快進到現在,我在烹飪意大利面條方面變得更有經驗。我現在先開始做意大利面,然后在這期間進行醬汁的制作。烹飪時間幾乎減少到一半,因為烹飪意大利面條和醬汁是同時進行的。
并發性與并行性有些不同。并行性與并發性類似,即同時發生多項工作。然而,在并行性中,多個線程分別在進行不同的工作,而在并發性中,一個線程在不同的工作之間游走。
因此,并發性和并行性是兩個不同的概念。一個程序既可以并發地運行,也可以并行地運行。你的代碼可以按順序寫,也可以按并發寫。該代碼可以在單核機器或多核機器上運行。把并發性看作是你的代碼的一個特征,而把并行性看作是執行的一個特征。
Go使編寫并發代碼變得非常簡單。每個并發的工作都由一個goroutine來表示。你可以通過在函數調用前使用go關鍵字來啟動一個goroutine。看過《瑞克和莫蒂》嗎?想象一下,你的主函數是一個Rick,他把任務委托給goroutine Mortys。
讓我們從一個連續的代碼開始。
package main import ( "fmt" "time" ) func main() { simple() } func simple() { fmt.Println(time.Now(), "0") time.Sleep(time.Second) fmt.Println(time.Now(), "1") time.Sleep(time.Second) fmt.Println(time.Now(), "2") time.Sleep(time.Second) fmt.Println("done") }
2022-08-14 16:22:46.782569233 +0900 KST m=+0.000033220 0
2022-08-14 16:22:47.782728963 +0900 KST m=+1.000193014 1
2022-08-14 16:22:48.782996361 +0900 KST m=+2.000460404 2
done
上面的代碼打印出當前時間和一個字符串。每條打印語句的運行時間為一秒。總的來說,這段代碼大約需要三秒鐘的時間來完成。
現在讓我們把它與一個并發的代碼進行比較。
func main() { simpleConc() } func simpleConc() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } time.Sleep(time.Second) fmt.Println("done") }
2022-08-14 16:25:14.379416226 +0900 KST m=+0.000049175 2
2022-08-14 16:25:14.379446063 +0900 KST m=+0.000079012 0
2022-08-14 16:25:14.379450313 +0900 KST m=+0.000083272 1
done
上面的代碼啟動了三個goroutines,分別打印當前時間和i。這段代碼花了大約一秒鐘完成。這比順序版本快了三倍左右。
"等一下,"我聽到你問。"為什么要等整整一秒?難道我們不能刪除這一行以使程序盡可能快地運行嗎?"好問題!讓我們看看會發生什么。
func main() { simpleConcFail() } func simpleConcFail() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } fmt.Println("done") }
done
嗯......。程序確實在沒有任何慌亂的情況下退出了,但我們缺少來自goroutines的輸出。為什么它們被跳過?
這是因為在默認情況下,Go并不等待goroutine的完成。你知道main也是在goroutine里面運行的嗎?主程序通過調用simpleConcFail來啟動工作程序,但它在工作程序完成工作之前就退出了。
讓我們回到烹飪的比喻上。想象一下,你有三個廚師,他們分別負責烹飪醬料、意大利面和肉丸。現在,想象一下,如果戈登-拉姆齊命令廚師們做一盤意大利面條和肉丸子。這三位廚師將努力工作,烹制醬汁、意大利面條和肉丸。但是,在廚師們還沒有完成的時候,戈登就按了鈴,命令服務員上菜。很明顯,食物還沒有準備好,顧客只能得到一個空盤子。
這就是為什么我們在退出節目前等待一秒鐘。我們并不總是確定每項工作都會在一秒鐘內完成。有一個更好的方法來等待工作的完成,但我們首先需要學習另一個概念。
總結一下,我們學到了這些東西:
工作被委托給goroutines。
使用并發性可以提高你的性能。
主goroutine默認不等待工作goroutine完成。
我們需要一種方法來等待每個goroutine完成。
goroutines之間是如何交流的?當然是通過通道。通道的作用類似于門戶。你可以通過通道發送和接收數據。下面是你如何在Go中制作一個通道。
ch := make(chan int)
每個通道都是強類型的,并且只允許該類型的數據通過。讓我們看看我們如何使用這個。
func main() { unbufferedCh() } func unbufferedCh() { ch := make(chan int) go func() { ch <- 1 }() res := <-ch fmt.Println(res) }
1
很簡單,對嗎?我們做了一個名為ch的通道。我們有一個goroutine,向ch發送1,我們接收該數據并將其保存到res。
你問,為什么我們在這里需要一個goroutine?因為不這樣做會導致死鎖。
func main() { unbufferedChFail() } func unbufferedChFail() { ch := make(chan int) ch <- 1 res := <-ch fmt.Println(res) }
fatal error: all goroutines are asleep - deadlock!
我們碰到了一個新詞。什么是死鎖?死鎖就是你的程序被卡住了。為什么上面的代碼會卡在死鎖中?
為了理解這一點,我們需要知道通道的一個重要特性。我們創建了一個無緩沖的通道,這意味著在某一特定時間內沒有任何東西可以被存儲在其中。這意味著發送方和接收方都必須同時準備好,才能在通道上傳輸數據。
在失敗的例子中,發送和接收的動作依次發生。我們發送1到ch,但在那個時候沒有人接收數據。接收發生在稍后的一行,這意味著在接收行運行之前,1不能被發送。可悲的是,1不能先被發送,因為ch是沒有緩沖的,沒有空間來容納任何數據。
在這個工作例子中,發送和接收的動作同時發生。主函數啟動了goroutine,并試圖從ch中接收,此時goroutine正在向ch發送1。
另一種從通道接收而不發生死鎖的方法是先關閉通道。
func main() { unbufferedCh() } func unbufferedCh() { ch3 := make(chan int) close(ch3) res2 := <-ch3 fmt.Println(res2) }
0
關閉通道意味著不能再向它發送數據。我們仍然可以從該通道中接收它。對于未緩沖的通道,從一個關閉的通道接收將返回一個通道類型的零值。
總結一下,我們學到了這些東西:
通道是goroutines之間相互交流的方式。
你可以通過通道發送和接收數據。
通道是強類型的。
沒有緩沖的通道沒有空間來存儲數據,所以發送和接收必須同時進行。否則,你的代碼就會陷入死鎖。
一個封閉的通道將不接受任何數據。
從一個封閉的非緩沖通道接收數據將返回一個零值。
如果通道能保持數據一段時間,那不是很好嗎?這里就是緩沖通道發揮作用的地方。
Buffered channels, the portal that is somehow cylindrical?
緩沖通道是帶有緩沖器的通道。數據可以存儲在其中,所以發送和接收不需要同時進行。
func main() { bufferedCh() } func bufferedCh() { ch := make(chan int, 1) ch <- 1 res := <-ch fmt.Println(res) }
1
在這里,1被儲存在ch里面,直到我們收到它。
很明顯,我們不能向一個滿了緩沖區的通道發送更多的信息。你需要在緩沖區內有空間才能發送更多。
func main() { bufferedChFail() } func bufferedChFail() { ch := make(chan int, 1) ch <- 1 ch <- 2 res := <-ch fmt.Println(res) }
fatal error: all goroutines are asleep - deadlock!
你也不能從一個空的緩沖通道接收。
func main() { bufferedChFail2() } func bufferedChFail2() { ch := make(chan int, 1) ch <- 1 res := <-ch res2 := <-ch fmt.Println(res, res2) }
fatal error: all goroutines are asleep - deadlock!
如果一個通道已滿,發送操作將等待,直到有可用的空間。這在這段代碼中得到了證明。
func main() { bufferedCh3() } func bufferedCh3() { ch := make(chan int, 1) ch <- 1 go func() { ch <- 2 }() res := <-ch fmt.Println(res) }
1
我們接收一次是為了取出1,這樣goroutine就可以發送2到通道。我們沒有從ch接收兩次,所以只接收1。
我們也可以從封閉的緩沖通道接收。在這種情況下,我們可以在封閉的通道上設置范圍來迭代里面的剩余項目。
func main() { bufferedChRange() } func bufferedChRange() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch) for res := range ch { fmt.Println(res) } // you could also do this // fmt.Println(<-ch) // fmt.Println(<-ch) // fmt.Println(<-ch) }
1
2
3
在一個開放的通道上測距將永遠不會停止。這意味著在某些時候,通道將是空的,測距循環將試圖從一個空的通道接收,從而導致死鎖。
總結一下:
緩沖通道是有空間容納項目的通道。
發送和接收不一定要同時進行,與非緩沖通道不同。
向一個滿的通道發送和從一個空的通道接收將導致一個死鎖。
你可以在一個封閉的通道上進行迭代,以接收緩沖區內的剩余值。
等待戈多...我的意思是,goroutines來完成,使用通道
通道可以用來同步goroutines。還記得我告訴過你,在通過無緩沖通道傳輸數據之前,發送方和接收方必須都準備好了嗎?這意味著接收方將等待,直到發送方準備好。我們可以說,接收是阻斷的,意思是接收方將阻斷其他代碼的運行,直到它收到東西。讓我們用這個巧妙的技巧來同步我們的goroutines。
func main() { basicSyncing() } func basicSyncing() { done := make(chan struct{}) go func() { for i := 0; i < 5; i++ { fmt.Printf("%s worker %d start\n", fmt.Sprint(time.Now()), i) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) } close(done) }() <-done fmt.Println("exiting...") }
我們做了一個done通道,負責阻斷代碼,直到goroutine完成。done可以是任何類型,但struct{}經常被用于這些類型的通道。它的目的不是為了傳輸結構,所以它的類型并不重要。
一旦工作完成,worker goroutine 將關閉 done。此時,我們可以從 done 中接收,它將是一個空結構。接收動作解除了代碼的阻塞,使其可以退出。
這就是我們使用通道等待goroutine完成的方式。
關于“Golang中的并發性是什么”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Golang中的并發性是什么”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。