goroutine
是Golang
特有,類似於線程,但是線程是由操作系統進行調度管理,而goroutine
是由Golang
運行時進行調度管理的用戶態的線程。
1.C#的線程操作
1.1 創建線程
static void Main(string[] args)
{
Thread thread = new Thread(Count);
thread.IsBackground = true;
thread.Start();
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count()
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
1.2 向線程傳參
Thread
構造函數有兩個參數ParameterizedThreadStart
與ThreadStart
public delegate void ParameterizedThreadStart(object? obj);
public delegate void ThreadStart();
沒錯,一個是無參委託,,一個是有參委託且參數類型爲object
,因此我們用以創建線程的方法參數需爲object
類型
static void Main(string[] args)
{
Thread thread = new Thread(Count);
thread.IsBackground = true;
thread.Start(100);
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count(object times)
{
int count = (int)times;
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
當然簡單的還是直接使用lambda
表達式
static void Main(string[] args)
{
Thread thread = new Thread(()=>{
Count(100);
});
thread.IsBackground = true;
thread.Start();
for (int i = 0; i < 10; i++)
Console.Write("x\n");
}
static void Count(object times)
{
int count = (int)times;
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i); ;
}
}
注意:使用Lambda表達式可以很簡單的給Thread傳遞參數,但是線程開始後,可能會不小心修改了被捕獲的變量,這要多加註意。比如循環體中,最好創建一個臨時變量。
1.3 線程安全與鎖
從單例模式來看線程安全:
class Student
{
private static Student _instance =new Student();
private Student()
{
}
static Student GetInstance()
{
return _instance;
}
}
- 單例模式,我們的本意是始終保持類實例化一次然後保證內存中只有一個實例。上述代碼中在類被加載時,就完成靜態私有變量的初始化,不管需要與否,都會實例化,這個被稱爲
餓漢模式的單例模式
。這樣雖然沒有線程安全問題,但是這個類如果不使用,就不需要實例化。然後便有了下面的寫法:需要時實例化
class Student
{
private static Student _instance;
private Student()
{
}
static Student GetInstance()
{
if (_instance == null)
_instance = new Student();
return _instance;
}
}
- 調用時判斷靜態私有變量是否爲空,然後再給。這個其實就有一個線程安全的問題:多線程調用
GetInstance()
,當同時多個線程執行時,條件_instance == null
可能會同時都滿足。這樣_instance
就完成了多次實例化賦值操作,就引出了我們的鎖Lock
private static Student _instance;
private static readonly object locker = new object();
private Student()
{
}
static Student GetInstance()
{
lock (locker)
{
if (_instance == null)
_instance = new Student();
}
return _instance;
}
- 第一個線程運行,就會加鎖
Lock
- 第二個線程運行,首先檢測到locker對象爲"加鎖"狀態(是否還有線程在
lock
內,未執行完成),該線程就會阻塞等待第一個線程解鎖 - 第一個線程執行完
lock
體內代碼,解鎖,第二個線程纔會繼續執行
上面看似已經完美了,但是多線程情況下,每次都要經歷,檢測(是否阻塞),上鎖,解鎖,其實是很影響性能,我們本來的目的是返回單一實例即可。我們在檢測locker
對象是否加鎖之前,如果實例已經存在,那麼後續工作是沒必要做的。所以就有了下面的 雙重檢驗:
private static Student _instance;
private static readonly object locker = new object();
private Student()
{
}
static Student GetInstance()
{
if (_instance == null)
{
lock (locker)
{
if (_instance == null)
_instance = new Student();
}
}
return _instance;
}
1.4 Task
線程有兩種工作類型:
-
CPU-Bound:計算密集型,花費大部分時間執行CPU密集型工作的操作,這種工作類型永遠也不會讓線程處在等待狀態。
-
IO-Bound:I/O密集型,花費大部分時間等待某事發生的操作,一直等待着,導致線程進入等待狀態的工作類型。比如通過http請求對資源的訪問。
對於IO-Bound的操作,時間花費主要是在I/O
上,而在CPU
上幾乎是沒有花費時間。對於此,更推薦使用Task
編寫異步代碼,而對於CPU-Bound
和IO-Bound
的異步代碼不是我們本篇的重點,博主將大概介紹一下Task的優勢:
-
不再幹等:: 以
HttpClient
使用異步代碼請求GetStringAsync
爲例,這顯然是一個I/O-Bound
,最終會對操作系統本地網絡庫進行調用,系統API調用,比如發起請求的socket,但是這個時間長短並不是由代碼決定,他取決硬件,操作系統,網絡情況。控制權會返回給調用者,我們可以做其他操作,這讓系統能處理更多的工作而不是等待 I/O 調用結束。直到await
去獲得請求結果。 -
讓調用者不再幹等: 對於
CPU-Bound
,沒有辦法避免將一個線程用於計算,因爲這畢竟是一個計算密集型的工作。但是使用Task
的異步代碼(async
與await
)不僅可以與後臺線程交互,還可以讓調用者繼續響應(可以併發執行其他操作)。同上,直到遇到await
時,異步方法都會讓步於調用方。
2.Golang的goroutine
2.1 啓動goroutine
Golang
中啓動一個goroutine
沒有C#的線程那麼麻煩,只需要在調用方法的前面加上關鍵字go
.
func main(){
go Count(100)
}
func Count(times int) {
for i := 0; i < times; i++ {
fmt.Printf("%v\n", i)
}
}
2.2 goroutine的同步
在C#中的任務(Task)可以使用Task.WhenAll
來等待Task
對象完成。Golang
中比較原始,像個花名冊一樣登記造冊:
- 啓動 - 計數+1
- 執行完畢 - 計數-1
- 完成
要使用sync
包:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
//登記造冊
wg.Add(2)
go Count(100)
go Count(100)
//等待所有登記的goroutine都結束
wg.Wait()
fmt.Println("執行完成")
}
func Count(times int) {
//執行完成
defer wg.Done()
for i := 0; i < times; i++ {
fmt.Printf("%v\n", i)
}
}
2.3 channel通道
一個goroutine
發送特定值到另一個goroutine
的通信機制就是channel
2.3.1 channel聲明
channel
是引用類型
var 變量 chan 元素類型
chan 元素類型
跟struct
,interface
一樣,就是一種類型。後面的元素類型限定了通道具體存儲類型。
2.3.2 channel初始化
聲明後的channel
是空值nil
,需要初始化才能使用。
var ch chan int
ch=make(chan int,5) //5爲設定的緩衝大小,可選參數
2.3.3 channel操作
操作三板斧:
- send - 發送
ch<-100
值指向通道
- receive - 接收
value:=<-ch
i, ok := <-ch1 // 通道關閉後再取值ok=false
//或者
for i := range ch1 { // 通道關閉後會退出for range循環
fmt.Println(i)
}
通道指向變量
- close - 關閉
close(ch)
2.3.3 緩衝與無緩衝
ch1:=make(chan int,5) //5爲設定的緩衝大小,可選參數
ch2:=make(chan int)
無緩衝的通道,無緩衝的通道只有在接收值的時候才能發送值。
- 只往通道傳值,不從通道接收,就會出現
deadlock
- 只從通道接收,不往淘到發送,也會發生阻塞
使用無緩衝通道進行通信將導致發送和接收的goroutine
同步化。因此,無緩衝通道也被稱爲同步通道。
有緩衝的通道,可以有效緩解無緩衝通道的尷尬,但是通道裝滿,上面的尷尬依然存在。
2.3.4 單向通道
限制通道在函數中只能發送或只能接收,單向通道粉墨登場,單向通道的使用是在函數的參數中,也沒有引入新的關鍵字,只是簡單的改變的箭頭的位置:
chan<- int 只寫不讀
<-chan int 只讀不寫
函數傳參及任何賦值操作中可以將雙向通道轉換爲單向通道,反之,不行。
2.3.5 多路複用
package main
import "fmt"
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Printf("第%v次,x := <-ch,從通道中讀取值%v", i+1, x)
fmt.Println()
case ch <- i:
fmt.Printf("第%v次,執行ch<-i", i+1)
fmt.Println()
}
}
}
第1次,執行ch<-i
第2次,x := <-ch,從通道中讀取值0
第3次,執行ch<-i
第4次,x := <-ch,從通道中讀取值2
第5次,執行ch<-i
第6次,x := <-ch,從通道中讀取值4
第7次,執行ch<-i
第8次,x := <-ch,從通道中讀取值6
第9次,執行ch<-i
第10次,x := <-ch,從通道中讀取值8
Select
多路複用的規則:
- 可處理一個或多個channel的發送/接收操作。
- 如果多個
case
同時滿足,select
會隨機選擇一個。 - 對於沒有
case
的select{}
會一直等待,可用於阻塞main函數。
2.5 併發安全與鎖
goroutine是通過channel通道進行通信。不會出現併發安全問題。但是,實際上還是不能完全避免操作公共資源的情況,如果多個goroutine
同時操作這些公共資源,可能就會發生併發安全問題,跟C#的線程一樣,鎖的出現就是爲了解決這個問題:
2.5.1 互斥鎖
互斥鎖,這個就跟C#的鎖是一樣,一個goroutine訪問,另外一個就這能等待互斥鎖的釋放。同樣需要sync
包:
sync.Mutex
var lock sync.Mutex
lock.Lock()//加鎖
//操作公共資源
lock.Unlock()//解鎖
2.5.2 讀寫互斥鎖
互斥鎖是完全互斥的,如果是讀多寫少,大部分goroutine
都在讀,少量的goroutine
在寫,這時併發讀是沒必要加鎖的。使用時,依然需要sync
包:
sync.RWMutex
讀寫鎖分爲兩種:
- 讀鎖
- 寫鎖。
import (
"fmt"
"sync"
)
var (
lock sync.Mutex
rwlock sync.RWMutex
)
rwlock.Lock() // 加寫鎖
//效果等同於互斥鎖
rwlock.Unlock() // 解寫鎖
rwlock.RLock() //加讀鎖
//可讀不可寫
rwlock.RUnlock() //解讀鎖
2.6* sync.Once
goroutine
的同步我們使用過sync.WaitGroup
Add(count int)
計數器累加,在調用goroutine外部執行,由開發人員指定- Done() 計數器-1,在goroutine內部執行
- Wait() 阻塞 直至計數器爲0
除此之外還有一個sync.Once
,顧名思義,一次,只執行一次。
func (o *Once) Do(f func()) {}
var handleOnce sync.Once
handleOnce.Do(函數)
sync.Once
其實內部包含一個互斥鎖和一個布爾值,互斥鎖保證布爾值和數據的安全,而布爾值用來記錄初始化是否完成。這樣設計就能保證初始化操作的時候是併發安全的並且初始化操作也不會被執行多次。type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/x86), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
2.7* sync.Map
Golang
的map不是併發安全的。sync
包中提供了一個開箱即用的併發安全版map–sync.Map
。
開箱即用表示不用像內置的map一樣使用make函數初始化就能直接使用。同時sync.Map
內置了諸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
var m = sync.Map{}
m.Store("四川", "成都")
m.Store("成都", "高新區")
m.Store("高新區", "應龍南一路")
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
v, ok := m.Load("成都")
if ok {
fmt.Println(v)
}
fmt.Println()
value, loaded := m.LoadOrStore("陝西", "西安")
fmt.Println(value)
fmt.Println(loaded)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//存在就加載,不存在就添加
value1, loaded1 := m.LoadOrStore("四川", "成都")
fmt.Println(value1)
fmt.Println(loaded1)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//加載並刪除 key存在
value2, loaded2 := m.LoadAndDelete("四川")
fmt.Println(value2)
fmt.Println(loaded2)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
//加載並刪除 key 不存在
value3, loaded3 := m.LoadAndDelete("北京")
fmt.Println(value3)
fmt.Println(loaded3)
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
m.Delete("成都") //內部是調用的LoadAndDelete
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("k=:%v,v:=%v\n", key, value)
return true
})
fmt.Println()
k=:四川,v:=成都
k=:成都,v:=高新區
k=:高新區,v:=應龍南一路
高新區
西安
false
k=:四川,v:=成都
k=:成都,v:=高新區
k=:高新區,v:=應龍南一路
k=:陝西,v:=西安
成都
true
k=:四川,v:=成都
k=:成都,v:=高新區
k=:高新區,v:=應龍南一路
k=:陝西,v:=西安
成都
true
k=:陝西,v:=西安
k=:成都,v:=高新區
k=:高新區,v:=應龍南一路
<nil>
false
k=:成都,v:=高新區
k=:高新區,v:=應龍南一路
k=:陝西,v:=西安
k=:高新區,v:=應龍南一路
k=:陝西,v:=西安
2.8 單例模式
綜上,sync.Once
其實內部包含一個互斥鎖和一個布爾值,這個布爾值就相當於C#單例模式下的雙重檢驗的第一個判斷。所以在golang
中可以利用sync.Once
實現單例模式:
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
2.9* 原子操作
代碼中的加鎖操作因爲涉及內核態的上下文切換會比較耗時、代價比較高。針對基本數據類型我們還可以使用原子操作來保證併發安全。Golang
語言中原子操作由內置的標準庫sync/atomic
提供。由於場景較少,就不做介紹,詳細操作請自行查閱學習。
再次強調:這個系列並不是教程,如果想系統的學習,博主可推薦學習資源。