如果通过上下文语义在同一个执行线程中花费大量时间计算,我就不能弄清楚如何取消一个任务?
我使用这个例子作为一个参考点test.go。
这里的目标是调用一个doWork,如果doWork花费了很长时间来计算,那么GetValueWithDeadline应该在超时后返回0,或者如果调用方调用cancel取消等待(这里主要是调用者)或返回在给定时间窗口中返回的值。
同样的场景可以用不同的方式来完成。(单独的goroutine睡眠,唤醒检查值等,互斥对象上的条件等等)但我真的想了解使用上下文的正确方法。
我理解的通道语义,但在这里我无法达到预期的效果,默认情况调用默认情况下的doWork错误和睡眠。
package main
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
)
type Server struct {
lock sync.Mutex
}
func NewServer() *Server {
s := new(Server)
return s
}
func (s *Server) doWork() int {
s.lock.Lock()
defer s.lock.Unlock()
r := rand.Intn(100)
log.Printf("Going to nap for %d", r)
time.Sleep(time.Duration(r) * time.Millisecond)
return r
}
// I take an example from here and it very unclear where is do work executed
// https://golang.org/src/context/context_test.go
func (s *Server) GetValueWithDeadline(ctx context.Context) int {
val := 0
select {
case <- time.After(150 * time.Millisecond):
fmt.Println("overslept")
return 0
case <- ctx.Done():
fmt.Println(ctx.Err())
return 0
default:
val = s.doWork()
}
return all
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
s := NewServer()
for i :=0; i < 10; i++ {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
log.Print(s.GetValueWithDeadline(ctx))
cancel()
}
}
谢谢
发布于 2020-06-06 05:34:37
您的方法存在多个问题。
问题上下文解决了什么?
首先,在Go中发明上下文的主要原因是它们允许统一一种取消一组任务的方法。
要使用一个简单的例子来解释这个概念,请考虑向某个服务器发送一个客户端请求;为了进一步简化,让它成为一个HTTP请求。客户端连接到服务器,发送一些数据告诉服务器如何完成请求,然后等待服务器响应。
现在让我们假设请求需要在服务器上进行精细且耗时的处理--例如,假设它需要对多个远程数据库引擎执行多个复杂的查询,对外部服务执行多个HTTP请求,然后处理所获得的结果以实际生成客户端想要的数据。
因此,客户端启动其请求,服务器继续处理所有这些请求。
为了隐藏服务器为完成请求而必须执行的单个任务的延迟,它在单独的goroutines中运行这些任务。一旦每个goroutine完成指定的任务,它就会将其结果(和/或错误)传递回处理客户端请求的goroutine,等等。
现在假设客户机由于任何原因不能等待对其请求的响应--网络中断、客户端软件中的显式超时、用户杀死发起请求的应用程序等等--有很多可能性。
正如您所看到的,服务器继续花费资源来完成逻辑上绑定到“现在死亡”请求的任务是没有什么意义的:无论如何,没有人能听到结果。
因此,一旦我们知道请求不会完成,就应该收获这些任务,而这正是上下文发挥作用的地方:您可以将每个传入的请求与单个上下文关联起来,然后将其传递给为完成请求而产生的任何goroutine,或者从请求中派生出另一个请求,并将其传递给它。然后,一旦您取消了“根”请求,该信号就会传播到从根请求派生的整个请求树中。
现在,每个被赋予上下文的goroutine,可能会在发送取消信号时“监听”通知它,并且一旦goroutine注意到它可能会放下它正在忙着做的任何事情并退出。
就实际的context.Context
类型而言,这个信号被称为“完成”--就像“我们完成了与上下文相关的任何事情”--这就是为什么希望知道它应该停止工作的goroutine在上下文的方法Done
返回的一个特殊通道上监听。
回到你的例子
为了让它发挥作用,你应该做一些如下的事情:
func (s *Server) doWork(ctx context.Context) int {
s.lock.Lock()
defer s.lock.Unlock()
r := rand.Intn(100)
log.Printf("Going to nap for %d", r)
select {
case <- time.After(time.Duration(r) * time.Millisecond):
return r
case <- ctx.Done():
return -1
}
}
func (s *Server) GetValueWithTimeout(ctx context.Context, maxTime time.Duration) int {
d := time.Now().Add(maxTime)
ctx, cancel := context.WithDeadline(ctx, d)
defer cancel()
return s.doWork(ctx)
}
func main() {
const maxTime = 50 * time.Millisecond
rand.Seed(time.Now().UTC().UnixNano())
s := NewServer()
for i :=0; i < 10; i++ {
v := s.GetValueWithTimeout(context.Background(), maxTime)
log.Print(v)
}
}
(游乐场)。
那么这里发生了什么?
GetValueWithTimeout
方法接受它应该使用的doWork
方法产生值的最大时间,计算截止日期,导出一个上下文,该上下文在截止日期从传递给该方法的上下文传递后取消,并使用新的context对象调用doWork
。
doWork
方法启动自己的计时器,使其在随机时间间隔后消失,然后侦听上下文和计时器。
这是一个关键点:执行某些工作单元的代码应该是可取消的,它必须检查上下文才能自动“完成”。
因此,在我们的玩具示例中,要么是doWork
自己的计时器首先触发,要么是首先到达生成上下文的截止日期;无论首先发生什么,都会使select
语句解除阻塞并继续进行。
请注意,如果您的“做工作”代码更复杂--它实际上会做一些事情而不是睡觉--您很可能需要定期检查上下文的状态,通常是在执行该工作的不生动的部分之后。
https://stackoverflow.com/questions/62231235
复制相似问题