三重检查锁定?

发布于 2024-08-23 20:14:56 字数 2156 浏览 8 评论 0原文

因此,同时我们知道双重检查锁定在 C++ 中不起作用,至少不能以可移植的方式起作用。

我刚刚意识到我在用于地形光线追踪器的惰性四叉树中有一个脆弱的实现。因此,我试图找到一种仍然以安全的方式使用延迟初始化的方法,因为我不想将内存使用量增加四倍并重新排序大部分已实现的算法。

这种遍历的灵感来自于 C++ 和双重检查锁定的危险,但试图做得更便宜:

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            // get updated view
            #pragma flush childCreated[c]
            if (!childCreated[c]) { 
                ScopedLock sl (mutex[c]);
                if (!childCreated[c]) {
                    create (c);
                    #pragma flush childCreated[c]  
                    childCreated[c] = true;
                }
            }
        }
    }
}

假设#pragmalush也将充当硬序列点,不允许编译器和处理器在它们之间重新排序操作。

您看到哪些问题?

编辑:版本 2,尝试考虑 Vlads 的回答(引入第三次冲洗):

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            // get updated view
            #pragma flush childCreated[c]
            if (!childCreated[c]) { 
                ScopedLock sl (mutex[c]);
                #pragma flush childCreated[c]
                if (!childCreated[c]) {
                    create (c);
                    #pragma flush childCreated[c]
                    childCreated[c] = true;
                }
            }
        }
    }
}

编辑:版本 3,我不知何故发现这与版本 2 相当,因为我是不使用子级本身,而是使用原始标志来检查有效性,基本上依赖于创建子级和写入该标志之间的内存屏障。

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            ScopedLock sl (mutex[c]);
            #pragma flush childCreated[c]
            if (!childCreated[c]) {
                create (c);
                #pragma flush childCreated[c]
                childCreated[c] = true;
            }
        }
    }
}

So in the meanwhile we know that double-checked-locking as is does not work in C++, at least not in a portable manner.

I just realised I have a fragile implementation in a lazy-quadtree that I use for a terrain ray tracer. So I tried to find a way to still use lazy initialization in a safe manner, as I wouldn't like to quadruple memory usage and re-order large parts of implemented algorithms.

This traversal is inspired by the pattern on page 12 of C++ and the Perils of Double-Checked Locking, but tries to do it cheaper:

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            // get updated view
            #pragma flush childCreated[c]
            if (!childCreated[c]) { 
                ScopedLock sl (mutex[c]);
                if (!childCreated[c]) {
                    create (c);
                    #pragma flush childCreated[c]  
                    childCreated[c] = true;
                }
            }
        }
    }
}

It is assumed that #pragma flush would also serve as a hard sequence point where compilers and processors won't be allowed to re-order operations across them.

Which problems do you see?

edit: Version 2, trying to take into account Vlads answer (introduce third flush):

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            // get updated view
            #pragma flush childCreated[c]
            if (!childCreated[c]) { 
                ScopedLock sl (mutex[c]);
                #pragma flush childCreated[c]
                if (!childCreated[c]) {
                    create (c);
                    #pragma flush childCreated[c]
                    childCreated[c] = true;
                }
            }
        }
    }
}

edit: Version 3, I somehow find this pretty equivalent to Version 2, because I am not using the child itself but a primitive flag to check for validity, basically relying on a memory barrier between creating a child and writing to that flag.

(pseudo code!)

struct Foo {
    bool childCreated[4];
    Mutex mutex[4];
    Foo child[4];

    void traverse (...) {
        ...
        if (!childCreated[c]) { 
            ScopedLock sl (mutex[c]);
            #pragma flush childCreated[c]
            if (!childCreated[c]) {
                create (c);
                #pragma flush childCreated[c]
                childCreated[c] = true;
            }
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

情深已缘浅 2024-08-30 20:14:56

看来你的模式不正确。考虑线程#1 执行到第一个#pragmalush 之后的情况。然后控制权切换到线程 #2,该线程继续创建一个 c,控制权在第二个 #pragmalush 之前被收回。现在第一个线程醒来,并重新创建子线程。

编辑:抱歉,错误:它将无法锁定。

编辑 2:不,仍然正确,因为该值不会在线程 #1 中刷新

It seems that your pattern is not correct. Consider the case when thread #1 executes till after the first #pragma flush. Then the control switches to the thread #2, which goes on and creates a c, the control is taken back just before second #pragma flush. Now the first thread wakes up, and creates the child anew.

Edit: sorry, wrong: it will be unable to take the lock.

Edit 2: no, still correct, because the value will be not flushed in thread #1

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文