危险指针的内存排序
Posted
技术标签:
【中文标题】危险指针的内存排序【英文标题】:Memory ordering for hazard-pointers 【发布时间】:2020-06-07 03:55:23 【问题描述】:以下代码是在显着简化 hazard-pointer 算法(在this 论文中介绍)之后可以得到的。由于大量的简化,它不能用来代替算法(回答这个问题不需要知道任何关于算法的知识)。但是,我相信它仍然完美地代表了原始算法中的内存排序挑战。
所以问题是最好的内存排序是什么,这样如果ptr->a = 1;
被执行,结果就不会是未定义的(order1
... order5
的值)?
struct T int a = 0; ;
static_assert(std::is_trivially_destructible_v<T>);
std::atomic<T*> anew T();
std::atomic<T*> hnullptr;
// Thread 1
auto ptr = a.load(order1);
h.store(ptr,order2);
if(ptr == nullptr || ptr != a.load(order3))
return;
ptr->a = 1;
// Thread 2
auto ptr = a.exchange(nullptr,order4);
if(ptr != h.load(order5))
delete ptr;
我们知道要执行 ptr->a=1;
,a.exchange
必须在第二个 a.load
之后发生(即使是宽松的内存排序也能保证这一点)。但是,问题是如何确保h.load
会看到h.store
的效果。即使我们只在各处使用顺序内存排序,我也无法弄清楚为什么代码可以工作。
【问题讨论】:
【参考方案1】:为简单起见,这些论文通常假设一个顺序一致的内存模型 - 您引用的论文也是如此。您的示例被高度简化,但它仍然包含危险指针算法的要点。您必须确保线程 2“看到”线程 1 存储的危险指针(即线程 1 已获得安全引用),或者线程 1 看到 a 的更新值。
在我的论点中,我将使用以下符号
- a -sb-> b
表示“a 在 b 之前排序”
- a -sco-> b
表示“在所有顺序一致操作的单个总顺序 S 中 a 先于 b”
- a -rf-> b
表示“b读取a写入的值”(reads-from)
让我们假设所有原子操作都是顺序一致的。这将产生以下情况:
线程 1:a.load() -sb-> h.store() -sb-> a.load() -sb-> ptr->a=1
线程 2:a.exchange() -sb-> h.load() -> delete ptr
由于顺序一致的操作是完全有序的,我们必须考虑两种情况:
h.store() -sco-> h.load()
这意味着h.store() -rf-> h.load()
,即线程 2 保证“看到”写入线程 1 的危险指针,因此它不会删除 ptr(因此线程 1 可以安全地更新ptr->a
)。
h.load() -sco-> h.store()
因为我们还有a.exchange() -sb-> h.load()
(线程2)和h.store() -sb-> a.load()
(线程1),这意味着a.exchange() -sco-> a.load()
和a.exchange() -rf-> a.load()
,即线程1保证“看到”a
的更新值(因此不会尝试更新ptr->a
)。
因此,如果所有操作都是顺序一致的,那么算法就会按预期工作。但是,如果我们不能(或不想)假设所有操作都是顺序一致的怎么办?可以放宽一些操作吗?问题是我们必须确保两个不同线程中两个不同变量(a
和h
)之间的可见性,这需要获取/释放可以提供的更强保证。但是,如果您引入顺序一致的栅栏,则可以放宽操作:
// Thread 1
auto ptr = a.load(std::memory_order_acquire);
h.store(ptr, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if(ptr == nullptr || ptr != a.load(std::memory_order_relaxed))
return;
ptr->a = 1;
// Thread 2
auto ptr = a.exchange(nullptr, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if(ptr != h.load(std::memory_order_relaxed))
delete ptr;
所以我们有以下情况:
线程 1:a.load() -sb-> h.store() -sb-> fence() -sb-> a.load() -sb-> ptr->a=1
线程 2:a.exchange() -sb-> fence() -sb-> h.load() -> delete ptr
标准规定:
对于原子对象M上的原子操作A和B,其中A修改M 和 B 取它的值,如果有 memory_order_seq_cst 栅栏 X 和 Y 使得 A 是排在X之前,Y排在B之前,X排在Y之前S,然后 B 观察 A 的效果或 M 在其修改顺序中的后续修改。
栅栏也是单个总订单的一部分S,所以我们再次需要考虑两种情况:
Thread1 fence -sco-> Thread 2 fence
由于h.store() -sb-> fence()
(线程1)和fence() -sb-> h.load()
(线程2)保证线程2“看到”线程1写入的危险指针。
Thread 2 fence -sco-> Thread 1 fence
由于a.exchange() -sb-> fence()
(线程2)和fence() -sb-> a.load()
(线程1)保证线程1“看到”a
的更新值。
后来的版本正是我在xenium 库中实现危险指针的方式。
【讨论】:
以上是关于危险指针的内存排序的主要内容,如果未能解决你的问题,请参考以下文章
C 语言二级指针作为输入 ( 自定义二级指针内存 | 二级指针 排序 | 通过 交换指针方式 进行排序 )
C 语言二级指针作为输入 ( 自定义二级指针内存 | 二级指针排序 | 抽象业务逻辑函数 )
C 语言二级指针内存模型 ( 指针数组 | 二维数组 | 自定义二级指针 | 将 一二 模型数据拷贝到 三 模型中 并 排序 )