開篇
之前寫過一篇文章,它有個響亮的名字: Handling 1 Million Requests per Minute with Go
。 這是國外的一個作者寫的,我做了一篇說明。起的也是這個標題, 閱讀量是我最好的一篇,果然文章都是靠標題出彩的…..
今天偶然看到另一篇文章(原文在文末)。兩篇文章原理相似:有一批工作任務(job),通過工作池(worker-pool)的方式,達到多 worker
併發處理 job
的效果。
他們還是有很多不同的點,實現上差別也是蠻大的。
首先上一篇文章我放了一張圖片,大概就是上篇整體的工作流。
- 每個
worker
處理完任務就好,不關心結果,不對結果做進一步處理。 - 只要請求不停止,程式就不會停止,沒有控制機制,除非當機。
這篇文章不同點在於:
首先資料會從 generate
(生產資料)->併發處理資料->處理結果聚合。 圖大概是這樣的,
然後它可以通過 context.context
達到控制工作池停止工作的效果。
最後通過程式碼,你會發現它不是傳統意義上的 worker-pool
,後面會說明。
下圖能清晰表達整體流程了。
順便說一句,這篇文章實現的程式碼比 Handling 1 Million Requests per Minute with Go
的程式碼簡單多了。
首先看 job
。
package wpool
import (
"context"
)
type JobID string
type jobType string
type jobMetadata map[string]interface{}
type ExecutionFn func(ctx context.Context, args interface{}) (interface{}, error)
type JobDescriptor struct {
ID JobID
JType jobType
Metadata map[string]interface{}
}
type Result struct {
Value interface{}
Err error
Descriptor JobDescriptor
}
type Job struct {
Descriptor JobDescriptor
ExecFn ExecutionFn
Args interface{}
}
// 處理 job 邏輯,處理結果包裝成 Result 結果
func (j Job) execute(ctx context.Context) Result {
value, err := j.ExecFn(ctx, j.Args)
if err != nil {
return Result{
Err: err,
Descriptor: j.Descriptor,
}
}
return Result{
Value: value,
Descriptor: j.Descriptor,
}
}
這個可以簡單過一下。最終每個 job
處理完都會包裝成 Result
返回。
下面這段就是核心程式碼了。
package wpool
import (
"context"
"fmt"
"sync"
)
// 執行中的每個worker
func worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan Job, results chan<- Result) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
results <- job.execute(ctx)
case <-ctx.Done():
fmt.Printf("cancelled worker. Error detail: %v\n", ctx.Err())
results <- Result{
Err: ctx.Err(),
}
return
}
}
}
type WorkerPool struct {
workersCount int //worker 數量
jobs chan Job // 儲存 job 的 channel
results chan Result // 處理完每個 job 對應的 結果集
Done chan struct{} //是否結束
}
func New(wcount int) WorkerPool {
return WorkerPool{
workersCount: wcount,
jobs: make(chan Job, wcount),
results: make(chan Result, wcount),
Done: make(chan struct{}),
}
}
func (wp WorkerPool) Run(ctx context.Context) {
var wg sync.WaitGroup
for i := 0; i < wp.workersCount; i++ {
wg.Add(1)
go worker(ctx, &wg, wp.jobs, wp.results)
}
wg.Wait()
close(wp.Done)
close(wp.results)
}
func (wp WorkerPool) Results() <-chan Result {
return wp.results
}
func (wp WorkerPool) GenerateFrom(jobsBulk []Job) {
for i, _ := range jobsBulk {
wp.jobs <- jobsBulk[i]
}
close(wp.jobs)
}
整個 WorkerPool
結構很簡單。 jobs
是一個緩衝 channel
。每一個任務都會放入 jobs
中等待處理 woker
處理。
results
也是一個通道型別,它的作用是儲存每個 job
處理後產生的結果 Result
。
首先通過 New
初始化一個 worker-pool
工作池,然後執行 Run
開始執行。
func New(wcount int) WorkerPool {
return WorkerPool{
workersCount: wcount,
jobs: make(chan Job, wcount),
results: make(chan Result, wcount),
Done: make(chan struct{}),
}
}
func (wp WorkerPool) Run(ctx context.Context) {
var wg sync.WaitGroup
for i := 0; i < wp.workersCount; i++ {
wg.Add(1)
go worker(ctx, &wg, wp.jobs, wp.results)
}
wg.Wait()
close(wp.Done)
close(wp.results)
}
初始化的時候傳入 worker
數,對應每個 g
執行 work(ctx,&wg,wp.jobs,wp.results)
,組成了 worker-pool
。 同時通過 sync.WaitGroup
,我們可以等待所有 worker
工作結束,也就意味著 work-pool
結束工作,當然可能是因為任務處理結束,也可能是被停止了。
每個 job
資料來源是如何來的?
// job資料來源,把每個 job 放入到 jobs channel 中
func (wp WorkerPool) GenerateFrom(jobsBulk []Job) {
for i, _ := range jobsBulk {
wp.jobs <- jobsBulk[i]
}
close(wp.jobs)
}
對應每個 worker
的工作,
func worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan Job, results chan<- Result) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
results <- job.execute(ctx)
case <-ctx.Done():
fmt.Printf("cancelled worker. Error detail: %v\n", ctx.Err())
results <- Result{
Err: ctx.Err(),
}
return
}
}
}
每個 worker 都嘗試從同一個 jobs
獲取資料,這是一個典型的 fan-out
模式。 當對應的 g
獲取到 job
進行處理後,會把處理結果傳送到同一個 results channel
中,這又是一個 fan-in
模式。 當然我們通過 context.Context
可以對每個 worker
做停止執行控制。
最後是處理結果集合,
// 處理結果集
func (wp WorkerPool) Results() <-chan Result {
return wp.results
}
那麼整體的測試程式碼就是:
func TestWorkerPool(t *testing.T) {
wp := New(workerCount)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
go wp.GenerateFrom(testJobs())
go wp.Run(ctx)
for {
select {
case r, ok := <-wp.Results():
if !ok {
continue
}
i, err := strconv.ParseInt(string(r.Descriptor.ID), 10, 64)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
val := r.Value.(int)
if val != int(i)*2 {
t.Fatalf("wrong value %v; expected %v", val, int(i)*2)
}
case <-wp.Done:
return
default:
}
}
}
看了程式碼之後,我們知道,這並不是一個傳統意義的 worker-pool
。它並不像 Handling 1 Million Requests per Minute with Go
這篇文章一樣, 初始化一個真正的 worker-pool
,一旦接收到 job
,就嘗試從池中獲取一個 worker
, 把對應的 job
交給這個 work
進行處理,等 work
處理完畢,重新進行到工作池中,等待下一次被利用。
本作品採用《CC 協議》,轉載必須註明作者和本文連結