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

时间:2019-05-02 22:32:17

标签: multithreading locking shared-libraries common-lisp

当可以从多个线程访问/更新变量时,通常需要保护它免受同时更改。一种有效的方法是使用原子函数来保证互斥访问。例如(sb-ext:atomic-incf *count*)。另一种方法是像这样(bt:with-lock-held (*lock*) (incf *count*))那样在更新操作周围包裹一个锁,但这会花费很多。

在多线程代码中是否存在一种有效的方法来包含库函数(例如亚历山大库中的库函数)?例如,是否要从多个线程执行(alexandria:deletef x *list*)?还是需要锁? (ps:我假设deletef需要保护,但不能完全确定。)

2 个答案:

答案 0 :(得分:5)

众所周知,完全通用的,优化的多线程代码很难编写。

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

原子操作(sb-ext:atomic-incf *count*)是一个很底层的原语:非常快,但是很难正确地组合成更复杂的操作。 如果可以将所需功能一对一映射到原子操作,则只需使用它们就可以完成。 但是大多数时候,您需要组合原子操作以提供更复杂的功能-随之而来的困难是:您需要深入了解所使用的体系结构,包括(缺少)内存排序保证和内存障碍。这是一条极其艰难的道路。

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

有一些警告:

  1. 它仅适用于事务感知类型,例如tvartcelltconstlisttmap,{{1} },tfifo,用tchannel定义的类或用(stmx:transactional (defclass ...))定义的结构

  2. 如果多个线程之间发生冲突,(stmx:transactional (defstruct ...))中的
  3. 代码可能会重试多次,因此它不应执行I / O。

  4. 通常比使用原子操作的手动优化代码要慢,但编写起来却容易得多,还因为它是可组合的:(stmx:atomic ...)是单个事务(不是三个),等效于(atomic (atomic (foo) (atomic (bar))

在您的特定情况下,您希望在多线程代码中将现有的非线程安全的库调用用作(atomic (foo) (bar)),即要使其成为线程安全的。

如果库未在内部使用全局变量并对其进行突变,则可以成功使用锁和原子操作。相反,只有在您同时仅修改并修改了事务感知类型的情况下,才可以使用STMX-可以使用库提供的类型,但应由并发代码将其视为只读。

如果相反,库在内部使用和更改全局变量,则您的约束会更多,并且锁可能是唯一可行的解​​决方案。

P.S。请在https://github.com/cosmos72/stmx/issues提交STMX问题 @Svante刚刚在https://github.com/cosmos72/stmx/issues/14中用上面的注释中的@davipough错误进行了更新,但是需要确切的版本来解决此问题。

答案 1 :(得分:1)

您可以使用STMX来获得具有“乐观锁定”的软件交易。

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