Goroutine + Channel 實踐

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

背景

在最近開發的項目中,後端需要編寫許多提供HTTP介面的API,另外技術選型相對寬鬆,因此選擇Golang + Beego架構進行開發。之所以選擇Golang,主要是考慮到開發的模組,都需要接受瞬時大並發、請求需要經曆多個步驟、處理時間較長、無法同步立即返回結果的情境,Golang的goroutine以及channel所提供的語言層級的特性,正好可以滿足這方面的需要。

goroutine不同於thread,threads是作業系統中的對於一個獨立運行執行個體的描述,不同作業系統,對於thread的實現也不盡相同;但是,作業系統並不知道goroutine的存在,goroutine的調度是有Golang運行時進行管理的。啟動thread雖然比process所需的資源要少,但是多個thread之間的環境切換仍然是需要大量的工作的(寄存器/Program Count/Stack Pointer/...),Golang有自己的調度器,許多goroutine的資料都是共用的,因此goroutine之間的切換會快很多,啟動goroutine所耗費的資源也很少,一個Golang程式同時存在幾百個goroutine是很正常的。

channel,即“管道”,是用來傳遞資料(叫訊息更為合適)的一個資料結構,即可以從channel裡面塞資料,也可以從中擷取資料。channel本身並沒有什麼神奇的地方,但是channel加上了goroutine,就形成了一種既簡單又強大的請求處理模型,即N個工作goroutine將處理的中間結果或者最終結果放入一個channel,另外有M個工作goroutine從這個channel拿資料,再進行進一步加工,通過組合這種過程,從而勝任各種複雜的業務模型。

模型

自己在實踐的過程中,產生了幾種通過goroutine + channel實現的工作模型,本文分別對這些模型進行介紹。

V0.1: go關鍵字

直接加上go關鍵字,就可以讓一個函數脫離原先的主函數獨立運行,即主函數直接繼續進行剩下的操作,而不需要等待某個十分耗時的操作完成。比如我們在寫一個服務模組,接收到前端請求之後,然後去做一個比較耗時的任務。比如下面這個:

