【问题标题】:cgo Interacting with C Library that uses Thread Local Storagecgo 与使用线程本地存储的 C 库交互
【发布时间】:2020-08-09 09:26:51
【问题描述】:

我正在用 cgo 包装一个 C 库,以供普通 Go 代码使用。

我的问题是我想将错误字符串传播到 Go API,但有问题的 C 库通过线程本地存储提供错误字符串;有一个全局的get_error() 调用,它返回一个指向线程本地字符数据的指针。

我最初的计划是通过 cgo 调用 C,检查调用是否返回错误,如果是,则使用 C.GoString 包装错误字符串,将其从原始字符指针转换为 Go 字符串。它看起来像C.GoString(C.get_error())

我在这里预见到的问题是 C 中的 TLS 在本机操作系统线程级别上工作,但据我了解,调用 Go 代码将来自潜在的 N 个 goroutine 之一,这些 goroutine 在一定数量的底层本地多路复用Go 调度程序管理的线程池中的线程。

我害怕遇到这样一种情况:我调用了 C 例程,然后在 C 例程返回之后,但在我复制错误字符串之前,Go 调度程序决定将当前的 goroutine 换成另一个.当原始的 goroutine 被换回时,据我所知,它可能位于不同的本地线程上,但即使它被换回同一个线程,在此期间运行的任何 goroutine 都可能改变了状态的 TLS,导致我为不相关的调用加载错误字符串。

我的问题是:

  • 这是一个合理的担忧吗?我是否误解了 go 调度程序或它与 cgo 交互的方式,这会导致这不是问题?
  • 如果这是一个合理的问题,我该如何解决?
    • cgo 以某种方式设法将 errno 值传播回调用 Go 代码,这些代码也存储在 TLS 中,这让我认为必须有一种安全的方法来执行此操作。
    • 我想不出 C 代码本身可以被 go 调度程序抢占的方法,所以我应该引入一个包装 C 函数并让 IT 进行必要的调用,然后在返回备份之前有条件地复制错误字符串去戈兰?

我对任何允许我将错误字符串传播到 Go 其余部分的解决方案感兴趣,但我希望避免任何需要我围绕 TLS 序列化访问的解决方案,例如添加锁仅仅抓住一个错误字符串对我来说似乎非常不幸。

提前致谢!

【问题讨论】:

    标签: c multithreading go cgo thread-local-storage


    【解决方案1】:

    我害怕遇到这样一种情况:我调用了 C 例程,然后在 C 例程返回之后,但在我复制错误字符串之前,Go 调度程序决定将当前的 goroutine 换成另一个. ...

    这是一个合理的担忧吗?

    是的。 cgo“调用 C 代码”包装器在每次调用期间锁定一个 POSIX / OS 线程,但它们锁定的线程并非一直固定;实际上bop around 确实会随着时间的推移对多个不同的线程进行处理,只要您的 goroutine 正常运行。 (由于 Go 在当前实现中是协同调度的,在某些情况下,您可以小心不要做任何可能让您切换底层操作系统线程的事情,但这可能不是一个好计划。)

    可以在这里使用runtime.LockOSThread,但我认为最好的方案是:

    我该如何解决?

    抓住错误之前Go 恢复其正常的调度算法(即,在从 C/POSIX 线程解锁 goroutine 之前)。

    cgo 设法传播 errno 值 ...

    从 POSIX 线程解锁 goroutine 之前获取 errno 值。

    我最初的计划是通过 cgo 调用 C,检查调用是否返回错误,如果是,则使用 C.GoString 包装错误字符串,将其从原始字符指针转换为 Go 字符串。它看起来像C.GoString(C.get_error())

    如果有这样的变体导致错误 number(而不是将其从 TLS 变量中找出来),那么该计划应该仍然有效:只需确保您的 C 例程同时提供返回值和错误号。

    如果没有,请按照您的建议编写自己的 C 包装器:

    ftype wrapper_for_realfunc(char **errp, arg1type arg1, arg2type arg2) {
        ftype ret = realfunc(arg1, arg2);
        if IS_ERROR(ret) {
            *errp = get_error();
        } else {
            *errp = NULL;
        }
        return ret;
    }
    

    现在你的 Go 包装器只是调用包装器,它用一个额外的 *C.char 参数填充指向 C 内存的指针,如果没有错误,将其设置为 nil,并将其设置为你可以使用的东西 @987654329 @如果有错误。

    如果由于某种原因这不可行,请考虑使用runtime.LockOSThread 及其对应的runtime.UnlockOSThread

    【讨论】:

    • 非常感谢您的详细回复!不幸的是,TLS 调用是库当前使错误字符串可用的唯一方法,并且我需要以这种方式包装大量函数......
    • 我通常会犹豫选择 LockOSThread 方向,因为我认为它对 Go 调度程序的整体性能有重大影响,但如果我没看错的话,cgo 已经 无论我做什么,都会在每次 C 调用时发出对 LockOSThread 的调用。如果这种理解是正确的,那么我是否正确地假设在进行 C 调用之前调用它的额外开销不太可能很大?
    • 我还没有具体看到 cgo 的实现,但我敢打赌它确实直接使用了runtime.LockOSThread。看起来开销应该相对较低。 (顺便说一句,您可能会考虑自动生成包装器——Go 已经做了很多“在编译时生成一些代码”,作为泛型的一种替代。)
    猜你喜欢
    • 2011-12-12
    • 2020-02-02
    • 1970-01-01
    • 2011-09-16
    • 1970-01-01
    • 2016-04-23
    • 2021-05-29
    • 1970-01-01
    • 2012-05-08
    相关资源
    最近更新 更多