简体   繁体   中英

Golang pause a loop in a goroutine with channels

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
}

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

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 case s 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.

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()
}

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 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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM