如何將golang的併發程式設計運用到實際開發

奇犽發表於2017-11-22

前言:這幾天在寫一個工具指令碼分析線上的大量的日誌檔案,本來應該是索然無味的一個工作,但是本著做到極致的原則,激發了我不斷思考如何優化。本文將從開發過程中的最開始版本,一點點講解優化的過程,最終用golang實現了一個類似java的worker執行緒池,收穫滿滿。

一,無腦開goroutine階段

1,任務背景 這個工具的作用簡單介紹如下:首先是線上的日誌量是非常龐大的,然後要去讀取日誌檔案的內容,然後一條條日誌項分析,匹配出想要的日誌項寫入檔案,再提取該檔案中的資料計算。日誌項格式模擬資料如下:

{
 "id":xx,"time":"2017-11-19","key1":"value1","key2":"value2".......  
}
複製程式碼

2,無腦開gorountine 先給下程式碼(只放核心程式碼,省略檔案操作和異常錯誤處理),再來說說這個階段的思路

var wirteChan = make(chan []byte) //用於寫入檔案
var waitgroup sync.WaitGroup //用於控制同步
func main(){
    //省略寫入檔案的開啟操作,畢竟我們主要講併發這塊
    
    //初始化寫入的channel
	InitWriter(outLogFileWriter)
	//接下來去遍歷每個日誌檔案,每讀出一個日誌項就開一個gorountine去處理
	for _,file := range logDir {
	    if file.IsDir() {
			continue
		} else {
			HandlerFile(arg.dir + "/" + file.Name()) //處理每個檔案
		}
	}
}
/**
 * 初始化Writer的channel
 */
func InitWriter(outLogFileWriter *bufio.Writer) {
	go func() {
		for data := range wirteChan {
			nn, err := outLogFileWriter.Write(data)
		}
	}()
}
//處理每個檔案,然後開G去處理每個日誌項
func HandlerFile(fileName string) {
	file, err := os.Open(fileName)
	defer file.Close()
	br := bufio.NewReader(file)
	for {
		data, err := br.ReadBytes('\n')
		if err == io.EOF {
			break
		} else {
			go Handler(data) //每次開一個G去處理,處理完寫入writeChannel
		}
	}
}
複製程式碼

解析:寫部落格很不喜歡放大篇幅程式碼,所以上面給的只是重要的程式碼,上面程式碼註釋有說到的也不重複說了。好了,我們來想想上面的程式碼有什麼問題?我們現在就假設我們就只有一個非常大的檔案,檔案中每個記錄項都無腦開一個G去執行。那麼,執行一下,我們會發現,好慢呀~。問題出在哪裡呢?首先我們無法控制G的數量,其次日誌檔案非常大,這樣執行下來,G的數量是非常龐大的,多個G要往一個channel中寫資料,那麼也會發生嚴重的阻塞。種種原因,導致了這個方法是不適用的

二,加入帶緩衝的任務佇列

1,任務佇列 在上面我們說到,我們無法控制任務的數量,那麼,我在這裡就加入了一個任務佇列,來對任務進行排隊,同時可以控制任務的數量。上程式碼:

/**
 * Job結構體,包含要處理的資料和處理函式(這個可根據需要修改)
 */
type Job struct {
	Data []byte
	Proc func([]byte)
}
//Job佇列,儲存要做的Job,將每個任務打包成Job傳送到這裡
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//啟動處理函式處理
func Handler(Data []byte) {
    for range job := <-Queue {
        job.Proc(Data)
    }
}
複製程式碼

解析:在這個時候,抽象出來了任務模型Job,由於函式呼叫其實就是函式地址加函式引數,所以我們可以將處理函式也放進Job中。然後讓處理函式去處理就行了。想到這裡,稍微有點佩服自己了,接著興致勃勃的執行一下。嗯,好像沒快多少(其實這個取決了你的處理函式,就是Job中的Proc)。What?冷靜下來分析一下,真覺得自己真可愛。我僅僅是對任務進行了包裝,然後用了一個帶緩衝的任務佇列,由於建立的Job遠遠大於單個M的處理能力,帶緩衝只是稍微把問題拖後了一點。

