iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Modern Web

fmt.Println("從零開始的Golang生活")系列 第 9

Day9 Goroutine

併發 vs併行

併發運算就是多線程運算,且併發(concurrency)並非併行(Parallelism)

雖然兩者從中文字面十分相似,但意義完全不同。

  • 併發共享時間運算,在一段時間內輪流享有時間資源。
  • 併行平行運算,在一段時間都能享有時間資源。
  • 併發是把時間切成很多小段,在這小段時間內先後執行多項任務。
  • 併行則是透過多核心同時處理多個任務。

以譬喻來說做兩件事

  • 併發: 一個人在一段時間做兩件事。
  • 併行:兩個人同事在做一件事。

Goroutines

Goroutines 是輕量級的線程

main func 則是程式當前最主要的goroutine

Go Func

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

輸出是一段一段的,一下一下X

交錯著,代表兩邊的線程都很努力的想噴射把值Print出來。

由上面範例我們可以得知

  • 範例1是在print1()執行完後才執行print2(),並無併發情況發生。
  • 範例2則是都加上go關鍵字後執行,從結果上看來是有併發的。

runtime.GOMAXPROCS

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

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

競爭危害(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: 互斥鎖,再多執行緒編成中,在對公共資源進行讀寫時,必須上鎖防止其他線程爭搶資源,並在結束讀寫時在解鎖,讓其他線程知道該資源已被釋放。

sync.Mutex

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)前 只有該線程能對其進行操作。

Summary

這章節教大家最基本的goroutine使用,讓大家在一些需要大量併發的情況下,能夠使用最基本的goroutine來解決,那下一章節會針對goroutine在更深入的去介紹,下個章節我們也會圍繞著sync這個標準庫去解說。


上一篇
Day8 Function and Interface
下一篇
Day10 Sync.WaitGroup & Sync.Map
系列文
fmt.Println("從零開始的Golang生活")30

尚未有邦友留言

立即登入留言