现在我必须详细说明我之前的评论。@paercebal 答案不正确。在示例代码中,没有人注意到应该作为参数的互斥锁实际上并没有传入?
我对这个结论提出异议,我断言:要使函数在存在并发的情况下安全,它必须是可重入的。因此并发安全(通常写成线程安全)意味着可重入。
线程安全和可重入都没有关于参数的任何内容:我们正在讨论函数的并发执行,如果使用不适当的参数,它仍然可能是不安全的。
例如, memcpy() 是线程安全且可重入的(通常)。显然,如果使用来自两个不同线程的指向相同目标的指针调用它,它将无法按预期工作。这就是 SGI 定义的重点,将责任放在客户端上,以确保对相同数据结构的访问由客户端同步。
重要的是要理解,通常让线程安全操作包含参数是无意义的。如果你做过任何数据库编程,你就会明白。什么是“原子的”并且可能受互斥锁或其他技术保护的概念必然是用户概念:在数据库上处理事务可能需要多次不间断的修改。除了客户端程序员,谁能说哪些需要保持同步?
关键是“损坏”不必用非序列化的写入来弄乱计算机上的内存:即使所有单独的操作都被序列化,损坏仍然可能发生。因此,当您询问一个函数是线程安全的还是可重入的时,这个问题意味着所有适当分离的参数:使用耦合参数并不构成反例。
那里有许多编程系统:Ocaml 就是其中之一,我认为 Python 也是如此,其中包含许多不可重入代码,但它使用全局锁来交错线程访问。这些系统不是可重入的,也不是线程安全的或并发安全的,它们安全运行仅仅是因为它们阻止了全局并发。
一个很好的例子是malloc。它不是可重入的,也不是线程安全的。这是因为它必须访问全局资源(堆)。使用锁并不能保证安全:它绝对不能重入。如果 malloc 的接口设计得当,就可以使其可重入和线程安全:
malloc(heap*, size_t);
现在它可以安全了,因为它将对单个堆的共享访问序列化的责任转移给了客户端。特别是如果有单独的堆对象,则不需要任何工作。如果使用公共堆,则客户端必须序列化访问。在函数内部使用锁是不够的:只需考虑一个 malloc 锁定堆*,然后出现一个信号并在同一个指针上调用 malloc:死锁:信号无法继续,客户端也不能,因为它被打断。
一般来说,锁不会使事情成为线程安全的......它们实际上通过不恰当地尝试管理客户端拥有的资源来破坏安全性。锁定必须由对象制造商完成,这是唯一知道创建了多少对象以及如何使用它们的代码。