『每周译Go』并发安全的集中式指针管理设施

在 Go 1.17 发行版中,我们贡献了一个新的 cgo 设施runtime/cgo.Handle,以帮助未来的 cgo 应用在 Go 和 C 之间传递指针的同时,更好、更容易地构建并发安全的应用。本文将通过询问该功能为我们提供了什么,为什么我们需要这样一个设施,以及我们最终究竟如何贡献具体实现来引导我们了解这个功能。

从 Cgo 和 X Window 剪贴板开始

Cgo 是在 Go 中与 C 语言设施进行交互的事实标准。然而,我们有多少次需要在 Go 中与 C 语言进行交互呢?这个问题的答案取决于我们在系统层面上的工作程度,或者我们有多少次需要利用传统的 C 库,比如用于图像处理。每当 Go 程序需要使用一个来自C的遗留物时,它需要导入一种 C的专用包,如下所示,然后在 Go 方面,人们可以通过导入的C符号简单地调用myprint函数。


/*
#include <stdio.h>

void myprint() {
    printf("Hello %s", "World");
}
*/
import "C"

func main() {
    C.myprint()
    // Output:
    // Hello World
}

几个月前,当我们在建立一个提供跨平台剪贴板访问的新的包golang.design/x/clipboard时, 我们发现,Go 中缺乏这样的设施,尽管有很多野路子,但仍然存在健全性和性能问题。

golang.design/x/clipboard软件包中,我们不得不与 cgo 合作来访问系统级的 API(从技术上讲,它是一个来自传统的、广泛使用的 C 系统的 API),但缺乏在 C 代码侧了解执行进度的设施。例如,在 Go 代码这边,我们必须在一个 goroutine 中调用 C 代码,然后并行地做其他事情。


go func() {
    C.doWork() // Cgo: 调用一个C函数,并在C代码侧做一些事情
}()

// .. 在Go代码侧做点事 ..

然而,在某些情况下,我们需要一种机制来了解 C 代码的执行进度,这就带来了 Go 和 C 之间通信和同步的需要。例如,如果我们需要我们的 Go 代码等待 C 代码完成一些初始化工作,直到某个执行点执行,我们将需要这种类型的通信来精确的了解 C 函数的执行进度。

我们遇到的一个真实的例子是需要与剪贴板设施互动。在 Linux 的X Window 环境中,剪贴板是分散的,只能由每个应用程序拥有。需要访问剪贴板信息的人需要创建他们的剪贴板实例。假设一个应用程序A想把某些东西粘贴到剪贴板上,它必须向 X Window 服务器提出请求,然后成为剪贴板的所有者,在其他应用程序发出复制请求时把信息送回去。

这种设计被认为是自然的,经常需要应用程序进行合作。如果另一个应用程序B试图提出请求,成为剪贴板的下一个所有者,那么A将失去其所有权。之后,来自应用程序CD等的复制请求将被转发给应用程序B而不是A。类似于一个共享的内存区域被别人覆盖了,而原来的所有者失去了访问权。

根据以上的上下文信息,我们可以理解,在一个应用程序开始 "粘贴"(服务)剪贴板信息之前,它首先要获得剪贴板的所有权。在我们获得所有权之前,剪贴板信息将不能用于访问目的。换句话说,如果一个剪贴板 API 被设计成如下方式:

clipboard.Write("某些信息")

我们必须从内部保证,当函数返回时,信息应该可以被其他应用程序访问。

当时,我们处理这个问题的第一个想法是,从 Go 到 C 传递一个channel,然后通过channel从 C 到 Go 发送一个值。经过快速的研究,我们意识到这是不可能的,因为由于Cgo 中传递指针的规则,channel不能作为一个值在 C 和 Go 之间传递(见之前的提案文件)。即使有办法将整个channel的值传递给 C,在 C 代码侧也没有设施可以通过该channel发送值,因为 C 没有<-操作符的语言支持。

下一个想法是传递一个函数回调,然后让它在 C 代码侧被调用。该函数的执行将使用所需的channel向等待的 goroutine 发送一个通知。

经过几次尝试,我们发现唯一可能的方法是附加一个全局函数指针,并通过一个函数包装器使其被调用:

/*
int myfunc(void* go_value);
*/
import "C"

// 这个funcCallback试图避免在运行时直接出现恐慌错误。
// 传递给Cgo,因为它违反了指针传递规则:
//
//   panic: runtime error: cgo argument has Go pointer to Go pointer
var (
    funcCallback   func()
    funcCallbackMu sync.Mutex
)

type gocallback struct{ f func() }

func main() {
    go func() {
        ret := C.myfunc(unsafe.Pointer(&gocallback{func() {
            funcCallbackMu.Lock()
            f := funcCallback // 必须使用一个全局函数变量。
            funcCallbackMu.Unlock()
            f()
        }}))
        // ... 搞事 ...
    }()
    // ... 搞事 ...
}

在上面的例子中,Go 一方的gocallback指针是通过 C 函数myfunc传递的。在 C 代码侧,将有一个使用go_func_callback的调用,通过传递结构gocallback作为参数,在 C 代码侧被调用:


// myfunc将在需要时触发一个回调,c_func,并通过void*参数传递
// gocallback的数据通过void*参数。
void c_func(void *data) {
    void *gocallbac
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值