当可以从多个线程访问/更新变量时,通常需要保护它免受同时更改。一种有效的方法是使用原子函数来保证互斥访问。例如(sb-ext:atomic-incf *count*)
。另一种方法是像这样(bt:with-lock-held (*lock*) (incf *count*))
那样在更新操作周围包裹一个锁,但这会花费很多。
在多线程代码中是否存在一种有效的方法来包含库函数(例如亚历山大库中的库函数)?例如,是否要从多个线程执行(alexandria:deletef x *list*)
?还是需要锁? (ps:我假设deletef
需要保护,但不能完全确定。)
答案 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上)以优化执行速度。
有一些警告:
它仅适用于事务感知类型,例如tvar
,tcell
,tcons
,tlist
,tmap
,{{1} },tfifo
,用tchannel
定义的类或用(stmx:transactional (defclass ...))
定义的结构
(stmx:transactional (defstruct ...))
中的代码可能会重试多次,因此它不应执行I / O。
通常比使用原子操作的手动优化代码要慢,但编写起来却容易得多,还因为它是可组合的:(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
之类的库函数可以正常工作。