如何使阻塞外部库调用超时?



(我不相信我的问题与此 QA 重复:go 例程阻止其他问题,因为我正在运行具有抢占式调度程序的 Go 1.9,而该问题是在 Go 1.2 中提出的)。

我的 Go 程序调用由另一个 Go-lang 库包装的 C 库,该库发出可能持续超过 60 秒的阻塞调用。我想添加一个超时,以便它在 3 秒内返回:

带有长块的旧代码:

// InvokeSomething is part of a Go wrapper library that calls the C library read_something function. I cannot change this code.
func InvokeSomething() ([]Something, error)  {
ret := clib.read_something(&input) // this can block for 60 seconds
if ret.Code > 1 {
return nil, CreateError(ret)
}
return ret.Something, nil
}
// This is my code I can change:
func MyCode() {
something, err := InvokeSomething()
// etc
}

我的代码带有 go-routine、通道和超时,基于这个 Go 示例:https://gobyexample.com/timeouts

type somethingResult struct {
Something []Something
Err        error
}
func MyCodeWithTimeout() {
ch = make(chan somethingResult, 1);
go func() {
something, err := InvokeSomething() // blocks here for 60 seconds
ret := somethingResult{ something, err }
ch <- ret
}()
select {
case result := <-ch:
// etc
case <-time.After(time.Second *3):
// report timeout
}
}

但是,当我运行MyCodeWithTimeout时,它仍然需要 60 秒才能执行case <-time.After(time.Second * 3)块。

我知道尝试从没有任何内容的未缓冲通道读取会阻塞,但我创建了缓冲大小为1的通道,以便据我所知我做得正确。我很惊讶 Go 调度程序没有抢占我的 goroutine,还是这取决于在 go-lang 代码中而不是外部本机库中执行?

更新:

我读到Go-scheduler,至少在2015年,实际上是"半抢占式"的,它不会抢占"外部代码"中的操作系统线程:https://github.com/golang/go/issues/11462

你可以认为 Go 调度程序是部分抢占式的。它绝不是完全合作的,因为用户代码通常无法控制调度点,但它也无法在任意点抢占。

我听说runtime.LockOSThread()可能会有所帮助,所以我将函数更改为:

func MyCodeWithTimeout() {
ch = make(chan somethingResult, 1);
defer close(ch)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
something, err := InvokeSomething() // blocks here for 60 seconds
ret := somethingResult{ something, err }
ch <- ret
}()
select {
case result := <-ch:
// etc
case <-time.After(time.Second *3):
// report timeout
}
}

。然而,它根本没有帮助,它仍然阻止了 60 秒。

您提出的在MyCodeWithTimeout()年开始的 goroutine 中进行线程锁定的解决方案并不能保证MyCodeWithTimeout()会在 3 秒后返回,原因是:首先:不能保证启动的 goroutine 会被调度并达到将线程锁定到 goroutine 的点,其次:因为即使外部命令或系统调用被调用并在 3 秒内返回, 不能保证其他 GoRoutine 正在运行的MyCodeWithTimeout()将被安排接收结果。

而是在MyCodeWithTimeout()中执行线程锁定,而不是在它启动的 go例程中:

func MyCodeWithTimeout() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ch = make(chan somethingResult, 1);
defer close(ch)
go func() {
something, err := InvokeSomething() // blocks here for 60 seconds
ret := somethingResult{ something, err }
ch <- ret
}()
select {
case result := <-ch:
// etc
case <-time.After(time.Second *3):
// report timeout
}
}

现在,如果MyCodeWithTimeout()执行开始,它将 goroutine 锁定到操作系统线程,您可以确定此 goroutine 会注意到计时器值上发送的值。

注意:如果您希望它在 3 秒内返回,这会更好,但此门槛不会提供保证,因为触发(在其通道上发送值)的计时器在其自己的 goroutine 中运行,并且此线程锁定对该 goroutine 的调度没有影响。

如果你想要保证,你不能依赖其他goroutine给出"exit"信号,你只能依靠在运行MyCodeWithTimeout()函数的goroutine中发生这种情况(因为因为你做了线程锁定,你可以确保它被调度)。

一个"丑陋"的解决方案会为给定的CPU内核启动CPU使用率:

for end := time.Now().Add(time.Second * 3); time.Now().Before(end); {
// Do non-blocking check:
select {
case result := <-ch:
// Process result
default: // Must have default to be non-blocking
}
}

请注意,在此循环中使用time.Sleep()的"冲动"会取消保证,因为time.Sleep()可能会在其实现中使用 goroutines,并且当然不能保证在给定持续时间之后准确返回。

另请注意,如果您有 8 个 CPU 内核,并且runtime.GOMAXPROCS(0)为您返回 8 个,并且您的 goroutines 仍然"饥饿",这可能是一个临时解决方案,但您在应用程序中使用 Go 的并发原语仍然遇到更严重的问题(或缺乏使用它们),您应该调查并"修复"这些原语。将线程锁定到 goroutines 甚至可能会使其余的 goroutines 变得更糟。

相关内容

  • 没有找到相关文章

最新更新