一個goroutine資料流任務的暫停⏸️與恢復⏯

阿狸不歌發表於2018-04-18

熟悉go程式設計的同學,肯定都用過time.Sleep來暫停goroutine的執行,但是time.Sleep無法實現按照事件暫停和恢復。換句話說,你一旦設定了暫停時間,那後面的事情就由不得你了,你設了暫停10秒就是10秒,設了1分鐘就是1分鐘,而且你沒法“永遠暫停”下去。

那麼現在問題就來了,我有一個資料流的播放任務,希望做到使用者點選暫停⏸️按鈕(發出暫停訊號)的時候,播放任務被暫停(不再輸出資料),而使用者點選恢復⏯按鈕(發出恢復訊號)的時候,播放任務從被暫停的地方繼續。這個暫停和恢復可以按照使用者的意願進行無數多次,暫停多久不能預先設定,而是看使用者的心情?

關於這個問題,我在網上找了挺久,並沒有找到特別好的例子,直到看到《Go併發程式設計實戰》這本書才得到了啟示。

Go併發程式設計實戰


select 語句

select語句是一個僅能被用於傳送和接收通道中的元素值的專用語句,一個select語句在被執行的時候會根據通道的值選擇執行其中的某一個分支,通道里沒有值的時候就走default分支(前提是你寫了這麼一個default分支)

現在利用select語句,我們可以寫出控制資料流的關鍵程式碼(為方便起見,在此我們把資料流簡化為一個迴圈輸出)

// 略去一些宣告
迴圈總次數 := 10000
執行訊號 := make(chan struct{})
for i := 0; i < 迴圈總次數; {
    select {
    case <-執行訊號:
        fmt.Printf("資料流播放到:%d\n", i)
        time.Sleep(1 * time.Second)    // 讓播放顯得慢一點,每次停1秒
        i++
    default:
        continue   // 暫停時保持空轉
    }
}

我們要暫停資料流播放的時候,只需要讓

執行訊號 = nil

即可。

此時,名為“執行訊號”的通道里沒有值,所以select語句只會走default分支,那麼整個for迴圈就進入到了一個無限空轉的過程中,直到下一次“執行訊號”裡再有值才會繼續資料流的播放。


Task 資料流的模擬

package task   // task.go
import "log"

// Task 任務結構體
type Task struct {
    任務編號      string
    迴圈總次數     int
    是否完成      bool
    工作訊號       <-chan struct{}
    工作訊號備份   <-chan struct{}
}

// NewTask 新任務
func NewTask(任務編號 string, 迴圈總次數 int) *Task {
    ch := make(chan struct{})
    defer close(ch)

    return &Task{
        任務編號:       任務編號,
        迴圈總次數:    迴圈總次數,
        工作訊號:       ch,
        工作訊號備份:   ch,
    }
}

// Play 開始執行
func (t *Task) Play() {
    log.Printf("啟動任務:%s , 次數: %d\n", t.任務編號, t.迴圈總次數)
    for i := 0; i < t.迴圈總次數; {      // 用迴圈模擬資料流
        select {
        case <-t.工作訊號:
            log.Printf("任務 %s @ %d\n", t.任務編號, i)
            time.Sleep(1 * time.Second)    // 讓模擬資料流走得慢點        
            i++
        default:    
            continue // ⚠️ 這個 continue 是Pause時執行 空轉的
        }
    }
    t.是否完成 = true
}

// Pause 暫停
func (t *Task) Pause() {
    if !t.是否完成 {
        t.工作訊號 = nil            
        log.Printf("%s # 暫停 \n", t.任務編號)
    } else {
        log.Printf("%s # 已執行完畢,不能暫停 \n", t.任務編號)
    }
}

// Resume 恢復執行
func (t *Task) Resume() {
    if !t.是否完成 {
        t.工作訊號 = t.工作訊號備份            
        log.Printf("%s # 恢復執行 \n", t.任務編號)
    } else {
        log.Printf("%s # 已執行完畢,不能恢復 \n", t.任務編號)
    }
}

主程式 模擬多次暫停與恢復

package main

import (
    "(...路徑)/task"    // 填寫task.go 檔案所在路徑
    "log"
    "sync"
    "time"
)

func main() {
    任務1名稱 := "task #15"
    t1 := task.NewTask(任務1名稱, 15)    // 建立一個資料流任務

    var wg sync.WaitGroup
    wg.Add(5)

    go func() {
        defer wg.Done()
        log.Println("任務啟動")
        t1.Play()  
        log.Println("播放結束")          
    }()

    go func() {
        defer wg.Done()
        time.Sleep(4 * time.Second)
        log.Printf("%s 啟動4秒後,試圖暫停\n", 任務1名稱)
        t1.Pause()
    }()

    go func() {
        defer wg.Done()
        time.Sleep(8 * time.Second)
        log.Printf("%s 啟動暫停8秒後,試圖恢復\n", 任務1名稱)
        t1.Resume()
    }()

    go func() {
        defer wg.Done()
        time.Sleep(12*time.Second)
        log.Printf("%s 啟動12秒後,再次暫停 \n", 任務1名稱)
        t1.Pause()
    }()

    go func() {
        defer wg.Done()
        time.Sleep(15 * time.Second)
        log.Printf("%s 啟動15秒後,再次恢復\n", 任務1名稱)
        t1.Resume()
    }()

    wg.Wait()
}

小結

以上模擬了一個最簡單的資料流的多次暫停與恢復,我們可以根據實際專案中的需要來擴充套件Task以及其呼叫。

Go併發程式設計實戰》寫的非常細緻,Go併發的原理講得很清楚,也有很多貼近實戰的程式碼,對於想深入學習Go語言的同學來說是本很好的教程。

相關文章