为啥 QPainterPath::contains() 尽管是 const 却不是线程安全的?

Posted

技术标签:

【中文标题】为啥 QPainterPath::contains() 尽管是 const 却不是线程安全的?【英文标题】:Why QPainterPath::contains() is not thread-safe, despite being const?为什么 QPainterPath::contains() 尽管是 const 却不是线程安全的? 【发布时间】:2022-01-14 09:20:27 【问题描述】:

虽然我知道 QT 声明只有特别声明的类是线程安全的,但我想了解为什么一个“const”标记的方法 - QPainterPath::contains() - 在并行循环中被调用时会中断没有任何并发​​写操作:

#include <QPainterPath>
#include <omp.h>
#include <iostream>

int main(int argc, char *argv[])

    QPainterPath path;
    path.addRect(-50,-50,100,100);

    #pragma omp parallel for
    for(int x=0; x<100000; ++x)
        if(!path.contains(QPoint(0,0)))
            std::cout << "failed\n";

    return 0;

上面的代码在不应该的时候随机输出“失败”。

我的理解是,尽管方法是“const”,但它正在以某种方式改变其内部状态: https://code.woboq.org/qt5/qtbase/src/gui/painting/qpainterpath.cpp.html#_ZNK12QPainterPath8containsERK7QPointF

我需要比较点是否在来自多个线程的路径内(以加快处理速度),但它不适用于 QPainterPath。即使我为每个线程创建了对象的副本,QT 也会在写入时复制,除非我更改派生对象(强制它分离),否则结果仍然是相同的错误行为,因为它仍然使用相同的共享数据。如果没有这种丑陋的黑客,我怎样才能以安全的方式做到这一点?

【问题讨论】:

你说得对,这很奇怪,但严格来说,const 成员函数不是必需线程安全的。标准库容器提供了这种保证,并且可以要求它们包含的元素。但该标准实际上并不要求所有类型都遵守此约定。例如,出于性能原因或类似原因,类型可能会在 const 成员函数中以非线程安全的方式执行一些内部缓存。 @AdrielJr 这是一个错误的假设。成员函数上的const 仅意味着您可以在const 实例上调用该函数,这意味着对象的可见状态不应更改。但是内部状态可能会发生很大变化。见mutable。在这种情况下,COW 似乎不太可能发生,但可能会发生类似惰性求值的情况。 @AdrielJr const!=pure (我希望它出现在语言中)。 const 方法可以轻松地对指针或引用持有的成员执行非常量操作,更不用说mutable 关键字了。现在,人们可以争论这是一种好还是坏的风格或语言缺陷;底线是: const 只能指逻辑 constness。如果它也是二进制不变性,那很好,但它不能在语言级别上强制执行。 @AdrielJr 您可以将多种可能且自洽的定义归因于具有不同粒度和范围级别的“恒定”和“不可变”。不可能用一个关键字来描述它们。我很高兴我们有 constconst 成员函数。对于大多数其他语言来说,情况并非如此。我希望该语言能让我们定义额外的限定符,例如 constvolatile @ArielJr mutable 通常被认为是代码异味,除非应用于同步变量,例如互斥锁或条件变量(可以在读取时更改);缓存有时也适用于此。你可能会问 const_cast 的存在,它有它的位置,它被滥用是一个完全不同的故事。 【参考方案1】:

答案在您链接到的第一行代码中:

if (isEmpty() || !controlPointRect().contains(pt))

controlPointRect() 有以下内容:

if (d->dirtyControlBounds)
    computeControlPointRect();

computeControlPointRect() 执行以下操作:

d->dirtyControlBounds = false;
...
d->controlBounds = QRectF(minx, miny, maxx - minx, maxy - miny);

换句话说,如果你并行调用controlPointRect(),可能会发生以下情况:

线程 T1 看到 d-&gt;dirtyControlBounds 并输入 computeControlPointRect() 将其清除。它开始计算边界。 线程 T2 进入 controlPointRect() 并看到 d-&gt;dirtyControlBounds 为假。它检查d-&gt;controlBounds(此时是一组空的点)是否包含该特定点。它没有,所以它返回 false。 线程 T1 完成并更新 d-&gt;controlBounds。从现在开始,所有线程都是同步的。

对于这个特定实例的明显解决方法是确保在您进入大规模并行计算之前清除所有脏位,但这可能不适用于所有对象。

【讨论】:

感谢您非常好的解释。在进入并行循环之前,我会进行一次测试调用它。尽管如此,我的理解是,将此函数标记为 const 是没有意义的,因为它们在内部使用了肮脏的设计模式。我至少绝对不会在我的代码中这样做。 只是为了让您知道在并行环境之前调用 controlPointRect() 可以解决问题。 如果答案对您有帮助,请将其标记为已接受,以便其他人更容易找到它。 我刚做了。谢谢。您可能也想为这个问题投票。 @AdrielJr 请注意,您现在依赖于实现细节。想象一下某个变化(例如升级),您的代码可能会无缘无故地以各种方式中断。

以上是关于为啥 QPainterPath::contains() 尽管是 const 却不是线程安全的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 glTranslatef?为啥不直接更改渲染坐标?

为啥 DataGridView 上的 DoubleBuffered 属性默认为 false,为啥它受到保护?

为啥需要softmax函数?为啥不简单归一化?

为啥 g++ 需要 libstdc++.a?为啥不是默认值?

为啥或为啥不在 C++ 中使用 memset? [关闭]

为啥临时变量需要更改数组元素以及为啥需要在最后取消设置?