【问题标题】:How do I timeout a blocking external library call?如何使阻塞的外部库调用超时?
【发布时间】:2018-07-14 06:13:21
【问题描述】:

(我不认为我的问题与此 QA 重复:go routine blocking the others one,因为我正在运行具有抢占式调度程序的 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 &lt;-time.After(time.Second * 3) 块。

I know that attempting to read from an unbuffered channel with nothing in it 会阻塞,但我创建了缓冲大小为1 的通道,据我所知,我做得正确。我很惊讶 Go 调度程序没有抢占我的 goroutine,或者这是否取决于执行是在 go-lang 代码中而不是外部本机库中?

更新:

我了解到,至少在 2015 年,Go 调度程序实际上是“半抢占式”的,它不会抢占“外部代码”中的操作系统线程: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 秒。

【问题讨论】:

  • “不重复”代码仍然阻塞在 Playground 中,即 Go 1.9。我怀疑这毕竟是同一个原因。
  • 你在设置 GOMAXPROCS 吗?
  • @superfell 项目中的其他代码设置GOMAXPROCS(runtime.NumCPU()),即我机器上的8。所以它应该仍然有效......
  • MyCodeWithTimeout()中进行线程锁定,而不是在它启动的goroutine中。

标签: go concurrency timeout goroutine


【解决方案1】:

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

而是在MyCodeWithTimeout() 中进行线程锁定,而不是在它启动的goroutine 中:

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锁定到OS线程,你可以确定这个goroutine会注意到定时器值上发送的值。

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

如果你想要保证,你不能依赖其他 goroutine 给出“退出”信号,你只能依赖运行 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,并且您的 goroutine 仍然“饿死”,这可能是一个临时解决方案,但是您在使用 Go 的并发原语时仍然会遇到更严重的问题应用程序(或缺乏使用它们),您应该调查并“修复”这些问题。将线程锁定到一个 goroutine 甚至可能会使其余的 goroutine 变得更糟。

【讨论】:

    猜你喜欢
    • 2013-11-16
    • 2013-12-07
    • 2010-12-26
    • 2014-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-12
    相关资源
    最近更新 更多