为啥基于锁的程序不能组成正确的线程安全片段?
Posted
技术标签:
【中文标题】为啥基于锁的程序不能组成正确的线程安全片段?【英文标题】:Why lock-based programs do not compose correct thread-safe fragments?为什么基于锁的程序不能组成正确的线程安全片段? 【发布时间】:2016-07-02 21:15:08 【问题描述】:蒂姆哈里斯说:
https://en.wikipedia.org/wiki/Software_transactional_memory#Composable_operations
也许最基本的反对意见 [...] 是基于锁的 程序不组合:正确的片段在组合时可能会失败。为了 例如,考虑具有线程安全插入和删除的哈希表 操作。现在假设我们要从表中删除一项 A t1,并将其插入到表 t2 中;但中间状态(其中 两个表都不包含该项目)不得对其他线程可见。 除非哈希表的实现者预料到这种需要,否则 根本没有办法满足这个要求。 [...] 简而言之, 单独正确的操作(插入、删除)不能 组成更大的正确运算。 ——蒂姆·哈里斯等人, “可组合内存事务”,第 2 节:背景,pg.2[6]
这是什么意思?
如果我有 2 个哈希映射 std::unordered_map
和 2 个互斥锁 std::mutex
(每个哈希映射一个),那么我可以简单地锁定它们:http://ideone.com/6RSNyN
#include <iostream>
#include <string>
#include <mutex>
#include <thread>
#include <chrono>
#include <unordered_map>
std::unordered_map<std::string, std::string> map1 ( "apple","red","lemon","yellow" );
std::mutex mtx1;
std::unordered_map<std::string, std::string> map2 ( "orange","orange","strawberry","red" );
std::mutex mtx2;
void func()
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2);
std::cout << "map1: ";
for (auto& x: map1) std::cout << " " << x.first << " => " << x.second << ", ";
std::cout << std::endl << "map2: ";
for (auto& x: map2) std::cout << " " << x.first << " => " << x.second << ", ";
std::cout << std::endl << std::endl;
auto it1 = map1.find("apple");
if(it1 != map1.end())
auto val = *it1;
map1.erase(it1);
std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(1000));
map2[val.first] = val.second;
int main ()
std::thread t1(func);
std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(500));
std::thread t2(func);
t1.join();
t2.join();
return 0;
如果我想实现自己的线程安全哈希映射my_unordered_map
,那么我将实现这样的事情:
template<typename key, template val>
class my_unordered_map
std::recursive_mutex mtx_ptr;
void lock() mtx_ptr->lock();
void unlock() mtx_ptr->unlock();
template<typename mutex_type> friend class std::lock_guard;
public:
// .. all required public methods which lock recursive mutex before do anything
void insert(key k, val v) std::lock_guard<std::recursive_mutex> lock(mtx); /* do insert ... */
// ...
;
并且会这样使用它:
my_unordered_map<std::string, std::string> map1 ( "apple","red","lemon","yellow" );
my_unordered_map<std::string, std::string> map2 ( "orange","orange","strawberry","red" );
void func()
std::lock_guard<my_unordered_map> lock1(map1);
std::lock_guard<my_unordered_map> lock2(map2);
// work with map1 and map2
// recursive_mutex allow multiple locks in: lock1(map1) and map1->at(key)
类似地,我得到了 map1 和 map2 的线程安全代码和完全顺序一致性。
但是,这是针对哪些情况说的?
也许最基本的反对意见 [...] 是基于锁的 程序不组合:正确的片段在组合时可能会失败。
【问题讨论】:
大概是为了让操作在事务上失败:只有当 map2 不包含密钥 k 时,才从 map1 中提取密钥 k。你不能在每个映射中只使用一个私有互斥锁,因为事务需要将两个互斥锁锁定在一起。 【参考方案1】:你的程序本身就很好。
另一个程序,对于另一个线程中的不同任务,可能会使用类似的东西
void func_other()
std::lock_guard<my_unordered_map> lock2(map2);
std::lock_guard<my_unordered_map> lock1(map1);
// work with map1 and map2
再一次,这很好,就其本身而言。
但是,如果我们同时运行这两个程序,可能会出现死锁。线程 1 锁定映射 1,线程 2 锁定映射 2,现在两个线程中的下一个锁定将永远等待。
因此我们不能天真地编写这两个程序。
使用 STM 而不是锁总是允许这样的组合(以某种性能为代价)。
【讨论】:
谢谢。是的,如果更改锁的顺序,基于锁的容器可能会出现死锁,即“组合时可能失败”意味着我们可以重写这个组合代码以避免任何失败。但是两个无锁容器一般不能使两个表同时保持一致。 IE。对于两张表的一致性,只有STM可以保证不出现故障。但是,像任何事务系统一样死锁 STM 可能会生成事务提交失败作为事务回滚(内部死锁或更新冲突),我们可以在软件中处理,不是吗? @Alex 是的。在大多数 STM 方法中,即使存在一些冲突,也会提交一个事务,而其他事务可能会回滚。然后 STM 引擎可以重试回滚的事务。 即我有 2 种方法:1. 我应该使用带有受控事务循环的 STM,直到它们完全执行。 2. 或者我应该自己解决基于锁的程序中的死锁,例如,通过在循环中为许多必需的互斥锁中的每一个使用mutex.try_lock()
,也可以在每个一定数量的互斥锁之后使用std::this_thread::yield()
重复,如果 mutex.try_lock() == false
在一定时间(1 微秒或 1 毫秒,...)之后抛出异常 throw my_roll_back_transaction();
,然后捕获它,回滚所有所做的更改并再次重试循环以一起更改一些表。【参考方案2】:
插入和擦除的线程安全原子操作通常将互斥锁隐藏在其中。这可以防止未经锁定的访问。
在您的情况下,您改为公开互斥锁。这需要每个用户正确处理互斥锁,否则它会中断。
通过完全访问互斥锁,您可以安全地排除看到中间状态:您的代码失败,因为它没有使用std::lock
来保证互斥锁锁定顺序是全局一致的,如果其他代码使用这可能会导致死锁不同的锁定顺序。
这类问题(您必须不断了解您的交易需要哪些互斥锁以及您持有哪些互斥锁)不会分解为易于确定正确的小块。正确性变得非局部,然后复杂性爆炸,错误比比皆是。
【讨论】:
是的,我将std::recursive_mutex mtx_ptr;
隐藏为私人会员,只有std::lock_guard
作为朋友才能直接使用。我使用 RAII std::lock_guard
以任何方法锁定此互斥锁,例如:insert()
、erase()
、find()
。此外,我建议使用std::lock_guard
手动锁定容器,以便在一个锁中进行许多操作:find-erase-insert、两表一致性……而且我只看到基于锁的程序的一个潜在问题:不同订单上的死锁锁。以上是关于为啥基于锁的程序不能组成正确的线程安全片段?的主要内容,如果未能解决你的问题,请参考以下文章