如何超时阻止外部库调用?

时间:2018-02-04 04:43:13

标签: go concurrency timeout goroutine

(我不相信我的问题与此质量检查重复: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例程,通道和超时,基于此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)块。

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

更新

我读到Go-scheduler,至少在2015年,实际上是半先发制人&#34;并且它不会抢占外部代码中的操作系统线程&#34;: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秒。

1 个答案:

答案 0 :(得分:2)

你提出的在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的调度没有影响。

如果你想要保证,你不能依赖其他goroutines给出&#34;退出&#34;信号,您只能依赖于运行MyCodeWithTimeout()函数的goroutine中发生的这种情况(因为自从您进行了线程锁定,您可以确定它已被安排)。

丑陋的&#34;提高给定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
    }
}

请注意&#34;敦促&#34;在此循环中使用time.Sleep()将取消保证,因为time.Sleep()可能在其实现中使用goroutine,并且当然不保证在给定的持续时间之后完全返回。

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