今天就是要來正面處理這個問題:
有一個 10 * 10 的 array1,裡面的資料結構如下
array[0] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
array[1] = [1, 1, 2, 3, 4, 5, 6, 7, 8, 9]
array[2] = [2, 1, 2, 3, 4, 5, 6, 7, 8, 9]
然後開 10 個 channel,等到每個 channel 的資料蒐集完後
對這些資料統一都加 1,存至新的 array2 中,此時array2 的順序是混亂
這樣才能確保 goroutine 有發揮到功能
根據昨天的做法,我們先來看一個最直覺的處理方式:
package main
import "fmt"
// 創建 2 維矩陣
func create2DimArray(m, n int) [][]int {
arr := make([][]int, m)
for i := 0; i < m; i++ {
arr[i] = make([]int, n)
// 裡面塞值,來確認是哪一個 row 的資料
for j := 0; j < n; j++ {
if j == 0 {
arr[i][j] = i
} else {
arr[i][j] = j
}
}
}
return arr
}
// 顯示 2 維陣列的內容物
func show2DimArray(arr [][]int) {
for i := range arr {
for j := range arr[i] {
fmt.Printf("%d ", arr[i][j])
}
fmt.Println()
}
}
// 對 1 個 row 裡面的每個值 +1
func add1Row(arr1Dim []int, c chan int) {
for _, v := range arr1Dim {
c <- v + 1 // 每個值 +1
}
}
// 對 1 個 row 裡面的每個值 +1,且輸出
func add1RowAndShow(arr [][]int, m, n int) {
var c [10]chan int // 這邊必須是靜態的,必須給定 const 值,不能用 m 取代
// 創建 2 維且長度為 10 的通道
for i := range c {
c[i] = make(chan int, n)
}
// 併發讀取與寫入
for i := 0; i < m; i++ {
go add1Row(arr[i], c[i])
}
// 無法併發讀取,因為 c[i] 限制了他的順序
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
fmt.Printf("%d ", <-c[i])
}
fmt.Println()
}
}
func main() {
m, n := 10, 10
arr := create2DimArray(m, n)
showTitle("=== 顯示 2 維 arr 內容物 ===")
show2DimArray(arr)
showTitle("=== goroutine 對 2 維每筆資料 +1 與 輸出結果 ===")
add1RowAndShow(arr, m, n)
}
func showTitle(s string) {
fmt.Println(s)
}
輸出結果為:
=== 顯示 2 維 arr 內容物 ===
0 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
2 1 2 3 4 5 6 7 8 9
3 1 2 3 4 5 6 7 8 9
4 1 2 3 4 5 6 7 8 9
5 1 2 3 4 5 6 7 8 9
6 1 2 3 4 5 6 7 8 9
7 1 2 3 4 5 6 7 8 9
8 1 2 3 4 5 6 7 8 9
9 1 2 3 4 5 6 7 8 9
=== goroutine 對 2 維每筆資料 +1 與 輸出結果 ===
1 2 3 4 5 6 7 8 9 10
2 2 3 4 5 6 7 8 9 10
3 2 3 4 5 6 7 8 9 10
4 2 3 4 5 6 7 8 9 10
5 2 3 4 5 6 7 8 9 10
6 2 3 4 5 6 7 8 9 10
7 2 3 4 5 6 7 8 9 10
8 2 3 4 5 6 7 8 9 10
9 2 3 4 5 6 7 8 9 10
10 2 3 4 5 6 7 8 9 10
可以看到可以正常顯示,但是仔細看會發現結果是有順序性的,這是為什麼呢?原因是 func add1RowAndShow(arr [][]int, m, n int) 中的下面這一段:
// 無法併發讀取,因為 c[i] 限制了他的順序
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
fmt.Printf("%d ", <-c[i])
}
fmt.Println()
}
為何可以確認呢?以概念來說只要觸發到 goroutine 的函式就會被排到執行序,而上面提到的那段就是放在 main 裡面等待執行緒結束的方法,那我們來一步一步排查與驗證,首先我們先看看是否讀取方面是否是順序性的,檢查的方式我們可以稍微修改一下 func add1Row(arr1Dim []int, c chan int)
// 對 1 個 row 裡面的每個值 +1
//func add1Row(arr1Dim []int, c chan int) {
// for i, v := range arr1Dim {
// 查看併發順序
if i == 0 {
fmt.Println("併發讀取的值", v)
}
// c <- v + 1 // 每個值 +1
// }
//}
輸出結果為:
併發讀取的值 9
併發讀取的值 3
併發讀取的值 1
併發讀取的值 2
併發讀取的值 6
併發讀取的值 5
併發讀取的值 7
併發讀取的值 8
併發讀取的值 0
併發讀取的值 4
如此一來就可以確認讀取的時候確實是併發的形式,那問題來了,我們該怎麼讓最後的結果輸出的時候也是併發的形式呢?其實我相信大家看到這邊應該也有答案了,沒錯,就是把輸出也改成 goroutine,那改的形式如下:
// 對 1 個 row 裡面的每個值 +1,且輸出
func add1RowAndShow(arr [][]int, m, n int) {
var c [10]chan int // 這邊必須是靜態的,必須給定 const 值,不能用 m 取代
// 創建 2 維且長度為 10 的通道
for i := range c {
c[i] = make(chan int, n)
}
// 併發讀取與寫入
for i := 0; i < m; i++ {
go add1Row(arr[i], c[i])
}
// 併發輸出,錯誤形式
for i := 0; i < m; i++ {
go showChanInfoBad(n, c[i])
}
}
// 為了併發顯示的方式 (錯)
func showChanInfoBad(n int, c chan int) {
for j := 0; j < n; j++ {
fmt.Printf("%d ", <-c)
}
fmt.Println()
}
輸出結果為:
=== 顯示 2 維 arr 內容物 ===
0 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
2 1 2 3 4 5 6 7 8 9
3 1 2 3 4 5 6 7 8 9
4 1 2 3 4 5 6 7 8 9
5 1 2 3 4 5 6 7 8 9
6 1 2 3 4 5 6 7 8 9
7 1 2 3 4 5 6 7 8 9
8 1 2 3 4 5 6 7 8 9
9 1 2 3 4 5 6 7 8 9
=== goroutine 對 2 維每筆資料 +1 與 輸出結果 ===
然後...就沒有然後了,原因是輸出的部分也變成併發後,併發的執行序還沒開始工作,主線程就已經結束了,所以甚至連讀取都還沒,就已經結束了。
那我們該怎麼告訴主線程我們的執行序還有東西要跑呢?這時候就是要有一個在主線程的 channel 負責告訴主線程我們併發的程式碼什麼時候結束,相當於 waitgroup 中 done 的角色,如果把它想成生活的例子,就是對講機講完後可以在訊息的最後加上一個 over,讓對方知道我們講完了,那作法如下:
// 對 1 個 row 裡面的每個值 +1,且輸出
func add1RowAndShow(arr [][]int, m, n int) {
var c [10]chan int // 這邊必須是靜態的,必須給定 const 值,不能用 m 取代
// 創建 2 維且長度為 10 的通道
for i := range c {
c[i] = make(chan int, n)
}
// 併發讀取與寫入
for i := 0; i < m; i++ {
go add1Row(arr[i], c[i])
}
// 既然上面的方法會導致有順序性
// 那我們讓 print 方法也變成併發的形式
over := make(chan int, n)
for i := 0; i < m; i++ {
go showChanInfo(n, c[i], over)
}
for i := 0; i < m; i++ {
<-over
}
}
// 為了併發顯示的方式 (對)
func showChanInfo(n int, c, over chan int) {
for j := 0; j < n; j++ {
fmt.Printf("%d ", <-c)
}
fmt.Println()
over <- 0
}
我們就可以看到下面的結果為:
=== 顯示 2 維 arr 內容物 ===
0 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
2 1 2 3 4 5 6 7 8 9
3 1 2 3 4 5 6 7 8 9
4 1 2 3 4 5 6 7 8 9
5 1 2 3 4 5 6 7 8 9
6 1 2 3 4 5 6 7 8 9
7 1 2 3 4 5 6 7 8 9
8 1 2 3 4 5 6 7 8 9
9 1 2 3 4 5 6 7 8 9
=== goroutine 對 2 維每筆資料 +1 與 輸出結果 ===
併發讀取的值 4
併發讀取的值 5
併發讀取的值 6
併發讀取的值 7
併發讀取的值 8
併發讀取的值 9
10 2 3 4 5 6 7 8 9 10
5 2 3 4 5 6 7 8 9 10
6 2 3 4 5 6 7 8 9 10
7 2 3 4 5 6 7 8 9 10
8 2 3 4 5 6 7 8 9 10
9 2 3 4 5 6 7 8 9 10
併發讀取的值 2
3 2 3 4 5 6 7 8 9 10
併發讀取的值 3
併發讀取的值 1
4 2 3 4 5 6 7 8 9 10
2 併發讀取的值 0
1 2 3 4 5 6 7 8 9 10
2 3 4 5 6 7 8 9 10
可以看到我們的結果是邊讀邊輸出,最後也終於還清了 day17 的債
https://github.com/luckyuho/ithome30-golang/tree/main/day30