很多初級的Gopher在學習了goroutine之後,在項目中其實使用率不高,尤其一些跨語言過來的人,對併發編程理解不深入,可能很多人只知道go func(),或者掌控不夠,謹慎一些,儘量少使用或者不使用,用的話就是go func(),主要列一下我們這邊的主要使用方法。
goroutine在項目中的使用方法
看一下樣例代碼,實際上,我們生產環境中就是這麼使用的。
package logic
import (
"context"
"fmt"
"sync"
"time"
)
type UserData struct {
Age int
Name string
Postion string
}
type ServerLogic struct {
ctx context.Context
cancel func()
waiter sync.WaitGroup
ch chan UserData
}
func NewServerLogic(logCtx *context.Context, worker int, queue int) *ServerLogic {
logic := &ServerLogic{}
logic.InitWorker(worker, queue)
return logic
}
func (this *ServerLogic) InitWorker(workers int, queue int) {
this.ch = make(chan UserData, queue)
this.ctx, this.cancel = context.WithCancel(context.Background())
this.waiter.Add(workers)
for i := 0; i < workers; i++ {
go this.Proc()
}
}
func (this *ServerLogic) Proc() {
defer this.waiter.Done()
for {
select {
case t := <-this.ch:
this.Dothing(t)
case <-this.ctx.Done():
return
}
}
}
func (this *ServerLogic) Dothing(data UserData) error {
//do code
time.Sleep(time.Second*30)
return nil
}
func (this *ServerLogic) Close() {
this.cancel()
this.waiter.Wait()
}
func (this *ServerLogic) PutData(user UserData) error {
select {
case this.ch<-user:
return nil
default:
return fmt.Errorf("queue overflow")
}
}
如果有人想直接使用的話,只需要把UserData struct換成自己的請求數據,把Dothing裏面的代碼換成讓goroutine多任務執行的代碼就可以在自己的項目中使用了。
PutData有請求數據就放入channel,每個goroutine不停的循環從channel裏面取數據,取到數據之後就執行相應的邏輯流程,可以看到整體的調度都是channel來控制的,通過channel的通信來傳遞數據。
不要通過共享內存來通信,要通過通信來共享內存
看看大概的代碼分析
- InitWorker的時候會創建queue個channl,再創建workers個goroutine,執行go Proc()
- Proc方法,裏面有for的無限循環,不停從步驟1裏面創建的channl裏面獲取UserData數據,一旦獲取數據成功,就會帶着UserData數據去執行Dothing方法。需要注意的是,這是workers個goroutine都在執行Proc
- Dothing方法,就是讓某一個goroutine拿到UserData數據去處理數據,執行邏輯
- Close方法,給所有的goroutine發送關閉的信號,channl裏面不在有數據寫入,waiter.Wait()等待現有的channel裏面數據被消費完,goroutine就執行完畢退出。
- PutData方法,就是把請求的數據交給goroutine去執行。具體的做法,是把數據 塞到channl隊列裏面,如果queue個channl隊列已滿,就拋出溢出錯誤。
當然了PutData也可以等待channl隊列裏面的數據被Proc拿出,然後空出位置再塞數據到channl隊列。
func (this *ServerLogic) PutData(user UserData) error {
timer := time.NewTimer(3*time.Second)
select {
case this.ch<-user:
return nil
case <-timer.C:
return fmt.Errorf("put timeout")
}
}
加一個超時器,總不能等到天荒地老把,如果超過三秒,仍然沒有空出channl位置,現有的隊列還沒有消費完,就拋出塞數據超時的錯誤.
看一下樣例的使用的代碼
package main
import (
context2 "context"
"fmt"
"test/logic"
)
func main() {
context := context2.Background()
server := logic.NewServerLogic(&context, 1, 2)
rt1 := server.PutData(logic.UserData{
Age: 11,
Name: "test1",
Postion: "golang",
})
fmt.Println(rt1)
rt2 := server.PutData(logic.UserData{
Age: 12,
Name: "test2",
Postion: "golang",
})
fmt.Println(rt2)
rt3 := server.PutData(logic.UserData{
Age: 13,
Name: "test3",
Postion: "golang",
})
fmt.Println(rt3)
server.Close()
fmt.Println("end")
}
等待了大概三十多秒之後的結果,打印結果其實跟預想的是一樣的。
<nil>
<nil>
queue overflow
end
NewServerLogic(&context, 1, 2)代碼中,我們要求創建了1個goroutine,大小爲2的channl隊列。
所以第一個PutData和第二個PutData是塞數據成功的。等到第三次PutData的時候,因爲我們channl隊列的大小是2,已經被佔滿了,所以第三次就會提示溢出錯誤。
使用goroutine另一種方法
我看項目中還有一些其他人的使用方法,區別只是退出的時候沒有使用context的cancel方法,而是使用了channel去通知退出goroutine,內部的原理其實是一樣的。看一下下面的代碼。
只有關閉這裏是不一樣的,其他的基本一致。執行退出的時候在Close()方法中,close(this.quit)會給quit channel寫入數據,Proc()方法會循環從channel和quit裏面取數據,一旦從this.quit裏面取出了數據,說明系統讓關閉goroutine,然後Proc方法就終止。
go func()行不行
有人說,扯這麼多,爲啥go func()不行,我在項目裏面使用go func()運行的好好,而且golang的HTTP庫裏也是使用的go c.serve(ctx)。
我的理解是主要看使用場景,如果你的服務對結果要求不是100%的成功,對併發的要求很高,那就可以使用go func(),go c.serve(ctx)也是類似,TCP本身就是不可靠的連接,HTTP也允許有極少量的失敗狀態。
如果你的服務裏面只是想讓多個goroutine處理你的數據,不希望這個goroutine太多影響你的主幹服務,或者你爲了提高數據處理效率,想讓多個goroutine去請求第三方的服務,這樣的話,就應該創建若干個goroutine去併發處理你的任務,也不建議直接go func(),goroutine數量不可控,會影響其他的主幹服務或者佔用服務器資源,如果請求第三方的服務,可能會因爲併發太高被限制,或者把第三方服務打掛。我們就遇到過這種情況。
總之,使用場景很重要,不是一概而論的。