在多线程代码中使用库函数 (Common Lisp)

Using Library Functions in Multi-threaded Code (Common Lisp)

当一个变量可以 accessed/updated 来自多个线程时,它通常需要防止同时更改。一种有效的方法是使用原子函数来保证互斥访问;例如,(sb-ext:atomic-incf *count*)。另一种方法是像这样 (bt:with-lock-held (*lock*) (incf *count*)) 对更新操作加锁,但这有点昂贵。

有没有一种有效的方法可以在多线程代码中包含库函数(比如来自 alexandria 库)?例如,如果您想从多个线程执行 (alexandria:deletef x *list*)?或者你需要做一个锁? (ps:我假设 deletef 需要保护,但不完全确定。)

您可以使用 STMX 通过“乐观锁定”获取软件事务。

这适用于标记为事务性的 类,或库也提供的事务性原语:tcell、tcons 等。您需要使用它们,或将其他东西包装到其中。这些结构中的位置可供位置机制使用,因此像 alexandria:deletef 这样的库函数可以正常工作。

完全通用的、优化的多线程代码是出了名的难写。

最简单的解决方案通常是使用锁来保护并发修改,如您提供的示例所示:(bt:with-lock-held (*lock*) (incf *count*))。在大多数情况下,性能是可以接受的。如果您对特定用例进行基准测试并发现它对您的需求来说太慢,我只会考虑下面的其他选项。

作为 (sb-ext:atomic-incf *count*) 的原子操作是非常低级的原语:非常快,但很难正确组合成更复杂的操作。 如果您需要的功能可以一对一映射到原子操作,那么您可以直接使用它们就可以了。 但大多数时候,您需要组合原子操作以提供更复杂的功能——困难随之而来:您需要深入了解您正在使用的架构,包括(缺乏)内存顺序保证和内存屏障。这是一条极其艰难的道路。

我的库 STMX 提供了据称直观且易于编写的原语,例如 (stmx:atomic (incf *count*))。 它在内部使用原子操作(如果可用)和 Intel TSX 事务内存 CPU 指令(仅在 sbcl x86-64 上并且仅在具有它们的 Intel CPUs 上)来优化执行速度。

它有一些注意事项:

  1. 它只适用于事务感知类型,如tvartcelltconstlisttmaptfifotchannel、类 定义为 (stmx:transactional (defclass ...)) 或结构定义为 (stmx:transactional (defstruct ...))

  2. (stmx:atomic ...)里面的代码在多线程冲突的情况下可能会重试多次,所以不应该执行I/O.

  3. 它通常比使用原子操作的手动优化代码慢,但它更容易编写,也因为它是可组合的:(atomic (atomic (foo) (atomic (bar)) 是单个事务(不是三个)相当于 (atomic (foo) (bar)).

在您的特定情况下,您想在多线程代码中使用现有的非线程安全库调用作为 (alexandria:deletef x *list*),即您想让它们成为线程安全的。

如果库不在内部使用和改变全局变量,则可以成功使用锁和原子操作。相反,只有当您同时修改事务感知类型时才能使用 STMX - 可以使用库提供的类型,但应被并发代码视为只读。

如果库在内部使用和改变全局变量,您将受到更多限制,锁可能是唯一可行的解​​决方案。

P.S。请在 https://github.com/cosmos72/stmx/issues 提交 STMX 问题 @Svante 刚刚更新 https://github.com/cosmos72/stmx/issues/14 上面评论中的 @davipough 错误,但需要准确的版本来解决问题。