无锁代码中的延迟初始化

Lazy initialization in lock free code

本文关键字:延迟 初始化 代码      更新时间:2023-10-16

假设我有一个原子指针:

std::atomic<void*> mItems;

在一个函数中,当一个线程需要访问它时,它首先检查它,如果它是空的,线程将为它分配内存:

void* lItems = std::atomic_load_explicit(&mItems, memory_order_relaxed);
if(lItems == nullptr)
{
    void* lAllocation = malloc(...);
    if(!std::atomic_compare_exchange_strong_explicit(
        &mItems, 
        &lItems, 
        lAllocation, 
        memory_order_relaxed, 
        memory_order_relaxed))
    {
        free(lAllocation);
    }
}
    ...

但是,如果 N 线程并发运行此方法并发并看到mItems等于 null,则它们都将分配内存,其中 N - 1 个将再次释放。

我如何用更好的方法编写类似的方法。

我想你可以让指针成为你的互斥锁,使用一些众所周知的值(比如全局的地址(作为其他线程已经在做分配的标志。

因此,您的值为:NULL ->神奇的"分配正在进行"指针 ->实际分配。

代码将执行以下操作:

  • 加载地址:它将具有以下值之一:
    1. 空:具有魔法值的 CAS
      • 中科院成功了吗?如果是,我们正在进行分配,每个人都知道
        • 进行分配,存储新地址,我们就完成了(不必对其进行 CAS,因为我们已经保证了第一个 CAS 的排除(
      • 不,然后其他人正在分配,回到 1
    2. 地址不是空,而是魔术值
      • 所以有人已经在进行分配 - 只需等到它发生变化,然后使用最终值
    3. 既不是 NULL 也不是魔法,所以它已经是一个真正的分配值 - 只需使用它

这样,只有一个线程执行分配,但其他 N-1 线程可能正忙于等待。这是否真的更好会有所不同...

据我所知,一旦第一个线程执行您的函数,您就需要该结构,那么在启动任何线程之前移动分配怎么样?我的意思是,重新排列代码,以便使用原子指针作为参数调用您的函数,并且在生成任何调用您的函数的线程之前分配结构(如果分配失败,您还可以避免创建任何线程(

像这样:

std::atomic<void*> mItems;
void func_that_uses_mItems();
int main()
{
    mItems = init_struct();
    std::thread t1 { &func_that_uses_mItems };
    std::thread t2 { &func_that_uses_mItems };

    // ... join with or detach threads ...
    return( 0 );
}

如果分配失败,则会引发异常,并且不会启动任何线程。