func (m *SomeController) PorcessSomeTask() {    var task models.Task    if err := task.Parse(m.Ctx.Request); err != nil {        m.Data["json"] = err         m.ServeJson()        return    }    task.Process()    m.ServeJson()

如果Process函數需要耗費大量時間的話,這個請求就會被block住。有時候,前端只需要發出一個請求給後端,並且不需要後端立即所處響應。遇到這樣的需求,直接在耗時的函數前面加上go關鍵字就可以將請求之間返回給前端了,保證了體驗。

func (m *SomeController) PorcessSomeTask() {    var task models.Task    if err := task.Parse(m.Ctx.Request); err != nil {        m.Data["json"] = err         m.ServeJson()        return    }    go task.Process()    m.ServeJson()

不過,這種做法也是有許多限制的。比如:

  • 只能在前端不需要立即得到後端處理的結果的情況下
  • 這種請求的頻率不應該很大,因為目前的做法沒有控制並發

V0.2: 並發控制

上一個方案有一個缺點就是無法控制並發,如果這一類請求同一個時間段有很多的話,每一個請求都啟動一個goroutine,如果每個goroutine中還需要使用其他系統資源,消耗將是不可控的。

遇到這種情況,一個解決方案是:將請求都轉寄給一個channel,然後初始化多個goroutine讀取這個channel中的內容,並進行處理。假設我們可以建立一個全域的channel

var TASK_CHANNEL = make(chan models.Task)

然後,啟動多個goroutine:

for i := 0; i < WORKER_NUM; i ++ {    go func() {        for {            select {            case task := <- TASK_CHANNEL:                task.Process()            }        }    } ()}

服務端接收到請求之後,將任務傳入channel中即可:

func (m *SomeController) PorcessSomeTask() {    var task models.Task    if err := task.Parse(m.Ctx.Request); err != nil {        m.Data["json"] = err         m.ServeJson()        return    }    //go task.Process()    TASK_CHANNEL <- task    m.ServeJson()}

這樣一來,這個操作的並發度就可以通過WORKER_NUM來控制了。

V0.3: 處理channel滿的情況

不過,上面方案有一個bug:那就是channel初始化時是沒有設定長度的,因此當所有WORKER_NUM個goroutine都正在處理請求時,再有請求過來的話,仍然會出現被block的情況,而且會比沒有經過最佳化的方案還要慢(因為需要等某一個goroutine結束時才能處理它)。因此,需要在channel初始化時增加一個長度:

var TASK_CHANNEL = make(chan models.Task, TASK_CHANNEL_LEN)

這樣一來,我們將TASK_CHANNEL_LEN設定得足夠大,請求就可以同時接收TASK_CHANNEL_LEN個請求而不用擔心被block。不過,這其實還是有問題的:那如果真的同時有大於TASK_CHANNEL_LEN個請求過來呢?一方面,這就應該算是架構方面的問題了,可以通過對模組進行擴容等操作進行解決。另一方面,模組本身也要考慮如何進行“優雅降級了”。遇到這種情況,我們應該希望模組能夠及時告知調用方,“我已經達到處理極限了,無法給你處理請求了”。其實,這種需求,可以很簡單的在Golang中實現:如果channel發送以及接收操作在select語句中執行並且發生阻塞,default語句就會立即執行

select {case TASK_CHANNEL <- task:    //do nothingdefault:    //warnning!    return fmt.Errorf("TASK_CHANNEL is full!")}//...

V0.4: 接收發送給channel之後返回的結果

如果處理常式比較複雜的時候,通常都會出現在一個goroutine中,還會發送一些中間處理的結果發送給其他goroutine去做,經過多道“工序”才能最終將結果產出。

那麼,我們既需要把某一個中間結果發送給某個channel,也要能擷取到處理這次請求的結果。解決的方法是:將一個channel執行個體包含在請求中,goroutine處理完成後將結果寫回這個channel

type TaskResponse struct {    //...}type Task struct {    TaskParameter   SomeStruct    ResChan         *chan TaskResponse}//...task := Task {    TaskParameter   : xxx,    ResChan         : make(chan TaskResponse),}TASK_CHANNEL <- taskres := <- task.ResChan//...

(這邊可能會有疑問:為什麼不把一個複雜的任務都放在一個goroutine中依次的執行呢?是因為這裡需要考慮到不同子任務,所消耗的系統資源不盡相同,有些是CPU集中的,有些是IO集中的,所以需要對這些子任務設定不同的並發數,因此需要經由不同的channel + goroutine去完成。)

V0.5: 等待一組goroutine的返回

將任務經過分組,交由不同的goroutine進行處理,最終再將每個goroutine處理的結果進行合并,這個是比較常見的處理流程。這裡需要用到WaitGroup來對一組goroutine進行同步。一般的處理流程如下:

var wg sync.WaitGroupfor i := 0; i < someLen; i ++ {    wg.Add(1)    go func(t Task) {        defer wg.Done()        //對某一段子任務進行處理    } (tasks[i])}wg.Wait()//處理剩下的工作

V0.6: 逾時機制

即使是複雜、耗時的任務,也必須設定逾時時間。一方面可能是業務對此有時限要求(使用者必須在XX分鐘內看到結果),另一方面模組本身也不能都消耗在一直無法結束的任務上,使得其他請求無法得到正常處理。因此,也需要對處理流程增加逾時機制。

我一般設定逾時的方案是:和之前提到的“接收發送給channel之後返回的結果”結合起來,在等待返回channel的外層添加select,並在其中通過time.After()來判斷逾時。

task := Task {    TaskParameter   : xxx,    ResChan         : make(chan TaskResponse),}select {case res := <- task.ResChan:    //...case <- time.After(PROCESS_MAX_TIME):    //處理逾時}

V0.7: 廣播機制

既然有了逾時機制,那也需要一種機制來告知其他goroutine結束手上正在做的事情並退出。很明顯,還是需要利用channel來進行交流,第一個想到的肯定就是向某一個chan發送一個struct即可。比如執行任務的goroutine在參數中,增加一個chan struct{}類型的參數,當接收到該channel的訊息時,就退出任務。但是,還需要解決兩個問題:

  1. 怎樣能在執行任務的同時去接收這個訊息呢?
  2. 如何通知所有的goroutine?

對於第一個問題,比較優雅的作法是:使用另外一個channel作為函數d輸出,再加上select,就可以一邊輸出結果,一邊接收退出訊號了。

另一方面,對於同時有未知數目個執行goroutine的情況,一次次調用done <-struct{}{},顯然無法實現。這時候,就會用到golang對於channel的tricky用法:當關閉一個channel時,所有因為接收該channel而阻塞的語句會立即返回。範例程式碼如下:

// 執行方func doTask(done <-chan struct{}, tasks <-chan Task) (chan Result) {    out := make(chan Result)    go func() {        // close 是為了讓調用方的range能夠正常退出        defer close(out)        for t := range tasks {            select {            case result <-f(task):            case <-done:                return            }        }    }()    return out}// 調用方func Process(tasks <-chan Task, num int) {    done := make(chan struct{})    out := doTask(done, tasks)    go func() {        <- time.After(MAX_TIME)        //done <-struct{}{}        //通知所有的執行goroutine退出        close(done)    }()    // 因為goroutine執行完畢,或者逾時,導致out被close,range退出    for res := range out {        fmt.Println(res)        //...    }}

參考

  • http://blog.golang.org/pipelines
  • https://gobyexample.com/non-blocking-channel-operations

-- EOF --

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.