125

I hear that const means thread-safe in C++11. Is that true?

Does that mean const is now the equivalent of Java's synchronized?

Are they running out of keywords?

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
K-ballo
  • 80,396
  • 20
  • 159
  • 169
  • 1
    The C++-faq is generally administered by the C++ community, and you could kindly come and ask us for opinions in our chat. – Puppy Jan 02 '13 at 19:23
  • @DeadMG: I was unaware of the C++-faq and its etiquette, it was suggested in a comment. – K-ballo Jan 02 '13 at 19:26
  • 2
    Where did you hear that const means thread-safe? – Mark B Jan 02 '13 at 20:33
  • 2
    @Mark B: _Herb Sutter_ and _Bjarne Stroustrup_ were saying so at the _Standard C++ Foundation_, see the link at the bottom of the answer. – K-ballo Jan 02 '13 at 20:36
  • **NOTE TO THOSE COMING HERE: the real question is NOT whether `const` *means* thread-safe.** That would be nonsense, since otherwise it'd mean you should be able to just go ahead and mark every thread-safe method as `const`. Rather, the question we're really asking is `const` **IMPLIES** thread-safe, and that's what this discussion is about. – user541686 Jul 30 '19 at 07:00
  • People reading this question may also be interested in: [What is the definition of a thread safe function in C++11?](https://stackoverflow.com/questions/67143880/what-is-the-definition-of-a-thread-safe-class-member-function-according-to-the?noredirect=1#comment118711010_67143880) – m7913d Apr 19 '21 at 10:44

3 Answers3

146

I hear that const means thread-safe in C++11. Is that true?

It is somewhat true...

This is what the Standard Language has to say on thread-safety:

[1.10/4] Two expression evaluations conflict if one of them modifies a memory location (1.7) and the other one accesses or modifies the same memory location.

[1.10/21] The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

which is nothing else than the sufficient condition for a data race to occur:

  1. There are two or more actions being performed at the same time on a given thing; and
  2. At least one of them is a write.

The Standard Library builds on that, going a bit further:

[17.6.5.9/1] This section specifies requirements that implementations shall meet to prevent data races (1.10). Every standard library function shall meet each requirement unless otherwise specified. Implementations may prevent data races in cases other than those specified below.

[17.6.5.9/3] A C++ standard library function shall not directly or indirectly modify objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s non-const arguments, including this.

which in simple words says that it expects operations on const objects to be thread-safe. This means that the Standard Library won't introduce a data race as long as operations on const objects of your own types either

  1. Consist entirely of reads --that is, there are no writes--; or
  2. Internally synchronizes writes.

If this expectation does not hold for one of your types, then using it directly or indirectly together with any component of the Standard Library may result in a data race. In conclusion, const does mean thread-safe from the Standard Library point of view. It is important to note that this is merely a contract and it won't be enforced by the compiler, if you break it you get undefined behavior and you are on your own. Whether const is present or not will not affect code generation --at least not in respect to data races--.

Does that mean const is now the equivalent of Java's synchronized?

No. Not at all...

Consider the following overly simplified class representing a rectangle:

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

The member-function area is thread-safe; not because its const, but because it consist entirely of read operations. There are no writes involved, and at least one write involved is necessary for a data race to occur. That means that you can call area from as many threads as you want and you will get correct results all the time.

Note that this doesn't mean that rect is thread-safe. In fact, its easy to see how if a call to area were to happen at the same time that a call to set_size on a given rect, then area could end up computing its result based on an old width and a new height (or even on garbled values).

But that is alright, rect isn't const so its not even expected to be thread-safe after all. An object declared const rect, on the other hand, would be thread-safe since no writes are possible (and if you are considering const_cast-ing something originally declared const then you get undefined-behavior and that's it).

So what does it mean then?

Let's assume --for the sake of argument-- that multiplication operations are extremely costly and we better avoid them when possible. We could compute the area only if it is requested, and then cache it in case it is requested again in the future:

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

