简体   繁体   中英

How to make this golang for-select code work?

Question

How to make the below code print "QUIT" after 3 seconds?

Code

package main

import (
    "fmt"
    "time"
)

func main() {
    quit := make(chan struct{})
    tasks := make(chan struct{})

    go func(){
      time.Sleep(1 * time.Second)
      tasks <- struct{}{}
    }()

    go func(){
      time.Sleep(3 * time.Second)
      quit <- struct{}{}
    }()

    for {
        select {
        case <-quit:
            fmt.Println("QUIT")
            return
        case <-tasks:
            fmt.Println("Doing")
            // do some long time jobs more than 10 seconds
            time.Sleep(10 * time.Second)
        }
    }
}

Observations

The above code prints "Doing". and sleep 10 seconds, then print "QUIT". How to interrupt this sleep, let it receive quit channel after 3 seconds and print "QUIT"?

It seems select is blocked by case tasks , and it will not receive from the quit channel after 3 seconds.

To signal end of an asynchronous task it is a best practice to close the channel, this is rather important to prevent many missuse that leads to various deadlocks. In your original code I would have written close(quite) rather than quit <- struct{}{} Remember, read on a closed does not block and always return the zero value, that is the trick.

Anyways, appart from this, an elegant way to solve your problem is to use a combination of both context.Context and time.After .

time.After will help you block on a selectable task set.

context.Context is better suited to handle this kind of signals.

https://play.golang.org/p/ZVsZw3P-YHd

package main

import (
    "context"
    "log"
    "time"
)

func main() {
    log.Println("start")
    defer log.Println("end")
    ctx, cancel := context.WithCancel(context.Background())
    tasks := make(chan struct{})

    go func() {
        time.Sleep(1 * time.Second) // a job
        tasks <- struct{}{}
    }()

    go func() {
        time.Sleep(3 * time.Second) // a job
        cancel()
    }()

    for {
        select {
        case <-ctx.Done():
            log.Println("QUIT")
            return
        case <-tasks:
            log.Println("Doing")
            // do some long time jobs more than 10 seconds
            select {
            case <-ctx.Done():
                return
            case <-time.After(time.Second * 10):
            }
        }
    }
}

The second job is running for 10 seconds, so it will block the loop for that time, and the signal you sent into the quit channel will not be received until this job is complete.

To interrupt the time-consuming job, maybe you can split it into another goroutine. Something like this would do:

go func() {
  for {
    select {
    case <-tasks:
      fmt.Println("Doing")
      // do some long time jobs more than 10 seconds
      time.Sleep(10 * time.Second)
    }   
  }   
}() 

<-quit
fmt.Println("QUIT")

It would be easier to use [sync.WaitGroup.

package main

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

func worker(msg string, duration time.Duration, doing bool, wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(duration)

    fmt.Println(msg)

    if doing {
        time.Sleep(10 * time.Second)
    }
}

func main() {

    var wg sync.WaitGroup
    msgs := [2]string{"QUIT", "Doing"}
    durations := [2]time.Duration{3 * time.Second, 1 * time.Second}
    doing := [2]bool{false, true}

    for i, msg := range msgs {
        wg.Add(1)

        go worker(msg, durations[i], doing[i], &wg)
    }

    wg.Wait()
}

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