英文:
Timing of requests in goroutines
问题
我正在尝试计算并发请求所需的时间。我的计时结果比ab2报告的结果慢大约四倍。
我尝试了两种不同的计时请求方法,两种方法得到的结果都相似(与ab2的结果相差很大)。举个例子,ab2会报告本地服务器上最长的请求持续时间为2毫秒,而这段代码会报告长达4.5毫秒的时间。顺便说一下,整个代码库可以在这里找到。
我该如何正确计时Go中的请求?
方法1:计时包括更多内容
来自这个提交。
// 让我们生成所有请求,以及它们各自的并发数。
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
go func(no int, greq goreq.Request) {
r.ongoingReqs <- struct{}{} // 添加哨兵值以限制并发。
startTime := time.Now()
greq.Uri = r.URL.Generate()
gresp, err := greq.Do()
if err != nil {
log.Critical("无法发送请求到 #%d %s: %s", no, r.URL, err)
} else if no < r.Repeat {
// 我们总是使用最后一个响应来处理下一批请求。
gresp.Body.Close()
}
<-r.ongoingReqs // 我们完成了,为下一个请求腾出空间。
resp := Response{Response: gresp, duration: time.Now().Sub(startTime)}
// 让我们将该请求添加到已完成请求的列表中。
r.doneChan <- &resp
runtime.Gosched()
}(rno, greq)
}
方法2:使用带有defer语句的内部函数
来自这个提交。
// 让我们生成所有请求,以及它们各自的并发数。
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
go func(no int, greq goreq.Request) {
r.ongoingReqs <- struct{}{} // 添加哨兵值以限制并发。
greq.Uri = r.URL.Generate()
var duration time.Duration
gresp, err := func(dur *time.Duration) (gresp *goreq.Response, err error) {
defer func(startTime time.Time) { *dur = time.Now().Sub(startTime) }(time.Now())
return greq.Do()
}(&duration)
if err != nil {
log.Critical("无法发送请求到 #%d %s: %s", no, r.URL, err)
} else if no < r.Repeat {
// 我们总是使用最后一个响应来处理下一批请求。
gresp.Body.Close()
}
<-r.ongoingReqs // 我们完成了,为下一个请求腾出空间。
resp := Response{Response: gresp, duration: duration}
// 让我们将该请求添加到已完成请求的列表中。
r.doneChan <- &resp
runtime.Gosched()
}(rno, greq)
}
我查看了这个问题,但没有帮助。
英文:
I am trying to time how long it takes to make concurrent requests. My timing results are about four times slower than what ab2 reports.
I've tried timing requests in two different ways, both of which lead to similar results (which are far off ab2's results). To give an example, ab2 will report the longest request to last 2 milliseconds on a local server, whereas this code will report up to 4.5 milliseconds. By the way, the whole codebase is available here.
How do I properly time a request in go?
Method 1: timing includes more than just the request
From this commit.
// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
go func(no int, greq goreq.Request) {
r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
startTime := time.Now()
greq.Uri = r.URL.Generate()
gresp, err := greq.Do()
if err != nil {
log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
} else if no < r.Repeat {
// We're always using the last response for the next batch of requests.
gresp.Body.Close()
}
<-r.ongoingReqs // We're done, let's make room for the next request.
resp := Response{Response: gresp, duration: time.Now().Sub(startTime)}
// Let's add that request to the list of completed requests.
r.doneChan <- &resp
runtime.Gosched()
}(rno, greq)
}
Method 2: using an internal function with a defer statement
From this commit.
// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
go func(no int, greq goreq.Request) {
r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
greq.Uri = r.URL.Generate()
var duration time.Duration
gresp, err := func(dur *time.Duration) (gresp *goreq.Response, err error) {
defer func(startTime time.Time) { *dur = time.Now().Sub(startTime) }(time.Now())
return greq.Do()
}(&duration)
if err != nil {
log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
} else if no < r.Repeat {
// We're always using the last response for the next batch of requests.
gresp.Body.Close()
}
<-r.ongoingReqs // We're done, let's make room for the next request.
resp := Response{Response: gresp, duration: duration}
// Let's add that request to the list of completed requests.
r.doneChan <- &resp
runtime.Gosched()
}(rno, greq)
}
I had a look at this question, which did not help.
答案1
得分: 1
当你的goroutine进入系统调用(写入socket)时,它们会被抢占。这意味着它们会被中断,另一个goroutine会运行在它们的位置上。最终,被抢占的goroutine会再次被调度,并在离开的地方继续运行。不过,并不一定会在系统调用完成后立即发生。
在goroutine中进行计时是困难的,因为即使你按顺序执行所有操作,Go 1.5的垃圾回收器也会偶尔运行,中断你理论上的顺序循环。
唯一真正的解决方案有点复杂:
- 直接使用RawSyscall。
- 在函数上注释
//go:nosplit
,以防止它被抢占。 - 禁用垃圾回收器。
即使这样,我可能还是忘记了一些东西。
英文:
As your goroutines enter syscall (writing to a socket), they are preempted. This means that they are interrupted and another goroutine will run in their place. Eventually, your preempted goroutine will be scheduled again and it will continue running where it left off. This doesn't necessarily happen exactly after the syscall is done, though.
Timing in a goroutine is difficult, because even if you did everything serially, Go 1.5's garbage collector will run occasionally, interrupting your theoretical serial loop.
The only real solution is a bit complex:
- Use RawSyscall directly.
- Annotate your function with
//go:nosplit
to prevent it from being preempted. - Disable garbage collector.
Even then, I might be forgetting something.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论