[If this example seems too artificial, you could mentally replace int by a very large dynamically allocated integer which is inherently non thread-safe and for which multiplications are extremely costly.]

The member-function area is no longer thread-safe, it is doing writes now and is not internally synchronized. Is it a problem? The call to area may happen as part of a copy-constructor of another object, such constructor could have been called by some operation on a standard container, and at that point the standard library expects this operation to behave as a read in regard to data races. But we are doing writes!

As soon as we put a rect in a standard container --directly or indirectly-- we are entering a contract with the Standard Library. To keep doing writes in a const function while still honoring that contract, we need to internally synchronize those writes:

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

Note that we made the area function thread-safe, but the rect still isn't thread-safe. A call to area happening at the same time that a call to set_size may still end up computing the wrong value, since the assignments to width and height are not protected by the mutex.

If we really wanted a thread-safe rect, we would use a synchronization primitive to protect the non-thread-safe rect.

Are they running out of keywords?

Yes, they are. They have been running out of keywords since day one.


Source: You don't know const and mutable - Herb Sutter

Community
  • 1
  • 1
K-ballo
  • 80,396
  • 20
  • 159
  • 169
  • Thanks for formatting that talk, might be worth tagging as c++-faq. – Matthieu M. Jan 02 '13 at 19:08
  • @Matthieu M.: Done. I was unaware of that tag. Thank you. – K-ballo Jan 02 '13 at 19:09
  • Very informative. This appears to forbid reference-counted copy-on-write `std::string` implementations, thread-safety of which came up in a recent question, because `std::string::string(const std:string& other)` is not permitted to mutate a use count in `other`. Do you agree? What implications are there for `std::shared_ptr`? – Ben Voigt Jan 02 '13 at 19:17
  • (That probably deserves its own question or two, which I'll post when I have time.) – Ben Voigt Jan 02 '13 at 19:18
  • 6
    @Ben Voigt: It is my understanding that the _C++11_ specification for `std::string` is worded in a way that already forbids _COW_. I don't remember the specifics, though... – K-ballo Jan 02 '13 at 19:18
  • 3
    @BenVoigt: No. It would merely prevent such things from being unsynchronized- i.e., not thread safe. C++11 already explicitly bans COW- this particular passage has nothing to do with that, though, and would not ban COW. – Puppy Jan 02 '13 at 19:24
  • @DeadMG: I don't see how "shall not directly or indirectly modify" permits synchronized modification. Yes, there's the "accessible from other threads" bit, but synchronization in one function doesn't make it inaccessible from others. – Ben Voigt Jan 02 '13 at 19:26
  • @K-ballo deleted my comment because I realized that calling two methods (`r.width() * r.height()`) would never be thread-safe even if the object was thread-safe internally. – StackedCrooked Jan 02 '13 at 19:28
  • @Ben: "Modify" is defined with reference to 1.10, which permits correctly synchronized writes. – Puppy Jan 02 '13 at 19:52
  • That's funny, I've watched this talk by Herb Sutter this afternoon and I was hoping to find a place to discuss it. Thank you for fulfilling my wish. This said, there's still something I don't quite get in the wording of [17.6.5.9/3]: instead of writing "shall not directly or indirectly **modify**", shouldn't it write "shall not directly or indirectly introduce data races"? I mean, if a `const` function alters a `mutable` variable then it *does* modify the value stored at that memory location. Simply, this modification does not turn out to create data races per [1.10/21] because it is atomic. – Andy Prowl Jan 03 '13 at 00:24
  • @Andy Prowl: DeadMG covered that a few comments up, is _modify_ as described by 1.10 – K-ballo Jan 03 '13 at 00:27
  • @K-ballo: Point 1.10 contains 25 paragraphs. Reading quickly through them I could not find out which one does explain that an operation which is not a *modify* may include writes other than locking mutexes etc. But I accept I may have just overlooked it. I will keep searching, but if you have time could you please point me to the right paragraph? – Andy Prowl Jan 03 '13 at 00:32
  • @K-ballo: maybe I'm just too stupid to get it, but I need help. p21 defines "data race" and in particular it states that there's no data race when one of the actions is atomic. However, it doesn't say that the atomic action is *not* a modify, while STL's paragraph explicitly forbids modifications. Which is too much I think. It should only forbid data races. Am I writing nonsense? – Andy Prowl Jan 03 '13 at 00:44
  • @Andy Prowl: I think I misunderstood your comment. The specification does not guarantee that it does not create races, neither does forbid modifications. It just considers that writes can only be introduced by calls on non-const objects, and if thats true then it will not introduce data races – K-ballo Jan 03 '13 at 00:46
  • @K-ballo: But if you write a synchronized mutable variable, that's a write. And [17.6.5.9/3] explicitly says "shall not directly or indirectly **modify**". Btw is it OK for you to continue here, or shall we move to the chat? Or you don't have time for that? – Andy Prowl Jan 03 '13 at 00:48
  • @Andy Prowl: I can't give it my full attention at the moment, but I would like to continue this discussion at the chat later today. – K-ballo Jan 03 '13 at 00:54
  • @K-ballo: it is 2 AM here so I don't know how long I will be around. is there a way for you to drop me a message if I won't be here later? Anyway, I'm going to clarify my point in the following comment (hope there will be enough space) – Andy Prowl Jan 03 '13 at 00:56
  • 1
    if I got it right, the whole point is: "inside a `const` function you *can* modify a `mutable` variable, as long as you do that *atomically* (i.e. with proper synchronization)". However, [17.6.5.9/3] says: [an STL function] "shall not directly or indirectly **modify**" [its arguments by (among other things) calling one of their `const` member functions]. But since those `const` member functions can modify `mutable` variables, this means modifying a `mutable` variable is not seen a "**modify**" per [17.6.5.9/3] *as long as you do it atomically*. And I cannot find where this is stated. – Andy Prowl Jan 03 '13 at 01:00
  • 3
    It seems to me that there is a logical gap. [17.6.5.9/3] forbids "too much" by saying "it shall not directly or indirectly modify"; it should say "shall not directly or indirectly introduce a data race", *unless* an atomic write is somewhere defined *not* to be a "modify". But I can't find this anywhere. – Andy Prowl Jan 03 '13 at 01:02
  • @Andy Prowl: I think I see your point... Modifications via const are not forbidden, but an implementation of the standard library shall not do those in order to avoid data races (read that paragraph in context with paragraph 1). If that is not enough, then its up to you to avoid data races. – K-ballo Jan 03 '13 at 02:24
  • @K-ballo I don't know, as much as I understand the spirit and the intended meaning of the whole thing - which in the end is what matters - I still believe that *technically* there is a logical gap in the wording which prevents it from expressing that intended meaning. Even from a grammatical viewpoint, the mere presence of the paragraph number "(1.10)" in [17.6.5.9/3] after "modify objects" is not enough to qualify that expression as "modify objects *in a way that introduces a data race as per (1.10.4)*", although this is most likely what is meant. – Andy Prowl Jan 03 '13 at 02:39
  • 1
    I probably made my whole point a little bit clearer here: http://isocpp.org/blog/2012/12/you-dont-know-const-and-mutable-herb-sutter Thank you for trying to help anyway. – Andy Prowl Jan 03 '13 at 02:41
  • 1
    sometimes I wonder who was the one (or the ones directly involved) actually responsible for writing down some standard paragraphs such as these. – oblitum May 30 '13 at 20:37
  • Is the `std::lock_guard` needed inside the `set_size` to make the `area` function thread-safe (i.e. comply with the requirements of the Standard Library)? – m7913d Apr 17 '21 at 22:05
  • Is it correct to say that `area` is thread safe even if it access memory that could be changed by `set_size` simultaneously resulting a data race? See this [related question](https://stackoverflow.com/questions/67143880/what-is-the-definition-of-a-thread-safe-class-member-function-according-to-the-c). – m7913d Apr 17 '21 at 23:34
2

This is an addition to K-ballo's answer.

The term thread-safe is abused in this context. The correct wording is: a const function implies thread-safe bitwise const or internally synchronised, as stated by Herb Sutter (29:43) himself

It should be thread-safe to call a const function from multiple threads simultaneously, without calling a non-const function at the same time in another thread.

So, a const function should not (and will not most of the time) be really thread-safe, as it may read memory (without internal synchronisation) that could be changed by another non-const function. In general, this is not thread-safe as a data race occurs even if only one thread is writing (and another reading the data).

See also my answer to the related question What is the definition of a thread safe function according to the C++11 (Language/Library) Standard?.

m7913d
  • 10,244
  • 7
  • 28
  • 56
  • I think the first sentence of your last paragraph is misleading, since a `const` function can read memory that could be changed by another *`const`* function, too. (Imagine a `const` function that does `++this->p->value`.) – user541686 Oct 27 '22 at 07:02
  • No, if a const function does `++this->p->value` (without internal synchronisation), it is not `bitwise const`. So, it violates the rule anyway. In my last paragraph I try to explain that it is ok to make a const function not really thread safe (as long as it bitwise const or internally synchronised), i.e. it is allowed to read memory without internal synchronisation. So, calling only const functions is (or should be) thread-safe, but it is ok that your implementation is not thread safe if a const and a non-const function are called simultaneously. The caller should avoid this from happening. – m7913d Oct 27 '22 at 11:13
  • If a const function does `++this->p->value`, it is not bitwise const. So, it should be internally synchronised, i.e. each access (read and write) to `this->p->value` should be internally synchronised (mutexes, atomic operations). In this case the const function will be "really" thread-safe (independent of any other function that is called simultaneously and concerning accessing `this->p->value`). – m7913d Oct 27 '22 at 11:26
0

No! Counterexample:

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

The copy-constructor of C is perfectly legitimate, but it is not thread-safe despite C being const.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • As explained in `K-Ballo`'s answer. This statement is not about a hard requirement that is enforced by the compiler (or C++ Standard Language), but an assumption from the _Standard library_. So, all functions interacting with the standard library should obey this rule to ensure that no data races occur. Assuming interaction with the standard library is common, all code should obey this rule as good practice. – m7913d Oct 27 '22 at 20:01
  • @m7913d: What I was trying to get at was, "obey this rule" is not something you (or the stdlib) can safely assume, because callers have good reason not to follow it: it requires e.g. atomic read-modify-write, which is *much* slower than single-threaded code. Considering that "good practice" also includes "avoid sharing data across threads when you don't need to", it's perfectly logical for programs to avoid synchronization for single-threaded data structures. Given it's impossible to detect violations, the implication here is you cannot assume anything you see as `const` is thread-safe. – user541686 Oct 28 '22 at 10:26
  • As far as I understand, this is something that is effectively assumed by C++11 Standard Library. So, all function interacting with it, should obey this rule, otherwise it may be undefined behavior (which may be not a problem in most cases as most Standard Library functions doesn't use multiple threads anyway). Note that if the const functions are bitwise const, the non-const functions may perform non-atomic/non-synchronised writes. Do you often use const functions that are not bitwise const? – m7913d Oct 28 '22 at 12:14
  • @m7913d: If the standard library assumed you only called it on a full moon and declared other uses UB, you would ignore that clause unless you were actually doing something moon-related. This is basically the same thing - and your parenthesized statement is precisely why the standard library's opinion on this (whether right or wrong) is irrelevant in practice. As for whether I do this "often", no - and I'm not suggesting you should go out of your way to *write* thread-unsafe const code if you can avoid it. I'm just saying it's not an assumption that *readers* can make, for the reasons above. – user541686 Oct 29 '22 at 05:04