https://en.wikipedia.org/wiki/Software_transactional_memory#Composable_operations
也许最基本的反对意见是基于锁定的 程序不构成:组合时正确的片段可能会失败。对于 例如,考虑具有线程安全插入和删除的哈希表 操作。现在假设我们要从表中删除一个项目A. t1,并将其插入表t2;但中间状态(其中 任何表都不包含该项目)其他线程不得显示。 除非哈希表的实现者预见到这种需要,否则就有了 根本无法满足这一要求。 [...] 简而言之, 单独正确的操作(插入,删除)不能 组成更大的正确操作。 -Tim Harris等人, "可组合内存事务",第2部分:背景,第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的线程安全代码和完全顺序一致性。
但确实说过这个案例?
也许最基本的反对意见是基于锁定的 程序不构成:组合时正确的片段可能会失败。
答案 0 :(得分:4)
你的程序完全没问题。
另一个程序,对于另一个线程中的不同任务,可能会使用类似
的东西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而不是锁将始终允许这种类型的组合(以某种性能价格)。
答案 1 :(得分:4)
插入和擦除的线程安全原子操作通常会隐藏其中的互斥锁。这可以防止无锁定的访问。
在您的情况下,您改为暴露互斥体。这需要每个用户正确地纠缠互斥锁,否则就会中断。
通过对互斥锁的完全访问权限,您可以排除安全地查看中间状态:您的代码失败,因为它不使用std::lock
来保证互斥锁定顺序是全局一致的,这可能会导致死锁代码使用不同的锁定顺序。
这种问题 - 您必须经常了解交易所需的互斥量,以及您持有的互斥量 - 不会分解成小的,易于确定的正确部分。正确性变得非本地化,然后复杂性爆炸,错误比比皆是。