【问题标题】:c - designing thread-safe library [closed]c - 设计线程安全库
【发布时间】:2021-09-28 06:02:45
【问题描述】:

我正在开发新库,以便将我现有的大量应用程序移植到通用代码中,并与他人分享我的经验 (FOSS)。

库实现本身根本不使用任何全局/静态变量。相反,它提供了不透明的数据类型和相应的访问器函数来对该数据执行不同的任务。

未来将使用该库的应用程序可能(并且将会)是多线程的(例如具有实时处理功能的 GUI 应用程序),因此出现了关于线程安全的问题。至于我有两种实现方式:

  1. 升级当前库代码并实现一些线程感知的东西,例如,通过使用 PThreads API。对我来说,它看起来像这样:为每个可能可变的对象添加一些互斥体并在访问器例程中尊重它们 - 设置器、获取器、重新初始化器等。
  2. 以不会发生线程特定问题(例如死锁、并发等)的方式编写客户端程序 - 通过在最终用户应用程序中使用相同的 PThreads,而不是在库中。

乍一看,第一种方法更加通用和灵活 - 我的库的最终用户不应该担心使用多线程破坏事物:库的内部将在互斥锁和其他同步的帮助下负责访问关键部分机制。可能是我缺乏一些经验,但我不记得使用这种方法的任何项目。可能是我没有深入挖掘...

第二种方法看起来更简单(对于我作为库开发人员而言),因为我将所有线程同步任务卸载给最终用户,而无需自己处理 PThreads(或类似的东西)。

我的问题是:纯粹在库本身(方法 1)中处理线程安全问题是否被认为是好的做法?将所有线程同步任务卸载给最终用户会导致在使用库的同时减轻其开发负担吗?

编辑:符合 SO 风格

【问题讨论】:

  • 基于意见和背景,VTC
  • 第一种方法强制用户包含 pthread,即使他们不需要它。
  • 这是一个非常广泛的话题,虽然您的问题在总体方案中是有效的,但在 SO 中却是题外话。以下是一些自以为是的想法: 最简单的方法是确保一切都是可重入的;这意味着您要么在堆栈框架上维护单独的结构,要么对任何堆写锁使用原子防护。您不需要或担心是否有人在使用 pthreads 或其他东西。 (除非你的库自己做线程)。
  • 作为一个狂热的 C 程序员,我鼓励你看看 Rust ;) 这是 HYPER 固执己见,但它有效。

标签: c multithreading thread-safety


【解决方案1】:

最广泛可移植的方法是要求客户端代码提供定义或回调,以便在底层平台上以原子方式执行一些操作,无论需要什么内存屏障,可能包括使用 C11 实现这些方法的库原子操作。原子操作比锁要好,因为如果应用程序不能立即完成一个动作,它不需要担心它应该做什么;允许客户端代码提供函数可能比使用 C11 操作更好,因为许多平台将能够直接在某些类型上处理有限范围的原子操作,而无法处理 C11 标准规定的所有操作。

【讨论】:

  • 让我们考虑一个例子:我有一个 GUI 应用程序,UI 的东西在线程 (1) 中处理,数据采集/处理在线程 (2) 中。在数据处理期间,数据对象的一些状态变量(例如,信号强度)将被更新。我希望我的 GUI 线程 (1) 从该数据中提取新的信号强度值。为此,它应该调用一些 getter 访问器,对吗?我可以很容易地想象如何使用互斥锁(在处理之前锁定(2),在退出时在(2)中释放,在访问之前锁定(1),在退出之前在(1)中释放)但几乎不能提出一种方法原子操作
  • 另外,很清楚如何对回调做同样的事情——我们只是在到达我们库代码中的某个点时调用用户指定的回调。但就我而言,至少有两个问题:1. 如果库和客户端应用程序是用不同的语言编写的怎么办?例如,在我的案例中,库是纯 C 语言,而大多数客户端都是基于 C++/Qt 的。 2. 如果我们在执行库例程时应该做多个动作怎么办?在这种情况下,传递一堆回调似乎不是一种可扩展的方式,互斥体应该表现得更好。可能是我错了……
  • @DrobotViktor:对于您的示例,每个客户端可以有三个缓冲区,其中包含应显示的状态,并且 UI 可以有两个指针,它们支持具有获取/释放语义的原子加载/存储,一个其中一个标识接下来应该显示的缓冲区,另一个标识当前正在显示的缓冲区。为了设置 UI 状态,客户端选择一个未被这些指针标识的缓冲区,将新数据放在那里,然后将“下一个显示值”指针写入新填充的缓冲区。
  • 使用回调将比直接使用互斥锁更好地适应混合语言程序。使用回调时,使用一种语言创建或等待互斥锁的库可以以相同的方式公开创建或等待互斥锁的函数,而不管调用它的语言是什么。否则,如果用 Acme C 编写的库使用 Acme mutex 函数创建了一个互斥锁,并且 Bob 的 C 编译器中的一个库尝试使用 BCC 的互斥锁函数等待它,则很可能会引起欢闹。
【解决方案2】:

我不知道是否有正确的方法,但一个好的方法是为调用代码提供一个不透明的句柄(就像你正在做的那样)并记录你提供的句柄应该只一次由一个线程使用。然后由用户来确保只有一个线程拥有句柄的副本(以便其他线程无法访问其资源,仅仅是因为它们无权访问句柄),或者调用代码可以共享跨线程的句柄,并通过互斥锁显式序列化所有使用该句柄的调用。

这样做的好处是,如果用户想避免互斥锁的开销,他可以;或者如果他想(小心地)在线程之间共享句柄并序列化对它的访问,这也是一种选择。

您当然需要确保您的库代码不会访问任何跨线程共享的内部资源(或者如果这样做,它会在这样做之前锁定共享互斥锁),但听起来像您可能已经在这样做了。

使用这种方法的 C 库的一些示例:C 运行时库(其 FILE * 句柄由 fopen() 返回,libsndfile(其 SNDFILE * 句柄由 sf_open() 返回),OpenSSL(与它的SSL * 句柄由SSL_new() 返回)

请注意,您的选项 (1) 会增加每次调用的开销,而且可能还不够,因为如果用户想按顺序自动/一起调用多个方法,他无论如何都需要手动锁定互斥锁。作为一个反例,Java 的原始 java.util.Hashtable 类实现了每次调用时隐式序列化的方法和 has been deprecated,主要是因为回想起来,这种线程安全方法被视为错误功能。

【讨论】:

  • 哇,我有点吃惊。我一直认为方法(1)更好,但现在我可以看到你的意见。是的,通过使用不透明句柄(实际上是指向已分配结构的指针),我在这些结构中携带了所有必要的状态和数据内容,因此不存在库全局符号。
  • 非阻塞算法可以广泛使用,并且可以设计为支持几乎任意复杂度的原子事务。否则,尝试获取和等待锁的库将要求用户考虑库无法控制的许多极端情况,在许多情况下,如果需要用户代码自行处理锁定,则推理所有适用的极端情况如果管理其他数据结构的库也尝试包含锁定逻辑,这将比它更容易。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多