goroutine
在其他的程式語言中,執行緒排程是交由os
來進行處理的。
但是在Go
語言中,會對此做一層封裝,Go
語言中的併發由goroutine
來實現,它類似於使用者態的執行緒,更類似於其他語言中的協程。它是交由Go
語言中的runtime
執行時來進行排程處理,這使得Go
語言中的併發效能非常之高。
一個Go
程式,可以啟動多個goroutine
。
一個普通的機器執行幾十個執行緒負載已經很高了,然而Go
可以輕鬆建立百萬goroutine
。
Go
標準庫的net
包,寫出的go web server效能直接媲美Nginx
。
比如在java/c++
裡,開發者通常要去自己維護一個執行緒池,並且需要包裝多個執行緒任務,同時還要由開發者手動排程執行緒執行任務並且維護上下文切換,這非常的耗費心智,故在Go
語言中出現了goroutine
,它的概念類似於執行緒與協程,Go
語言內建的就有排程與上下文切換機制,所以不用開發人員再去注意這些,並且goroutine
的使用也非常的簡單,它相較於其他語言的多併發程式設計更加輕鬆。
goroutine與執行緒
動態棧
作業系統中的執行緒都有固定的棧記憶體(一般為2MB),這使得開啟大量的執行緒會面臨效能下降的問題。
但是goroutine
在生命週期之處的棧記憶體一般只有2KB,並且它會按需進行增大和縮小。最大的棧限制可達到1GB,所以在Go
語言中一次建立上萬級別的goroutine
是沒有任何問題的。
goroutine排程
GPM
是Go
語言執行時runtime
層面的實現,這是Go
語言自己實現的一套排程系統,區別於作業系統來排程os
執行緒。
- G很好理解,就是單個goroutine的資訊,裡面除了存放本goroutine資訊外 還有與所在P的繫結等資訊。
- P管理著一組goroutine佇列,P裡面會儲存當前goroutine執行的上下文環境(函式指標,堆疊地址及地址邊界),P會對自己管理的goroutine佇列做一些排程(比如把佔用CPU時間較長的goroutine暫停、執行後續的goroutine等等)當自己的佇列消費完了就去全域性佇列裡取,如果全域性佇列裡也消費完了會去其他P的佇列裡搶任務。
- M(machine)是Go執行時(runtime)對作業系統核心執行緒的虛擬, M與核心執行緒一般是一一對映的關係, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關係是: P管理著一組G掛載在M上執行。當一個G長久阻塞在一個M上時,runtime
會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M。
P的個數是通過runtime.GOMAXPROCS
設定(最大256),Go1.5版本之後預設為物理執行緒數。 在併發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
單從執行緒排程講,Go語言相比起其他語言的優勢在於OS執行緒是由OS核心來排程的,goroutine
則是由Go執行時(runtime
)自己的排程器排程的,這個排程器使用一個稱為m:n排程的技術(複用/排程m個goroutine到n個OS執行緒)。 其一大特點是goroutine的排程是在使用者態下完成的, 不涉及核心態與使用者態之間的頻繁切換,包括記憶體的分配與釋放,都是在使用者態維護著一塊大的記憶體池, 不直接呼叫系統的malloc函式(除非記憶體池需要改變),成本比排程OS執行緒低很多。 另一方面充分利用了多核的硬體資源,近似的把若干goroutine均分在物理執行緒上, 再加上本身goroutine的超輕量,以上種種保證了go排程方面的效能。
上面這麼多專業術語看起來比較頭痛,這邊用一幅圖來明確的進行表示。
goroutine使用
在呼叫函式前加上go
關鍵字,就可以為函式建立一個goroutine
。
一個goroutine
必定對應一個函式,可以建立多個goroutine
去執行相同的函式。
每個Go
語言都有一個goroutine
,類似於主執行緒的概念。
goroutine
的啟動是隨機進行排程的,這個無法手動控制。
基本使用
下面是建立單個goroutine
與主goroutine
進行併發執行任務。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println(i)
}
fmt.Println("子goroutine執行完畢")
}() // 立即執行函式,一個goroutine任務
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
下面是建立多個goroutine
與主goroutine
進行併發執行任務。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務1",i)
}
fmt.Println("子goroutine1執行完畢")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務2",i)
}
fmt.Println("子goroutine2執行完畢")
}
func main() {
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
sync.WaitGroup
該屬性類似於一把全域性鎖,只有當子goroutine
任務結束後,主goroutine
任務才能結束。
類似於守護執行緒。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 當前有任務 0 個
func f1(){
defer wg.Done() // 執行完成後,任務減 1
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務1",i)
}
fmt.Println("子goroutine執行完畢")
}
func main() {
wg.Add(1) // 任務加 1 注意,一定要放外面,不能放函式中
go f1()
wg.Wait() // 任務必須為0時才繼續向下執行
fmt.Println("主goroutine執行完畢")
}
GOMAXPROCS
該函式可設定開啟多少os
執行緒來執行子goroutine
任務。
預設值是機器上的CPU核心數。例如在一個8核心的機器上,排程器會把Go程式碼同時排程到8個OS執行緒上(GOMAXPROCS是m:n排程中的n)。
Go語言中可以通過runtime.GOMAXPROCS()
函式設定當前程式併發時佔用的CPU邏輯核心數。
Go1.5版本之前,預設使用的是單核心執行。Go1.5版本之後,預設使用全部的CPU邏輯核心數。
如下示例,兩個子goroutine
任務在一個執行緒上執行,會通過時間片輪詢等策略來搶佔執行權。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務1",i)
}
fmt.Println("子goroutine1執行完畢")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務2",i)
}
fmt.Println("子goroutine2執行完畢")
}
func main() {
runtime.GOMAXPROCS(1) // 設定最多開啟1個子執行緒
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
時間輪詢
由於底層的os
執行緒切換機制是依照時間輪詢進行切換,所以goroutine
的切換時機也是由時間片輪詢來決定的。
使用runtime.Gosched()
可讓當前任務讓出執行緒佔用,交由其他任務進行執行。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務1",i)
if i == 300 {
runtime.Gosched() // 讓出執行緒佔用
}
}
fmt.Println("子goroutine1執行完畢")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務2",i)
}
fmt.Println("子goroutine2執行完畢")
}
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
終止任務
runtime.Goexit()
終止當前任務。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任務1",i)
if i == 300 {
runtime.Goexit() // 終止任務
fmt.Println("子goroutine任務被終止")
}
}
fmt.Println("子goroutine執行完畢")
}
func main() {
go f1()
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
通道使用
多個goroutine
中必須要有某種安全的機制來進行資料共享,這就出現了channel
通道。
它類似於管道或者佇列,作用在於保證多goroutine
訪問同一資源時達到資料安全的目的。
型別宣告
channel
是引用型別,這就代表必須要使用make()
進行記憶體分配。
初始值為nil
。
下面是進行宣告的示例:
var ch1 chan int // 宣告一個傳遞整型的通道
var ch2 chan bool // 宣告一個傳遞布林型的通道
var ch3 chan []int // 宣告一個傳遞int切片的通道
channel使用
使用前要進行記憶體分配,並且它還可選緩衝區。
代表該通道最多可容納多少資料。當然,緩衝區大小是可選的,它具有動態擴容的特性。
make(chan 元素型別, [緩衝大小])
示例如下:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作
以下是channel
的操作:
方法 | 說明 |
---|---|
ch <- 資料 | 將資料放入通道中 |
資料 <- ch | 將資料從通道取出 |
close() | 關閉通道 |
現在我們先使用以下語句定義一個通道:
ch := make(chan int)
將一個值傳送到通道中。
ch <- 10 // 把10傳送到ch中
從一個通道中接收值。
x := <- ch // 從ch中接收值並賦值給變數x
<-ch // 從ch中接收值,忽略結果
我們通過呼叫內建的close()
函式來關閉通道。
close(ch)
關於關閉通道需要注意的事情是,只有在通知接收方goroutine所有的資料都傳送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉檔案是不一樣的,在結束操作之後關閉檔案是必須要做的,但關閉通道不是必須的。
關閉後的通道有以下特點:
- 對一個關閉的通道再傳送值就會導致panic。
- 對一個關閉的通道進行接收會一直獲取值直到通道為空。
- 對一個關閉的並且沒有值的通道執行接收操作會得到對應型別的零值。
- 關閉一個已經關閉的通道會導致panic。
阻塞通道
當一個通道無緩衝區時,將被稱為阻塞通道。
通道中存放一個值,但該值並沒有被取出時將會引發異常。
必須先收,後發。因為傳送後會產生阻塞,如果沒有接收者則會導致死鎖異常
必須將通道中的值取盡,否則會發生死鎖異常,也就是說放了幾次就要取幾次
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
lily := <- ch // 等待取百合花
fmt.Println(rose)
fmt.Println(lily)
}
func main(){
wg.Add(1)
ch := make(chan string)
go f1(ch) // 必須先有接收者
ch <- "玫瑰花" // 開始放入玫瑰花
ch <- "百合花" // 開始放入百合花
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
非阻塞通道
非阻塞通道即為有緩衝區的通道。
只要通道的容量大於零,則代表該緩衝區中能夠去存放值。
非阻塞通道相較於阻塞通道,它的使用其實更加符合人類邏輯
阻塞通道必須要先接收再存入
非阻塞通道可以先存入再接收
並且,非阻塞通道中的值可以不必取盡
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
fmt.Println(len(ch)) // 獲取元素數量 1 代表還剩下一個沒取
fmt.Println(cap(ch)) // 獲取容量 10 代表最多可以放10個
fmt.Println(rose)
}
func main(){
wg.Add(1)
ch := make(chan string,10)
ch <- "玫瑰花" // 放入玫瑰花
ch <- "百合花" // 放入百合花
go f1(ch)
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
單向通道
單向通道即是隻能取,或者只能發。
上面的通道都是雙向通道,可能造成閱讀不明確的問題,故此Go
還提供了單向通道。
在函式傳參中,可以將雙向通道轉換為單項通道,這也是最常用的方式。
通道標識 | 說明 |
---|---|
ch <- string | 代表只能寫入string型別的值 |
<- ch string | 代表只能取出string型別的值 |
package main
import (
"sync"
"fmt"
)
var wg sync.WaitGroup
func recv(ch <-chan string) { // 只能取
defer wg.Done()
rose := <- ch
fmt.Println(rose)
}
func send(ch chan<- string) { // 只能放
defer wg.Done()
ch <- "玫瑰花"
}
func main() {
wg.Add(2)
ch := make(chan string, 10)
go send(ch)
go recv(ch)
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
常見情況
以下是通道的使用常見情況。
關閉已經關閉的channel也會引發panic。
任務池
多個goroutine
的切換會帶來效能損耗問題。
所以我們可以通過做一個goroutine
的池來解決這種問題,當一個goroutine
的任務結束後,它不會kill
掉該goroutine
,而是讓它繼續的取下一個任務。
所以我們需要與chan
結合進行構造一個簡單的任務池。
如下示例,構建了一個簡單的任務池並且開啟了3個goroutine
,並且放了6個任務在task
這個chen
中交由run
進行處理。
處理結果放在result
這個chen
中。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func run(id int, task <-chan string, result chan<- string) {
defer wg.Done()
for {
t, ok := <-task
if !ok {
fmt.Println("處理完了所有任務")
break
}
time.Sleep(time.Second * 2)
t += fmt.Sprintf(":已由%d處理", id)
result <- t
}
}
func main() {
task := make(chan string, 10)
result := make(chan string, 10)
wg.Add(3)
for i := 0; i < 3; i++ {
go run(i, task, result) // 開三個goroutine來處理
}
urlRequeste := []string{
"www.baidu.com",
"www.google.com",
"www.cnblog.com",
"www.xinlang.com",
"www.csdn.com",
"www.taobao.com",
}
for _, url := range urlRequeste {
task <- url // 開啟了六個任務
}
close(task)
for i := 0; i < len(urlRequeste); i++ {
fmt.Println(<-result)
}
close(result)
wg.Wait()
fmt.Println("主goroutine執行完畢")
}
// www.google.com:已由2處理
// www.cnblog.com:已由1處理
// www.baidu.com:已由0處理
// 處理完了所有任務
// 處理完了所有任務
// 處理完了所有任務
// www.taobao.com:已由0處理
// www.xinlang.com:已由2處理
// www.csdn.com:已由1處理
// 主goroutine執行完畢
select多路複用
類似於事件迴圈,我們來監聽多個通道。
當一個通道可用時就來操縱該通道。
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
預設操作
}
這個示例還是要在具體的應用場景中比較常見,並且一般的庫都已經寫好了。
只要知道其中理論就行,沒必要白手寫select
,除非你要做開源框架或公司框架等。
可處理一個或多個channel的傳送/接收操作。
如果多個case同時滿足,select會隨機選擇一個。
對於沒有case的select{}會一直等待,可用於阻塞main函式。
小例子:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch: // 允許賦值
fmt.Println("可以讀了,已經讀出了:", x) // 可讀
case ch <- i: // 可寫
fmt.Println("可以寫了,已經寫入了:", i)
}
}
}
鎖相關
鎖是為了解決資源同步的問題。
但是對於多個goroutine
通訊應該是去使用channel
,而不是用鎖進行解決。
互斥鎖
如下程式碼,會產生資源競爭問題。致使結果不正確:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num ++
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num --
}
}()
wg.Wait()
fmt.Println(num)
}
// 13966
// 7578
// 9475
此時新增互斥鎖即可,讓其變為序列執行:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加鎖
num ++
lock.Unlock() // 解鎖
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加鎖
num --
lock.Unlock() // 解鎖
}
}()
wg.Wait()
fmt.Println(num)
}
讀寫互斥鎖
互斥鎖是完全互斥,將併發執行轉變為序列執行,效能損耗比較大。
但是在更多的場景中,我們則不需要完全互斥。
比如多個人訪問統一資源但是並未對資源本身做修改時可以不加鎖,但是當有人對資源做修改時其他人將無法訪問。
以上場景使用讀寫鎖更加合適,讀寫鎖在讀多寫少的場景下非常高效。
讀鎖:我獲取了讀鎖你不能去修改,必須等我釋放
寫鎖:我獲取了寫鎖你不能去讀,必須等我釋放
如下,寫入200次,讀取2000次的用時為1s左右。
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var rwlock sync.RWMutex // 讀寫鎖
var variety = 10
func read() {
defer wg.Done()
rwlock.RLock() // 加讀鎖
fmt.Println(variety)
rwlock.RUnlock() // 釋放讀鎖
}
func write() {
defer wg.Done()
rwlock.Lock() // 加寫鎖
variety ++
fmt.Println(variety)
rwlock.Unlock() // 釋放寫鎖
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("執行時間:",end.Sub(start)) // 1s左右
}
如果單純使用互斥鎖,時間會更長:
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex // 互斥鎖
var variety = 10
func read() {
defer wg.Done()
lock.Lock() // 加互斥鎖
fmt.Println(variety)
lock.Unlock() // 釋放互斥鎖
}
func write() {
defer wg.Done()
lock.Lock() // 加互斥鎖
variety ++
fmt.Println(variety)
lock.Unlock() // 釋放互斥鎖
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("執行時間:",end.Sub(start)) // 2s左右
}
sync.Once
只執行一次,如果一個配置檔案體積過於巨大,在初始化時進行載入會拖慢啟動速度。
所以我們可以在要使用時進行載入(懶惰載入),如下示例,有10個goroutine
都需要用到配置檔案。
該配置檔案只會載入一次,之後便不會重複載入。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
func load() {
fmt.Println("載入配置檔案...")
}
func main() {
fmt.Println("執行程式碼邏輯...發現很多地方都要用配置檔案了")
for i := 0; i < 10; i++ {
fmt.Printf("%v需要用到配置檔案,開始載入\n", i)
wg.Add(1)
go func() {
defer wg.Done()
once.Do(load) // 只載入一次,並且該函式的格式必須是不能有引數與返回值
}()
}
wg.Wait()
}
sync.Map
Go
語言中內建的map
不是併發安全的。不要使用內建的map
進行資料傳遞,你應該使用channel
或者sync
給你提供的map
。該map
不用進行make
初始化記憶體。
sync
提供的map
有以下功能:
方法 | 描述 |
---|---|
Store(k,v) | 設定一組鍵值對 |
Load(k) | 根據k取出v |
LoadorStore(k,v) | 根據k取出v,如果沒有該k則建立v |
Delete(k) | 刪除一組鍵值對 |
Range | 迴圈遍歷出k和v |
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
var m = sync.Map{} // g安全的map
func out() {
defer wg.Done()
gift, _ := m.Load("禮物")
fmt.Println(gift)
}
func put() {
m.Store("禮物", "玫瑰花")
defer wg.Done()
}
func main() {
wg.Add(2)
go put()
go out()
wg.Wait()
}
原子操作
功能概述
對於多個goroutine
訪問同一資源造成的併發安全問題,可以通過加鎖來進行解決。
但是加鎖會使效能降低,所以這裡Go
語言中sync/atomic
包提供了原子操作來代替加鎖。
常用方法
主要對數字型別的資料的加減乘除等。
方法 | 描述 |
---|---|
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) |
讀取操作 |
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) |
寫入操作 |
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) |
交換操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) |
比較並交換操作 |
示例演示
使用原子操作,速度較快。
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var wg sync.WaitGroup
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, 1)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, -1)
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("執行時間:", end - start) // 981600
fmt.Println(num)
}
加鎖操作,速度會慢一些:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num++
lock.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num--
lock.Unlock()
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("執行時間:", end - start) // 1000300
fmt.Println(num)
}