4

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

William Poussier
  • 1,628
  • 5
  • 20
  • 30

4 Answers4

11

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
7

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.

Community
  • 1
  • 1
  • 2
    you made it so complex and complicated. –  Aug 08 '16 at 10:56
  • You may want to create a type (say, `type Player`) to encapsulate the variables (as private fields) and closures (as public methods, like `Start()`, `Pause()`, `Play()`, etc.). The OP showed me his original problem, and there are more states in it. Therefore, the `select`-`case` must be "flattened" in order to make it scalable. – Siu Ching Pong -Asuka Kenji- Aug 08 '16 at 11:04
0

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()
}
dapangmao
  • 2,727
  • 3
  • 22
  • 18
0

The above code when converted to a class becomes more useful and allows multiple players concurrently when used in a service. Below is same example written as a class.

// The class methods
type Player interface {
    Play()
    Pause()
    Stop()
    Routine()
}
// data handled by class as required
type action struct {
    uid         string
    command     chan string
    wg          *sync.WaitGroup
    i           int
}

// A map to hold instances of above class
var playList = make(map[string]action)
// Global object of type action
var playAction action

// implementation of methods
func (ch action) Play() {
    fmt.Println(ch.uid) // display unique id 
    ch.command <- "play" // update the channel status
}
func (ch action) Pause() {
    fmt.Println(ch.uid)
    ch.command <- "pause"
}
func (ch action) Stop() {
    fmt.Println(ch.uid)
    ch.command <- "stop"
}

func (ch action) Routine() {
    defer ch.wg.Done()
    fmt.Println(ch.uid)
    var status = "play" // initial status is always play
    for {
        select {
        case cmd := <-ch.command:
            fmt.Println(cmd)
            switch cmd {
            case "stop":
                return
            case "pause":
                status = "pause"
            default:
                status = "play"
            }
        default:
            if status == "play" {
                work()
            }
        }
    }
}
func main() {
    // This could be part of some service
    // some unique id
    var uid string ="Object1"
    var wg sync.WaitGroup
    wg.Add(1)
    command := make(chan string)
    i := 0
    playAction = action{uid,command, &wg, i}
    playList[uid] = playAction
    go playList[uid].Routine()
    command <- "play" // update the channel
}