我想将InsertRecords
函数运行30秒钟,并测试在给定时间内可以插入多少条记录。
如何在x秒后停止处理InsertRecords
,然后从处理程序返回结果?
func benchmarkHandler(w http.ResponseWriter, r *http.Request) {
counter := InsertRecords()
w.WriteHeader(200)
io.WriteString(w, fmt.Sprintf("counter is %d", counter))
}
func InsertRecords() int {
counter := 0
// db code goes here
return counter
}
答案 0 :(得分:2)
取消和超时通常是通过context.Context
完成的。
虽然这个简单的示例可以单独使用一个通道来完成,但是使用此处的上下文使其更加灵活,并且还可以考虑客户端断开连接的情况。
func benchmarkHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
counter := InsertRecords(ctx)
w.WriteHeader(200)
io.WriteString(w, fmt.Sprintf("counter is %d", counter))
}
func InsertRecords(ctx context.Context) int {
counter := 0
done := ctx.Done()
for {
select {
case <-done:
return counter
default:
}
// db code goes here
counter++
}
return counter
}
这将至少运行30秒,并返回完整的数据库迭代次数。如果您想确保处理程序总是在30秒后立即返回,即使阻止了DB调用,那么您也需要将DB代码推入另一个goroutine中,然后让它稍后返回。最短的示例将使用与上述类似的模式,但是同步对计数器变量的访问,因为它可以由DB循环在返回时写入。
func InsertRecords(ctx context.Context) int {
counter := int64(0)
done := ctx.Done()
go func() {
for {
select {
case <-done:
return
default:
}
// db code goes here
atomic.AddInt64(&counter, 1)
}
}()
<-done
return int(atomic.LoadInt64(&counter))
}
有关生产者和超时的示例,请参见@JoshuaKolden的答案,也可以将其与现有请求上下文结合使用。
答案 1 :(得分:2)
JimB指出取消限制HTTP请求所花费的时间可以使用context.WithTimeout进行处理,但是由于您出于基准测试的目的,您可能希望使用更直接的方法。
context.Context的目的是允许发生许多取消事件,并具有正常停止所有下游任务的相同净效果。在JimB的示例中,可能有一些其他过程将在30秒过去之前取消上下文,这从资源利用率的角度来看是理想的。例如,如果连接过早终止,则在构建响应方面没有任何其他工作要做。
如果基准测试是您的目标,那么您希望最大程度地减少多余事件对基准代码的影响。这是如何执行此操作的示例:
com.sun.xml.internal.bind.characterEscapeHandler
本质上,我们正在做的是在离散的db操作上创建无限循环,并计算循环的迭代次数,我们在触发func InsertRecords() int {
stop := make(chan struct{})
defer close(stop)
countChan := make(chan int)
go func() {
defer close(countChan)
for {
// db code goes here
select {
case countChan <- 1:
case <-stop:
return
}
}
}()
var counter int
timeoutCh := time.After(30 * time.Second)
for {
select {
case n := <-countChan:
counter += n
case <-timeoutCh:
return counter
}
}
}
时停止。
JimB示例中的一个问题是,尽管在循环中检查了ctx.Done(),但是如果“ db code”被阻止,循环仍然会被阻止。这是因为ctx.Done()仅与“ db code”块内联评估。
为避免此问题,我们将计时功能和基准测试循环分开,以便没有什么可以阻止我们在发生超时事件时接收它。一旦超时甚至发生,我们将立即返回计数器的结果。 “ db代码”可能仍处于执行中,但是InsertRecords仍将退出并返回其结果。
如果在InsertRecords退出时“ db代码”处于执行中,那么goroutine将保持运行状态,因此我们需要time.After
进行清理,以便在函数退出时一定要向goroutine发出信号在下一次迭代中退出。当goroutine退出时,它将清理用于发送计数的通道。
作为一般模式,上面是一个示例,说明了如何在Go中获得精确的计时,而无需考虑代码的实际执行时间。
旁注:更高级的观察结果是,我的示例未尝试在计时器和goroutine之间同步开始时间。在这里解决该问题似乎有些古怪。但是,您可以通过创建一个阻塞主线程的通道来轻松同步两个线程,直到goroutine在开始循环之前将其关闭为止。