iT邦幫忙

2022 iThome 鐵人賽

DAY 30
0
Software Development

30天學會Golang系列 第 30

Day30 - Go的 channel day17 的還債 (下)

  • 分享至 

  • xImage
  •  

channel 還債

今天就是要來正面處理這個問題:

有一個 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 的債

第30天報到,感覺...終於變強了一點,最後再做個總整理

代碼連結

https://github.com/luckyuho/ithome30-golang/tree/main/day30


上一篇
Day29 - Go的 channel day17 的還債 (上)
下一篇
Day31 - Go的 30 天回顧
系列文
30天學會Golang31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言