iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 18
1
Software Development

啥物碗Golang? 30天就Go系列 第 18

Goroutine

經過了十七天的努力,這個字眼常常在我找資料的時候出現,今天就來一探究竟,到底「Goroutine」是個什麼東西?

如果用簡單的譬喻去形容的話,Goroutine就像是golang裡面的Thread(執行緒),可以讓程式一次不只做一件事情,而可以做好多件事。

執行緒常常被使用在效能優化上,因為現在CPU普遍都效能不錯,單一執行緒有點太浪費寶貴的運算空間。比方說一個郵差要發十萬封信一定比五個郵差每人發兩萬封信來得慢,郵差好比執行緒,負責執行我們所寫的程式。但也不是越多執行緒越好,比方說如果十萬個執行緒每條發一封,可能反而一下子卡死在那邊動彈不得。合適的執行緒數量可以透過類似運算測試,得到合適的數量。

稍微離題,Goroutine在golang裡面的方法就是go,使用起來很簡單,我們來看一個例子:

package main

import "fmt"

func f(n int) {
	for i := 0; i < 10; i++ {
		fmt.Println(n, ":", i)
	}
}

func main() {
	go f(0)
}

如果你興高采烈的複製貼上去執行,會發現什麼都沒得到。沒錯,這是正常的。我們可以看到在main裡面,我們用go呼叫f方法,想要印出一些數字,但為什麼會沒有印出來呢?

答案是因為在開始列印之前,main就已經執行完了,所以程式關閉,這就是執行緒的特性之一。如果想要順利看到產出,我們必須想辦法「卡住」他,比方說要他等一下。

func main() {
	go f(0)
	time.Sleep(time.Second)
}

go的下方使用time.Sleep方法,讓他睡一秒(如果沒有自動引入time,要記得手動加上才能通過編譯)。這樣再次執行,我們就可以看到f()被執行了。在一秒的時間內,印出十筆資料:

0 : 0
0 : 1
0 : 2
0 : 3
0 : 4
0 : 5
0 : 6
0 : 7
0 : 8
0 : 9

聰明的朋友可能已經想到,這些特性可能會造成怎樣的問題,我們來看看另外一個例子:

package main

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

func withdraw() {
	balance := money
	time.Sleep(3000 * time.Millisecond)
	balance -= 1000
	money = balance
	fmt.Println("After withdrawing $1000, balace: ", money)
	wg.Done()
}

var wg sync.WaitGroup
var money int = 1500

func main() {
	fmt.Println("We have $1500")
	wg.Add(2)
	go withdraw() // first withdraw
	go withdraw() // second withdraw
	wg.Wait()
}

這個例子中我們使用sync來作為停止的方法。我們原本有1500元,領了兩次1000元,理論上會剩下-500才對,但是執行後會得到:

We have $1500
After withdrawing $1000, balace:  500
After withdrawing $1000, balace:  500

疑?為什麼呢?這是因為第一個執行空間再回報餘額剩下500的時候,因為平行的關係,第二個執行空間計算的結果餘額也是500元。為了解決這樣的問題,我們可以使用Mutex來把狀態鎖上,避免不同執行緒之間狀態不一致導致異常,修正後的程式碼如下:

package main

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

func withdraw() {
	mu.Lock()
	balance := money
	time.Sleep(3000 * time.Millisecond)
	balance -= 1000
	money = balance
	mu.Unlock()
	fmt.Println("After withdrawing $1000, balace: ", money)
	wg.Done()
}

var wg sync.WaitGroup
var mu sync.Mutex
var money int = 1500

func main() {
	fmt.Println("We have $1500")
	wg.Add(2)
	go withdraw() // first withdraw
	go withdraw() // second withdraw
	wg.Wait()
}

最重要的差異是我們宣告了一個新的Mutex,然後使用LockUnlock來鎖住與解鎖狀態。如果狀態被鎖住以後,其他執行緒就必須等待執行上一個執行緒完成,才能繼續執行。這樣我們就可以得到合理的結果:

We have $1500
After withdrawing $1000, balace:  500
After withdrawing $1000, balace:  -500

是不是很有意思呢?

Reference


上一篇
Interface 接口
下一篇
Error Handling 錯誤處理
系列文
啥物碗Golang? 30天就Go30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言