Golang實現ForkJoin小文

tensor發表於2019-04-21

如何用Golang實現一個簡單的ForkJoin框架

扔上我的專案地址

go-fork-join

簡易原理

  • 什麼是ForkJoin

    接觸到ForkJoin框架是因為學習Java中的Stream中的並行流,並行流的底層就是藉助ForkJoin框架

    ForkJoin框架更適合現在CPU多核的機器,一般用於處理可以將一個大任務分解成數個互相沒有依賴性的小任務,利用分治的策略,將任務不斷變小,將這些小任務分發到CPU的核中,將子任務並行執行,大大加快任務處理速度

    具體的很多部落格上說的都很不錯,這裡也不細說了,給幾個我當時學習的部落格地址吧

  • 任務偷竊

    任務偷竊演算法其實就是Worker可以從自己對應的工作佇列頭部或者其他Worker的工作佇列尾部獲取元素。

    每次在輪詢任務佇列時,先從每個Worker對應的任務佇列中去獲取任務,如果發現任務佇列此時沒有待處理的任務,那麼這個時候就會採用隨機選取策略,隨機選擇一個Worker對應的工作佇列,去竊取它的任務

  • Join子任務結果

    在Java中需要去不斷的獲取任務的執行情況,如果任務執行完就返回任務處理的結果;而在Golang中,由於chan的存在,使得Java的Future模式非常容易實現,只需要任務Join的時候去讀取通道就可以,因為當我們把chan的cap設定為1時,如果通道中沒有資料,讀取一方是會被阻塞等待的

func (f *ForkJoinTask) Join() (bool, interface{}) {
	for {
		select {
		case data, ok := <-f.result:
			if ok {
				return true, data
			}
		case <-f.ctx.Done():
			panic(f.taskPool.err)
		}
	}
}
複製程式碼

核心程式碼

任務佇列

對任務佇列進行遍歷操作。任務佇列不止一個,而是存在多個任務佇列,每次都會從這些任務佇列中獲取一個任務出來,如果任務存在則將任務包裝成一個結構體;在獲取到任務後,就是獲取一個任務的執行者worker了,隨後將包裝好的任務送入Worker的chan通道中非同步傳送任務

func (fp *ForkJoinPool) run(ctx context.Context) {
	go func() {
		wId := int32(0)
		for {
			select {
			case <-ctx.Done():
				fmt.Printf("here is err")
				fp.err = fp.wp.err
				return
			default:
				hasTask, job, ft := fp.taskQueue.dequeueByTali(wId)
				if hasTask {
					fp.wp.Submit(ctx, &struct {
						T Task
						F *ForkJoinTask
						C context.Context
					}{T: job, F: ft, C: ctx})
				}
				wId = (wId + 1) % fp.cap
			}
		}
	}()
}
複製程式碼

獲取一個Worker

ForkJoin初始化的時候,根據CPU核數對Worker池進行初始化操作

func newPool(ctx context.Context, cancel context.CancelFunc) *Pool {
	...
	wCnt := runtime.NumCPU()
	for i := 0; i < wCnt; i ++ {
		w := newWorker(p)
		w.run(ctx)
		p.workers = append(p.workers, w)
	}
	...
}
複製程式碼

隨後,處理任務肯定需要一個對應的worker去執行的,因此每次在獲取worker時,會先去worker池中判斷是否還存在空閒的worker,如果存在就直接獲取一個worker,否則直接建立一個worker進行接受任務

func (p *Pool) retrieveWorker(ctx context.Context) *Worker {

	var w *Worker

	idleWorker := p.workers

	if len(idleWorker) >= 1 {
		p.lock.Lock()
		n := len(idleWorker) - 1
		w = idleWorker[n]
		p.workers = idleWorker[:n]
		p.lock.Unlock()
	} else {
		if cacheWorker := p.workerCache.Get(); cacheWorker != nil {
			w = cacheWorker.(*Worker)
		} else {
			w = &Worker{
				pool: p,
				job: make(chan *struct {
					T Task
					F *ForkJoinTask
					C context.Context
				}, 1),
			}
		}
		w.run(ctx)
	}
	return w
}
複製程式碼

Worker

真正執行任務的物件,每個worker繫結一個goruntine,並且有一個chan通道,用於非同步接收任務以及在goruntine中非同步將任務取出並執行;當任務執行完後,將worker返回到worker池中

func (w *Worker) run(ctx context.Context) {
	go func() {

		var tmpTask *ForkJoinTask

		defer func() {
			if p := recover(); p != nil {
				w.pool.panicHandler(p)
				if tmpTask != nil {
					w.pool.err = p
					close(tmpTask.result)
				}
			}
		}()

		for {
			select {
			case <-ctx.Done():
				fmt.Println("An exception occurred and the task has stopped")
				return
			default:
				for job := range w.job {
					if job == nil {
						w.pool.workerCache.Put(w)
						return
					}
					tmpTask = job.F
					job.F.result <- job.T.Compute()
					w.pool.releaseWorker(w)
				}
			}
		}
	}()
}
複製程式碼

成果

benchtest

正在改進的地方

  • 任務偷竊演算法

    目前v0.1的任務偷竊演算法並不能說像Java的ForkJoin那樣,支援兩個worker同時從一個佇列中獲取任務,而是在獲取任務的時候鎖住整個佇列,因此併發效能不太好,目前正在採用CAS去替換悲觀鎖,實現兩個Worker可同時讀取一個佇列中的資料,如果兩Worker同時向一個長度只有1的任務佇列獲取元素,則樂觀鎖上升為悲觀鎖進行控制

  • Worker數量控制

    目前的Worker數量會隨著任務的不斷分解而不斷建立,如果任務分解過深可能會導致建立大量的Worker,因此還需要繼續理解ForkJoin的關於執行緒資源的排程

相關文章