简体   繁体   中英

Exit a goroutine after a timeout

I'm trying to write a program in go that is similar to cron with the addition that jobs are given a max runtime and if a function exceeds this duration, the job should exit. Here is my my whole code:

package main

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

type Job struct {
    ID         string
    MaxRuntime time.Duration
    Frequency  time.Duration
    Function   func()
}

func testFunc() {
    log.Println("OPP11")
    time.Sleep(7 * time.Second)
    log.Println("OP222")
}

func New(ID, frequency, runtime string, implementation func()) Job {
    r, err := time.ParseDuration(runtime)

    if err != nil {
        panic(err)
    }

    f, err := time.ParseDuration(frequency)

    if err != nil {
        panic(err)
    }

    j := Job{ID: ID, MaxRuntime: r, Frequency: f, Function: implementation}
    log.Printf("Created job %#v with frequency %v and max runtime %v", ID, f, r)
    return j
}

func (j Job) Run() {

    for range time.Tick(j.Frequency) {
        start := time.Now()
        log.Printf("Job %#v executing...", j.ID)
        done := make(chan int)
        //quit := make(chan int)
        //var wg sync.WaitGroup
        //wg.Add(1)

        go func() {
          j.Function()
          done <- 0
        }()

        select {
        case <-done:
            elapsed := time.Since(start)
            log.Printf("Job %#v completed in %v \n", j.ID, elapsed)
        case <-time.After(j.MaxRuntime):
            log.Printf("Job %#v halted after %v", j.ID, j.MaxRuntime)
            // here should exit the above goroutine
        }
    }

}

func main() {
    // create a new job given its name, frequency, max runtime
    // and the function it should run
    testJob := New("my-first-job", "3s", "5s", func() {
        testFunc()
    })

    testJob.Run()
}

What I'm trying to do is that in the second case in the select of the Run() function, it should exit the goroutine which is running the function. I tried to do this by wrapping the function in a for loop with a select statement which listens on a quit channel like this:

go func() {
    for {
        select {
        case <-quit:
            fmt.Println("quiting goroutine")
            return
        default:
            j.Function()
            done <- 0
        }
    }
}()

And then having quit <- 1 in the Run() function, but that doesnt seem to be doing anything. Is there a better of doing this?

As explained in the comments, the whole problem is that you want to cancel the execution of a function ( j.Function ) that isn't cancellable.

There's no way to "kill a goroutine". Goroutines work in a cooperative fashion. If you want to be able to "kill it", you need to ensure that the function running in that Goroutine has a mechanism for you to signal that it should stop what it's doing and return, letting the Goroutine that was running it finally terminate.

The standard way of indicating that a function is cancellable is by having it take a context.Context as its first param:

type Job struct {
  // ...
  Function func(context.Context)
}

Then you create the context and pass it to the j.Function . Since your cancellation logic is simply based on a timeout, there's no need to write all that select... case <-time.After(...) , as that is provided as built-in functionality with a context.Context :


func (j Job) Run() {
  for range time.Tick(j.Frequency) {
    go j.ExecuteOnce()
  }
}

func (j Job) ExecuteOnce() {
  log.Printf("Job %#v executing...", j.ID)
  ctx, cancel := context.WithTimeout(context.Background(), j.MaxRuntime)
  defer cancel()
  j.Function(ctx)
}

Now, to finish, you have to rewrite the functions that you're going to be passing to your job scheduler so that they take context.Context and, very importantly, that they use it properly and cancel whatever they're doing when the context is cancelled.

This means that if you're writing the code for those funcs and they will somehow block, you'll be responsible for writing stuff like:

select {
case <-ctx.Done():
  return ctx.Err()
case ...your blocking case...:
}

If your funcs are invoking 3rd party code, then that code needs to be aware of context and cancellation, and you'll need to pass down the ctx your funcs receive.

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