三,Job/Worker模型

其實寫到這裡,心裡對如何優化已經有點B數了。我想起了java中的執行緒池的概念,我可以建立一個執行緒池,然後池中包含多個worker(數量可以指定),每個worker去佇列中取任務處理,處理完則繼續取任務。同時為了提高通用性,引數型別都改為了interface{}。好了,接下來看看程式碼,這裡的程式碼都很關鍵,所以就全部放上來了

如何將golang的併發程式設計運用到實際開發

type Job struct {
	Data interface{}
	Proc func(interface{})
}
//Job佇列,儲存要做的Job
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//Woker,用來從Job佇列中取出Job執行
type Worker struct {
	WokerPool  chan chan Job //表示屬於哪個Worker池,同時接收JobChannel註冊
	JobChannel chan Job      //任務管道,通過這個管道獲取任務執行
	Quit       chan bool     //用來停止Worker
}
//新建一個Worker,需要傳入Worker池引數
func NewWorker(wokerPool chan chan Job) Worker {
	return Worker{
		WokerPool:  wokerPool,
		JobChannel: make(chan Job),
		Quit:       make(chan bool),
	}
}
//Worker的啟動:包含:(1) 把該worker的JobChannel註冊到WorkerPool中去  (2) 監聽JobChannel上有沒有新的任務到來 (3) 監聽是否受到關閉的請求
func (worker Worker) Start() {
	go func() {
		for {
			worker.WokerPool <- worker.JobChannel //每次做完任務後就重新註冊上去通知本worker又處於可用狀態了
			select {
			case job := <-worker.JobChannel:
				job.Proc(job.Data)
			case quit := <-worker.Quit: //接收到關閉資訊,直接退出即可
				if quit {
					return
				}
			}
		}
	}()
}
//Worker的關閉:只要傳送一個關閉訊號即可
func (worker Worker) Stop() {
	go func() {
		worker.Quit <- true
	}()
}
//管理Worker的排程器,包含最大worker數量和workerpool
type Dispatcher struct {
	MaxWorker  int
	WorkerPool chan chan Job
}

//啟動一個排程器
func (dispatcher *Dispatcher) Run() {
	//啟動maxworker個worker
	for i := 0; i < dispatcher.MaxWorker; i++ {
		worker := NewWorker(dispatcher.WorkerPool)
		worker.Start()
	}
	//接下來啟動排程服務
	go dispatcher.dispatch()
}

func (dispatcher *Dispatcher) dispatch() {
	for {
		select {
		case job := <-JobQueue:
			go func(job Job) {
				jobChannel := <-dispatcher.WorkerPool //獲取一個可用的worker
				jobChannel <- job                     //將該job傳送給該worker
			}(job)
		}
	}
}

//新建一個排程器
func NewDispatcher(maxWorker int) *Dispatcher {
	workerPool := make(chan chan Job, maxWorker)
	return &Dispatcher{
		WorkerPool: workerPool,
		MaxWorker:  maxWorker,
	}
}
複製程式碼

解析:程式碼中每句都註釋得非常清楚了,就不重複了。我們可以通過這樣來開啟這個模型:dispatcher := NewDispatcher(MaxWorker) dispatcher.Run()。有一點需要強調的是,處理函式這塊需要根據自己的業務去寫,然後和資料打包成Job再發給JobQueue就行了。接著我執行了我的指令碼,幾十G的檔案經過三輪的處理函式(就是說我需要三輪處理,每輪處理都根據上輪的結果)耗時在三分鐘到四分鐘之間,而且CPU佔用率等也不高。對於耗時高的,可以使用pprof工具分析一下到底慢在了哪裡

四,總結

因為之前剛學了golang的併發原理,然後剛好有這個任務,於是自己就開始了從零一點點的摸索和優化,整個工具寫完,自己對golang的併發的理解又更加的深入了,而且對鎖,檔案操作等也熟悉了起來。收穫很多東西,所以我鼓勵學習一個新東西,不能只懂原理,還要自己多動手一下,這樣才牢固。其實這個模型還是存在一些不足之處,後續會繼續優化。 期間也參考了一些很不錯的部落格,在這裡也表示感謝。

相關文章