如何确保上下文监听器(context listener)在主程序退出之前完成?

huangapple go评论79阅读模式
英文:

How to ensure context listener routine finishes before main exits?

问题

以下是我翻译好的内容:

以下是我遇到的一个简化示例场景。Go Playground 在这里

主方法定期调用process函数。process函数在返回之前或应用程序由于中断而关闭之前必须释放的锁。

中断通过取消传递给process的上下文来处理。在我的示例中,我只是取消了上下文。

如何确保在取消上下文时解锁逻辑能够完整执行?

目前,在我的测试中,逻辑没有完整执行。该例程被启动,但似乎在程序退出之前没有完成。

就好像主方法正在退出,中途终止了该例程。这可能吗?如何确保该例程在程序退出之前始终完成?谢谢!

package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	
	var wg sync.WaitGroup
	wg.Add(1)
	go func(){
	    fmt.Println("Started periodic process")
		defer wg.Done()
		ticker := time.NewTicker(20 * time.Millisecond)
		for {
			select {
				case <-ticker.C:
					process(ctx)
				case <-ctx.Done():
					ticker.Stop()					
	
			}
		}
		fmt.Println("Finished periodic process")
	}()
	
	time.Sleep(100 * time.Millisecond)	
	
	// 取消上下文
	cancel()
	
	wg.Wait()
}

func process(ctx context.Context) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	
	go func() {
		<-ctx.Done()
		
		// 解锁资源
		fmt.Println("Unlocking resources")

		time.Sleep(20 * time.Millisecond)

		fmt.Println("Unlocked resources")		
	}()
	
	// 做一些工作
	fmt.Println("Started process")
	
	// 获取锁,在实际过程中
	// 获取锁将失败,除非
	// 它被前一个进程释放
	fmt.Println("Acquired lock")
	
	time.Sleep(10 * time.Millisecond)
	
	fmt.Println("Finished process")

}
英文:

Following is a simplified example of a scenario I am running into. Go Playground here.

The main method is periodically calling the process function. The process function acquires a lock that it must release before it returns or before the application is shut down due to an interrupt.

The interrupt is handled by cancelling the context passed to process. In my example, I am simply cancelling the context.

How can I ensure that when context is cancelled the unlocking logic is executed to completion?

ATM, in my tests, the logic is not getting executed to completion. The routine is kicked off, but does not seem to complete before the program exits.

It's as if the main method is exiting, killing the routine half way. Is that possible? How can I ensure that the routine always completes before the program exits? Thanks!

package main
import (
&quot;fmt&quot;
&quot;context&quot;
&quot;time&quot;
&quot;sync&quot;
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func(){
fmt.Println(&quot;Started periodic process&quot;)
defer wg.Done()
ticker := time.NewTicker(20 * time.Millisecond)
for {
select {
case &lt;-ticker.C:
process(ctx)
case &lt;-ctx.Done():
ticker.Stop()					
}
}
fmt.Println(&quot;Finished periodic process&quot;)
}()
time.Sleep(100 * time.Millisecond)	
// cancel context
cancel()
wg.Wait()
}
func process(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
&lt;-ctx.Done()
// unlock resources
fmt.Println(&quot;Unlocking resources&quot;)
time.Sleep(20 * time.Millisecond)
fmt.Println(&quot;Unlocked resources&quot;)		
}()
// do some work
fmt.Println(&quot;Started process&quot;)
// acquire lock, in actual process
// acquiring lock will fail unless
// it was released be previous process
// run
fmt.Println(&quot;Acquired lock&quot;)
time.Sleep(10 * time.Millisecond)
fmt.Println(&quot;Finished process&quot;)
}

答案1

得分: 1

根据你提供的代码,以下是翻译的内容:

目前你的程序将永远不会退出。这是因为在main函数中启动的goroutine中的for循环永远不会退出(你在ctx.Done()时停止了ticker,但没有退出循环)。

第二个问题是,在process函数中,当函数退出时(通过defer cancel()),goroutine会被取消,但由于延迟的存在,goroutine将继续运行一段时间。解决方案取决于你是否需要异步进行“解锁”操作(我假设这不重要)。

以下是解决这两个问题并确保在资源释放之前process函数不返回的代码(playground链接);如果你需要并行释放这些资源,可以将WaitGroup传递给函数。

package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	
	var wg sync.WaitGroup
	wg.Add(1)
	go func(){
	        fmt.Println("Started periodic process")
		defer wg.Done()
		ticker := time.NewTicker(20 * time.Millisecond)
		for {
			select {
				case <-ticker.C:
					process(ctx)
				case <-ctx.Done():
					ticker.Stop()					
					fmt.Println("Finished periodic process")
					return
			}
		}		
	}()	
	time.Sleep(100 * time.Millisecond)	
	cancel()	
	wg.Wait()
}

func process(ctx context.Context) {
	ctx, cancel := context.WithCancel(ctx)
	done := make(chan struct{}) // could also use a waitgroup
	
	go func() {
		<-ctx.Done()
		fmt.Println("Unlocking resources")
		time.Sleep(10 * time.Millisecond)
		fmt.Println("Unlocked resources")	
	    close(done)	
	}()
	

	// do some work
	fmt.Println("Started process") 	
	time.Sleep(10 * time.Millisecond)	
	fmt.Println("Finished process")
	
	cancel()
	<-done // Wait for resources to be unlocked
}
英文:

As it stands your program will never exit. This is because the for loop in the goroutine started in main never exits (you stop the ticker when ctx.Done() but do not exit the loop).

A second issue is that in process the goroutine is cancelled when the function exits (via defer cancel()) but, due to the delay, the goroutine will continue to run for a period. The solution here depends upon whether you need the 'unlocking' to occur asynchronously (I have assumed this is not important).

The following (playground) resolves both issues and ensures that process does not return before the resources are free; if you need these to be freed in parallel then one option is to pass the WaitGroup to the function)

package main
import (
&quot;fmt&quot;
&quot;context&quot;
&quot;time&quot;
&quot;sync&quot;
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func(){
fmt.Println(&quot;Started periodic process&quot;)
defer wg.Done()
ticker := time.NewTicker(20 * time.Millisecond)
for {
select {
case &lt;-ticker.C:
process(ctx)
case &lt;-ctx.Done():
ticker.Stop()					
fmt.Println(&quot;Finished periodic process&quot;)
return
}
}		
}()	
time.Sleep(100 * time.Millisecond)	
cancel()	
wg.Wait()
}
func process(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
done := make(chan struct{}) // could also use a waitgroup
go func() {
&lt;-ctx.Done()
fmt.Println(&quot;Unlocking resources&quot;)
time.Sleep(10 * time.Millisecond)
fmt.Println(&quot;Unlocked resources&quot;)	
close(done)	
}()
// do some work
fmt.Println(&quot;Started process&quot;) 	
time.Sleep(10 * time.Millisecond)	
fmt.Println(&quot;Finished process&quot;)
cancel()
&lt;-done // Wait for resources to be unlocked
}

huangapple
  • 本文由 发表于 2021年9月13日 17:55:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/69160695.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定