在多线程代码中使用库函数 (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 上)来优化执行速度。
它有一些注意事项:
它只适用于事务感知类型,如tvar
、tcell
、tcons
、tlist
、tmap
、 tfifo
、tchannel
、类 定义为 (stmx:transactional (defclass ...))
或结构定义为 (stmx:transactional (defstruct ...))
(stmx:atomic ...)
里面的代码在多线程冲突的情况下可能会重试多次,所以不应该执行I/O.
它通常比使用原子操作的手动优化代码慢,但它更容易编写,也因为它是可组合的:(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 错误,但需要准确的版本来解决问题。
当一个变量可以 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 上)来优化执行速度。
它有一些注意事项:
它只适用于事务感知类型,如
tvar
、tcell
、tcons
、tlist
、tmap
、tfifo
、tchannel
、类 定义为(stmx:transactional (defclass ...))
或结构定义为(stmx:transactional (defstruct ...))
(stmx:atomic ...)
里面的代码在多线程冲突的情况下可能会重试多次,所以不应该执行I/O.它通常比使用原子操作的手动优化代码慢,但它更容易编写,也因为它是可组合的:
(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 错误,但需要准确的版本来解决问题。