2
\$\begingroup\$

I have a scenario where I need to acquire lock on a resource inside a goroutine and release the lock (only once) after a timeout or when the goroutine is done with it's job.

I came up with a way to achieve this, but I'm not sure if it's best way.

package main
import (
 "fmt"
 "runtime/debug"
 "strings"
 "time"
)
func testTimeoutRoutine() {
 // Handle panic in goroutine so the caller is not impacted
 defer func() {
 if r := recover(); r != nil {
 defer debug.PrintStack()
 if err, ok := r.(error); ok {
 if strings.Contains(err.Error(), "send on closed channel") {
 fmt.Println("goroutine job done after timeout")
 return
 }
 }
 fmt.Printf("Panic in goroutine => %v", r)
 }
 }()
 jobDone := make(chan bool)
 go func() {
 defer close(jobDone)
 timer := time.After(2 * time.Second)
 for {
 select {
 case <-timer:
 fmt.Println("goroutine timed out before it's job is done")
 // Release lock on the resource
 return
 case <-jobDone:
 fmt.Println("goroutine job is done")
 // Release lock on the resource
 return
 }
 }
 }()
 // Acquire lock on a resource
 // Do some job
 time.Sleep(5 * time.Second)
 jobDone <- true
}
func main() {
 go testTimeoutRoutine()
 // Let's wait for the goroutine to do it's job or timeout
 time.Sleep(10 * time.Second)
 fmt.Println("\n\nmain done")
}

Is there a better way of handling this?

Code here: https://go.dev/play/p/Jigu8MK2aEA

toolic
14.3k5 gold badges29 silver badges200 bronze badges
asked Aug 23, 2024 at 2:50
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Please edit your question so that the title describes the purpose of the code, rather than its mechanism. We really need to understand the motivational context to give good reviews. It's best to describe what value this code provides to its user. \$\endgroup\$ Commented Aug 23, 2024 at 8:03

2 Answers 2

3
\$\begingroup\$

Use a context with timeout:

go func() {
 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 defer cancel()
 // Lock the mutex
 mu.Lock()
 defer mu.Unlock()
 // Do work
 // every now and then, check if timed out
 if ctx.Err()!=nil {
 return
 }
 // Do more work
 // When goroutine ends, mutex will be unlocked.
}()
answered Aug 23, 2024 at 3:09
\$\endgroup\$
12
  • \$\begingroup\$ // every now and then, check if timed out @Burak How do yo propose I do this? It has to be done via another go routine right? Which is what I sort of did. \$\endgroup\$ Commented Aug 23, 2024 at 19:14
  • \$\begingroup\$ You can check if something timed out using another goroutine. That's what context timeout does, so you don't need another goroutine. The problem with your code is that the other goroutine detects the timeout, but the process that timed out continues running to completion. You have to check often to detect timeout happened. Unlocking a mutex like you did will cause race, because the critical section is still running even though it timed out and mutex is released. \$\endgroup\$ Commented Aug 23, 2024 at 19:33
  • \$\begingroup\$ I understand the race condition part which I totally missed, thanks a lot for that. But, I still don't get how I should periodically check the context if it timed out. Are you saying I should keep inserting if conditions randomly at different places to detect timeout? \$\endgroup\$ Commented Aug 24, 2024 at 2:13
  • \$\begingroup\$ Yes. If the long running task is a for-loop, it is usually done once every iteration. If it is sequential, you can do it at certain milestones. \$\endgroup\$ Commented Aug 24, 2024 at 3:11
  • \$\begingroup\$ I still feel this approach is inefficient and incomplete. The reason to use timeout is to detect if there is anything in the code that got stuck and there by essentially kill the goroutine. To achieve this, we have to use a separate goroutine where the release is locked and something to kill the goroutine (which I feel is through raising panic()). But, your approach doesn't solve the problem as the code that might get stuck could be between any two if conditions that check for the timeout. \$\endgroup\$ Commented Aug 24, 2024 at 14:43
0
\$\begingroup\$

A better way to do this would be with the builtin context library. In general, if normal behavior of your program requires a panic to work, something's wrong.

A context.Context is a bit complicated, but the feature we care about here is that they have a channel, .Done(), which is closed when the Context is cancelled. Closed channels immediately send zero values to all receivers, satisfying the ctx.Done() branch of the select statement.

By instantiating a Context and then creating a child Context via WithTimeout, we create one which will automatically be cancelled after the specified time is elapsed.

package main
import (
 "context"
 "fmt"
 "time"
)
func testTimeoutRoutine(ctx context.Context) {
 ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
 jobDone := make(chan bool)
 go func() {
 defer close(jobDone)
 defer cancel()
 for {
 select {
 case <-ctx.Done():
 fmt.Println("goroutine timed out before it's job is done")
 // Release lock on the resource
 return
 case <-jobDone:
 fmt.Println("goroutine job is done")
 // Release lock on the resource
 return
 }
 }
 }()
 // Acquire lock on a resource
 // Do some job
 time.Sleep(5 * time.Second)
 select {
 case <-ctx.Done():
 // Take no action since context was cancelled.
 default:
 jobDone <- true
 }
}
func main() {
 ctx := context.Background()
 go testTimeoutRoutine(ctx)
 // Let's wait for the goroutine to do it's job or timeout
 time.Sleep(10 * time.Second)
 fmt.Println("\n\nmain done")
}
answered Aug 23, 2024 at 3:16
\$\endgroup\$
3
  • \$\begingroup\$ What exactly is the goroutine doing in this scenario? It does not timeout the job, it only detects the timeout only after the job is done. \$\endgroup\$ Commented Aug 23, 2024 at 3:26
  • \$\begingroup\$ I tried to keep the goroutine's purpose as close to the original post's as possible: the goroutine is responsible for releasing the resource's lock when either (A) the request has timed out or (B) the job is completed. I did, however, change the timeout to be set outside the goroutine so that the goroutine's caller can be notified of completion (e.g. so the job knows to stop and not write to the closed channel). Ideally if we has the "run job" function available - it would have access to this context.WithDeadline. \$\endgroup\$ Commented Aug 23, 2024 at 3:32
  • 2
    \$\begingroup\$ Releasing a lock like this makes code race-prone, as the job would be running with mutex unlocked. Also, if the job completes without a timeout, the unlock happens an unknown time later. It is not a good idea to unlock a resource from a separate goroutine. \$\endgroup\$ Commented Aug 23, 2024 at 3:35

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.