Golang在带有通道的goroutine中暂停循环

I have a function that is launched as a goroutine:

func (bt *BlinkyTape) finiteLoop(frames []Frame, repeat int, delay time.Duration) {
    bt.isPlaying = true
L:
    for i := 0; i < repeat; i++ {
        select {
        case <-bt.stop:
            break L
        default:
            bt.playFrames(frames, delay)
        }
    }
    bt.isPlaying = false
}

This function uses channels so it is possible to break the loop (loop can be finite or infinite)

What I would like to implement is a way to pause the execution of the loop and of course being able to resume it.

I was thinking to add another case to the select condition where I listen on another channel pause. If the case is executed, it enter in a new infinite loop that does nothing. Then it will need the same system as previously with a resume channel to break this loop.

What do you think ? Is there a better way to achieve what I need ?

Regards

The problem:

Amd's answer is essentially a state machine built with Go's select statement. One problem I noticed is that when you add more functionalities (like "fast forward", "slow motion", etc.), more cases have to be added to the select in the "pause" case.

Receiving from nil channels:

In Go, receiving from (or sending to) a nil channel results in "blocking forever". This in fact is a very important feature to implement the following trick: In a for-select pattern, if you set a case channel to nil, the corresponding case will not be matched in the next iteration. In other words, the case is "disabled".

Receiving from closed channels:

In Go, receiving from a closed channel always returns immediately. Therefore, you may replace your default case by a variable holding a closed channel. When the variable holds the closed channel, it behaves like the default case; However, when the variable holds nil, the case is never matched, having the "pause" behavior.

My ideas:

  • Modify your default case: read from a closed channel instead. (explained above);
  • Make a backup of the closed channel. When pause is needed, set the "default case channel" to nil; when play is needed, set it to the backup;
  • Make a "continue" channel to ask the select statement to re-read the variables after assignment;
  • In fact, the "quit" channel can be reused as the "continue" channel: send struct{}{} when "continue" is needed; close() when "quit" is needed;
  • Encapsulate the resources in closures, and ensure that cleanup is done;
  • Ensure that when start() is not yet called, no channels or go routines are created, in order to prevent leaks.

My implementation (also available at The Go Playground):

package main

import "fmt"
import "time"
import "sync"

func prepare() (start, pause, play, quit, wait func()) {
    var (
        chWork       <-chan struct{}
        chWorkBackup <-chan struct{}
        chControl    chan struct{}
        wg           sync.WaitGroup
    )

    routine := func() {
        defer wg.Done()

        i := 0
        for {
            select {
            case <-chWork:
                fmt.Println(i)
                i++
                time.Sleep(250 * time.Millisecond)
            case _, ok := <-chControl:
                if ok {
                    continue
                }
                return
            }
        }
    }

    start = func() {
        // chWork, chWorkBackup
        ch := make(chan struct{})
        close(ch)
        chWork = ch
        chWorkBackup = ch

        // chControl
        chControl = make(chan struct{})

        // wg
        wg = sync.WaitGroup{}
        wg.Add(1)

        go routine()
    }

    pause = func() {
        chWork = nil
        chControl <- struct{}{}
        fmt.Println("pause")
    }

    play = func() {
        fmt.Println("play")
        chWork = chWorkBackup
        chControl <- struct{}{}
    }

    quit = func() {
        chWork = nil
        close(chControl)
        fmt.Println("quit")
    }

    wait = func() {
        wg.Wait()
    }

    return
}

func sleep() {
    time.Sleep(1 * time.Second)
}

func main() {
    start, pause, play, quit, wait := prepare()

    sleep()
    start()
    fmt.Println("start() called")

    sleep()
    pause()

    sleep()
    play()

    sleep()
    pause()

    sleep()
    play()

    sleep()
    quit()

    wait()
    fmt.Println("done")
}

Extras:

If you really want to implement "fast forward" and "slow motion", simply:

  • Refactor the magic 250 to a variable;
  • Return one more closure from prepare() used to set the variable and send struct{}{} to chControl.

Please be reminded that "race conditions" are ignored for this simple case.

References:

https://golang.org/ref/spec#Send_statements

A send on a closed channel proceeds by causing a run-time panic. A send on a nil channel blocks forever.

https://golang.org/ref/spec#Receive_operator

Receiving from a nil channel blocks forever. A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.

https://golang.org/ref/spec#Close

Sending to or closing a closed channel causes a run-time panic. Closing the nil channel also causes a run-time panic. After calling close, and after any previously sent values have been received, receive operations will return the zero value for the channel's type without blocking. The multi-valued receive operation returns a received value along with an indication of whether the channel is closed.

Pause a loop in a goroutine with channels, use play, pause and quit channels like this working sample code:

package main

import "fmt"
import "time"
import "sync"

func routine() {
    for {
        select {
        case <-pause:
            fmt.Println("pause")
            select {
            case <-play:
                fmt.Println("play")
            case <-quit:
                wg.Done()
                return
            }
        case <-quit:
            wg.Done()
            return
        default:
            work()
        }
    }
}

func main() {
    wg.Add(1)
    go routine()

    time.Sleep(1 * time.Second)
    pause <- struct{}{}

    time.Sleep(1 * time.Second)
    play <- struct{}{}

    time.Sleep(1 * time.Second)
    pause <- struct{}{}

    time.Sleep(1 * time.Second)
    play <- struct{}{}

    time.Sleep(1 * time.Second)
    close(quit)

    wg.Wait()
    fmt.Println("done")
}

func work() {
    time.Sleep(250 * time.Millisecond)
    i++
    fmt.Println(i)
}

var play = make(chan struct{})
var pause = make(chan struct{})
var quit = make(chan struct{})
var wg sync.WaitGroup
var i = 0

output:

1
2
3
4
pause
play
5
6
7
8
pause
play
9
10
11
12
done

Modified according to @user6169399 above that uses a channel

package main

import (
    "fmt"
    "time"
    "sync"
)

var i int

func work() {
    time.Sleep(250 * time.Millisecond)
    i++
    fmt.Println(i)
}

func routine(command <- chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    var status = "play"
    for {
        select {
        case cmd := <- command:
            fmt.Println(cmd)
            switch cmd {
            case "stop":
                return
            case "pause":
                status = "pause"
            default:
                status = "play"
            }
        default:
            if status == "play" {
                work()
            }
        }
    }
}


func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    command := make(chan string)
    go routine(command, &wg)
    time.Sleep(1 * time.Second)
    command <- "pause"
    time.Sleep(1 * time.Second)
    command <- "play"
    time.Sleep(1 * time.Second)
    command <- "stop"
    wg.Wait()
}