併發運算就是多線程運算,且併發(concurrency)並非併行(Parallelism)
雖然兩者從中文字面十分相似,但意義完全不同。
併發
是共享時間運算
,在一段時間內輪流享有時間資源。併行
是平行運算
,在一段時間都能享有時間資源。併發
是把時間切成很多小段,在這小段時間內先後執行多項任務。併行
則是透過多核心
,同時處理多個任務。以譬喻來說做兩件事
併發
: 一個人在一段時間做兩件事。併行
:兩個人同事在做一件事。Goroutines
是輕量級的線程
而 main func
則是程式當前最主要的goroutine
。
go function()
Go的併發會用到多個核心下去執行,試著執行以下的程式看看:
package main
import (
"fmt"
"time"
)
func main() {
print1()
print2()
time.Sleep(time.Second)
}
func print1() {
for i := 0; i < 100; i++ {
fmt.Print("O")
}
}
func print2() {
for i := 0; i < 100; i++ {
fmt.Print("X")
}
}
運行後可得以下結果
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
再加上關鍵字go
之後
package main
import (
"fmt"
"time"
)
func main() {
go print1()
go print2()
time.Sleep(time.Second)
}
func print1() {
for i := 0; i < 100; i++ {
fmt.Print("O")
}
}
func print2() {
for i := 0; i < 100; i++ {
fmt.Print("X")
}
}
運行後可得以下結果
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXOOOOX
輸出是一段一段的,一下O
一下X
交錯著,代表兩邊的線程都很努力的想噴射把值Print出來。
由上面範例我們可以得知
print1()
執行完後才執行print2()
,並無併發情況發生。go
關鍵字後執行,從結果上看來是有併發的。runtime.GOMAXPROCS(n)
這一參數限制程式執行時 CPU用到的最大核心數量。
如果設置小於1,等於沒設,預設值是電腦核心數。
package main
import (
"fmt"
"time"
"runtime"
)
func main() {
runtime.GOMAXPROCS(2)
go print1()
go print2()
time.Sleep(time.Second)
}
func print1() {
for i := 0; i < 100; i++ {
fmt.Print("O")
}
}
func print2() {
for i := 0; i < 100; i++ {
fmt.Print("X")
}
}
表示設定為2核心
Panic
是發生了預期之外的事情,導致異常、錯誤的產生,退出程序的同時回傳錯誤代碼2 (Process finished with exit code 2)
。我們可以透過panic
的func來主動引起錯誤發生。
要注意的是若在併發線程
中發生了panic
,也會導致主程式也異常結束。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Start")
go p()
time.Sleep(time.Second * 1)
fmt.Println("End")
}
func p() {
fmt.Println("Going to crash")
panic("Crash!")
}
執行後得以下結果
Start
Going to crash
panic: Crash!
goroutine 34 [running]:
main.p()
/tmp/sandbox3289633550/prog.go:18 +0x65
created by main.main
/tmp/sandbox3289633550/prog.go:11 +0x65
雖然goroutine相當的便利,但不慎使用也會引發許多問題,最常見的就是Race Condition。
以下範例中,使用了10000個被併發出去的func,
每個func
只做一件事:count++。
package main
import (
"fmt"
"time"
)
var count = 0
func main() {
for i := 0; i < 10000; i++ {
go race()
}
time.Sleep(time.Millisecond * 100)
fmt.Println(count)
}
func race() {
count++
}
運行後得以下結果
9508
此時我們可以發現結果並非為10000,因為多個線程同時在爭奪資源,導致有許多的數字都被重複執行了。
這種情況該如何對付呢?
Ans: 互斥鎖,再多執行緒編成中,在對公共資源進行讀寫時,必須上鎖防止其他線程爭搶資源,並在結束讀寫時在解鎖,讓其他線程知道該資源已被釋放。
If the lock is already in use, the calling goroutine blocks until the mutex is available.
為了保證total.value += i的原子性,我們通過sync.Mutex的鎖來保證該語句在同一時間只被單一線程goroutine
所訪問。
package main
import (
"fmt"
"time"
"sync"
)
var count = 0
var m sync.Mutex
func main() {
for i := 0; i < 10000; i++ {
go race()
}
time.Sleep(time.Millisecond * 100)
fmt.Println(count)
}
func race() {
m.Lock()
count++
m.Unlock()
}
運行後可得以下結果
10000
只要在變數前上鎖(Lock),在解鎖(Unlock)前 只有該線程能對其進行操作。
這章節教大家最基本的goroutine使用,讓大家在一些需要大量併發的情況下,能夠使用最基本的goroutine來解決,那下一章節會針對goroutine在更深入的去介紹,下個章節我們也會圍繞著sync這個標準庫去解說。