125

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

这是否意味着const现在相当于Javasynchronized

他们用完了关键字吗?

4

2 回答 2

142

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

有点真实……

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

[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),除非通过函数的非const参数直接或间接访问对象,包括this.

简而言之,它希望对const对象的操作是线程安全的。这意味着只要对您自己类型的对象进行操作,标准库就不会引入数据竞争const

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

如果这种期望不适用于您的一种类型,那么直接或间接与标准库的任何组件一起使用它可能会导致数据竞争。总之,从标准库的角度来看,const确实意味着线程安全。重要的是要注意这只是一个合同,编译器不会强制执行它,如果你破坏它,你会得到未定义的行为并且你自己。是否存在不会影响代码生成——至少不会影响数据竞争——。const

这是否意味着const现在相当于Javasynchronized

没有。一点也不...

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

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;
    }
};

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

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

但这没关系,rect不是这样的,毕竟const它甚至不应该是线程安全的。const rect另一方面,声明的对象将是线程安全的,因为不可能进行写入(如果您正在考虑-const_cast最初声明的内容const,那么您会得到undefined-behavior就是这样)。

那么这意味着什么呢?

让我们假设——为了争论——乘法运算非常昂贵,我们最好尽可能避免它们。我们可以仅在请求时计算该区域,然后将其缓存以防将来再次请求它:

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用一个非常大的动态分配的整数来替换它,它本质上是非线程安全的,并且乘法非常昂贵。]

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

一旦我们将 arect放入标准容器中——直接或间接地——我们就与标准库签订了合同。为了在遵守合约的同时继续在函数中进行写入,我们需要在内部同步这些写入:const

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;
    }
};

请注意,我们创建了area函数thread-safe,但rect仍然不是thread-safe。在调用area的同时发生的调用set_size可能最终仍会计算错误的值,因为对width和的分配height不受互斥锁的保护。

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

他们用完了关键字吗?

对,他们是。从第一天开始,他们就已经用完了关键字


资料来源你不知道constmutable- Herb Sutter

于 2013-01-02T18:43:20.413 回答
1

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

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

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

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

于 2021-04-21T18:17:52.800 回答