Go 並發操作

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調度

   GPMGo語言運行時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(){
	wg.Add(1)
	defer wg.Done()
	for i := 0 ; i < 1000 ; i++ {
		fmt.Println("任務1",i)
	}
	fmt.Println("子goroutine1執行完畢")
}

func f2(){
	wg.Add(1)
	defer wg.Done()
	for i := 0 ; i < 1000 ; i++ {
		fmt.Println("任務2",i)
	}
	fmt.Println("子goroutine2執行完畢")
}

func main() {
	runtime.GOMAXPROCS(1) // 設置最多開啟1個子執行緒
	f1()
	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所有的數據都發送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。

   關閉後的通道有以下特點:

  1. 對一個關閉的通道再發送值就會導致panic。
  2. 對一個關閉的通道進行接收會一直獲取值直到通道為空。
  3. 對一個關閉的並且沒有值的通道執行接收操作會得到對應類型的零值。
  4. 關閉一個已經關閉的通道會導致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。

   channel異常總結

任務池

   多個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)
}

Tags: