const 在 C++11 中是否意味着线程安全?

Does const mean thread-safe in C++11?

提问人:K-ballo 提问时间:1/3/2013 最后编辑:Johannes Schaub - litbK-ballo 更新时间:10/29/2022 访问量:19682

问:

我听说这意味着 C++11 中的线程安全。这是真的吗?const

这是否意味着现在相当于 Java 的?constsynchronized

他们的关键词用完了吗?

C ++11 线程安全 常量 C++-FAQ

评论

1赞 Puppy 1/3/2013
C++-faq 通常由 C++ 社区管理,您可以在我们的聊天中向我们询问意见。
0赞 K-ballo 1/3/2013
@DeadMG:我不知道C++常见问题及其礼仪,这是在评论中建议的。
2赞 Mark B 1/3/2013
您从哪里听说 const 表示线程安全?
2赞 K-ballo 1/3/2013
@Mark B:Herb SutterBjarne Stroustrup标准C++基金会是这么说的,请参阅答案底部的链接。
0赞 user541686 7/30/2019
来这里的人请注意:真正的问题不是 const 是否意味着线程安全。这将是无稽之谈,否则这意味着您应该能够继续将每个线程安全方法标记为 .相反,我们真正要问的问题是暗示线程安全的,这就是本次讨论的内容。constconst

答:

148赞 K-ballo 1/3/2013 #1

我听说这意味着 C++11 中的线程安全。这是真的吗?const

在某种程度上是真的......

这就是标准语言对线程安全的看法:

[1.10/4] 如果其中一个表达式计算修改了内存位置 (1.7),而另一个表达式访问或修改了相同的内存位置,则两个表达式计算会发生冲突

[1.10/21] 如果一个程序在不同的线程中包含两个冲突的动作,其中至少一个不是原子的,并且两者都不先于另一个发生,则程序的执行包含数据争用。任何此类数据争用都会导致未定义的行为。

这只不过是发生数据争用的充分条件:

  1. 对给定的事物同时执行两个或多个操作;和
  2. 其中至少有一个是写入。

标准库在此基础上,更进一步:

[17.6.5.9/1] 本节指定了实现为防止数据争用而应满足的要求 (1.10)。除非另有说明,否则每个标准库函数都应满足每个要求。在以下指定的情况以外的情况下,实现可以防止数据争用。

[17.6.5.9/3] C++ 标准库函数不得直接或间接修改由当前线程以外的线程访问的对象 (1.10),除非通过函数的非常参数直接或间接访问对象,包括 .this

简单来说,它期望对对象的操作是线程安全的。这意味着,只要对你自己类型的对象进行操作,标准库就不会引入数据争用constconst

  1. 完全由读取组成 - 也就是说,没有写入 - ;或
  2. 在内部同步写入。

如果此预期不适用于您的某个类型,则直接或间接地将其与标准库的任何组件一起使用可能会导致数据争用。总之,从标准库的角度来看,确实意味着线程安全。需要注意的是,这只是一个合约,编译器不会强制执行,如果你违反它,你会得到未定义的行为,你要靠自己。是否存在不会影响代码生成 - 至少在数据竞争方面不会--.constconst

这是否意味着现在相当于 Java 的?constsynchronized

不可以。一点也不。。。

请考虑以下表示矩形的过于简化的类:

class rect {
    int width = 0, height = 0;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        width = new_width;
        height = new_height;
    }
    int area() const {
        return width * height;
    }
};

member-function线程安全的;不是因为它 ,而是因为它完全由读取操作组成。不涉及写入,并且至少涉及一次写入是发生数据争用所必需的。这意味着您可以从任意数量的线程中调用,并且您将始终获得正确的结果。areaconstarea

请注意,这并不意味着它是线程安全的。事实上,很容易看出,如果对给定的调用同时发生对 的调用,那么最终可能会根据旧宽度和新高度(甚至乱码值)计算其结果。rectareaset_sizerectarea

但这没关系,不是这样,它甚至不期望线程安全。另一方面,声明的对象将是线程安全的,因为不可能写入(如果您正在考虑 -ing 最初声明的东西,那么您将获得未定义的行为,仅此而已)。rectconstconst rectconst_castconst

那么这意味着什么呢?

为了论证起见,让我们假设乘法运算的成本非常高,我们最好尽可能避免使用它们。我们只能在请求时计算面积,然后缓存它以防将来再次请求:

class rect {
    int width = 0, height = 0;

    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        cached_area_valid = ( width == new_width && height == new_height );
        width = new_width;
        height = new_height;
    }
    int area() const {
        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

[如果这个例子看起来太人为了,你可以在心里用一个非常大的动态分配整数来替换,这个整数本质上是非线程安全的,而且乘法的成本非常高。int

member-function 不再是线程安全的,它现在正在执行写入,并且不会在内部同步。有问题吗?对 的调用可能作为另一个对象的复制构造函数的一部分发生,此类构造函数可能已由标准容器上的某些操作调用,此时标准库期望此操作在数据争用方面表现为读取但我们正在写!areaarea

一旦我们直接或间接地将一个放入标准容器中,我们就与标准库签订了合同。为了继续在函数中执行写入,同时仍然遵守该协定,我们需要在内部同步这些写入:rectconst

class rect {
    int width = 0, height = 0;

    mutable std::mutex cache_mutex;
    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        if( new_width != width || new_height != height )
        {
            std::lock_guard< std::mutex > guard( cache_mutex );
        
            cached_area_valid = false;
        }
        width = new_width;
        height = new_height;
    }
    int area() const {
        std::lock_guard< std::mutex > guard( cache_mutex );
        
        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

请注意,我们使函数线程安全,但 still 不是线程安全的。在调用的同时发生的对的调用可能最终仍会计算错误的值,因为对互斥锁的赋值不受互斥锁保护。arearectareaset_sizewidthheight

如果我们真的想要一个线程安全的,我们将使用同步原语来保护非线程安全的rectrect

他们的关键词用完了吗?

是的,他们是。从第一天起,他们就已经用完了关键词


来源You don't know const and mutable - Herb Sutter

评论

6赞 K-ballo 1/3/2013
@Ben Voigt:据我所知,C++11 规范的措辞已经禁止了 COW。不过,我不记得具体细节了......std::string
3赞 Puppy 1/3/2013
@BenVoigt:没有。它只会防止这些东西不同步,即不是线程安全的。C++11 已经明确禁止 COW——不过,这段话与此无关,也不会禁止 COW。
1赞 Andy Prowl 1/3/2013
如果我说对了,重点是:“在函数中,你可以修改一个变量,只要你以原子方式进行修改(即通过适当的同步)”。但是,[17.6.5.9/3]说:[STL函数]“不得直接或间接修改”[其参数(除其他外)调用其成员函数之一]。但是,由于这些成员函数可以修改变量,这意味着只要以原子方式进行修改,修改变量就不会被视为 [17.6.5.9/3] 的“修改”。我找不到这是在哪里说的。constmutableconstconstmutablemutable
3赞 Andy Prowl 1/3/2013
在我看来,存在逻辑上的差距。[17.6.5.9/3]禁止“过多”,说“不得直接或间接修改”;它应该说“不得直接或间接引入数据争用”,除非原子写入在某个地方被定义为不是“修改”。但我在任何地方都找不到这个。
1赞 Andy Prowl 1/3/2013
我可能在这里把我的整个观点说得更清楚一点:isocpp.org/blog/2012/12/......无论如何,感谢您的尝试帮助。
2赞 m7913d 4/22/2021 #2

这是对 K-ballo 答案的补充。

在这种情况下,术语线程安全被滥用。正确的措辞是:const 函数意味着线程安全的按位 const 内部同步,正如 Herb Sutter (29:43) 自己所说

同时从多个线程调用 const 函数应该是线程安全的,而无需在另一个线程中同时调用非常量函数。

因此,const 函数不应该(而且大多数时候也不会)真正是线程安全的,因为它可能会读取可能被另一个非常量函数更改的内存(没有内部同步)。通常,这不是线程安全的,因为即使只有一个线程在写入(另一个线程读取数据),也会发生数据争用。

另请参阅我对相关问题的回答:根据 C++11(语言/库)标准,线程安全函数的定义是什么?

评论

0赞 user541686 10/27/2022
我认为你最后一段的第一句话具有误导性,因为一个函数可以读取也可以被另一个 const 函数更改的内存。(想象一下一个函数。constconst++this->p->value
0赞 m7913d 10/27/2022
否,如果 const 函数有(没有内部同步),则它不是 。所以,无论如何它都违反了规则。在我的最后一段中,我试图解释使 const 函数不是真正的线程安全函数是可以的(只要它是按位 const 或内部同步的),即允许在没有内部同步的情况下读取内存。因此,仅调用 const 函数是(或应该是)线程安全的,但如果同时调用 const 和非 const 函数,您的实现就不是线程安全的,这是可以的。调用方应避免这种情况发生。++this->p->valuebitwise const
0赞 m7913d 10/27/2022
如果 const 函数这样做,则它不是按位 const。因此,它应该是内部同步的,即每次访问(读取和写入)都应该在内部同步(互斥锁、原子操作)。在这种情况下,const 函数将“真正”线程安全(独立于同时调用并涉及访问的任何其他函数)。++this->p->valuethis->p->valuethis->p->value
0赞 user541686 10/27/2022 #3

不!反例:

#include <memory>
#include <thread>

class C
{
    std::shared_ptr<int> refs = std::make_shared<int>();
public:
    C() = default;
    C(C const &other) : refs(other.refs)
    { ++*this->refs; }
};

int main()
{
    C const c;
    std::thread t1([&]() { C const dummy(c); });
    std::thread t2([&]() { C const dummy(c); });
}

的复制构造函数是完全合法的,但它不是线程安全的。CCconst

评论

0赞 m7913d 10/28/2022
正如在回答中解释的那样。此语句不是关于编译器(或 C++ 标准语言)强制执行的硬性要求,而是来自标准库的假设。因此,与标准库交互的所有函数都应遵守此规则,以确保不会发生数据争用。假设与标准库的交互很常见,则所有代码都应遵循此规则作为良好做法。K-Ballo
0赞 user541686 10/28/2022
@m7913d:我想说的是,“遵守这个规则”不是你(或 stdlib)可以安全地假设的,因为调用者有充分的理由不遵循它:它需要例如原子读取-修改-写入,这比单线程代码慢得多。考虑到“良好做法”还包括“在不需要时避免跨线程共享数据”,程序避免单线程数据结构的同步是完全合乎逻辑的。鉴于无法检测到违规行为,这里的含义是你不能假设你看到的任何内容都是线程安全的。const
0赞 m7913d 10/28/2022
据我了解,这是 C++ 标准库有效假设的东西。因此,所有与之交互的函数都应该遵守此规则,否则它可能是未定义的行为(在大多数情况下,这可能不是问题,因为大多数标准库函数无论如何都不会使用多个线程)。请注意,如果 const 函数是按位 const,则非 const 函数可以执行非原子/非同步写入。您是否经常使用非按位 const 的 const 函数?
0赞 user541686 10/29/2022
@m7913d:如果标准库假设你只在满月时调用它,并声明其他用途为 UB,那么除非你真的在做一些与月亮相关的事情,否则你会忽略该子句。这基本上是一回事——你括号里的陈述正是为什么标准图书馆对此的看法(无论是对还是错)在实践中是无关紧要的。至于我是否“经常”这样做,不 - 我并不是建议你应该特意编写线程不安全的常量代码,如果你可以避免它。我只是说,出于上述原因,这不是读者可以做出的假设。