سؤال

The C++ Standard library provides the following guarantees about its types unless stated otherwise:

(1) Read operations (i.e. working on a const object) are thread-safe. That means multiple threads may read from an object at the same time without race conditions as long as no thread is writing (applying a non-const operation) to the object at the same time.

(2) Multiple threads may read and write arbitrary objects at the same time as long as each object is only accessed by at most one thread at a time.

The standard library requires the same guarantees by user types. (You can read about this stuff in GotW #95 or watch Herb at C++ and Beyond 2012 explaining about this.)

Now my question is, if the following conclusion is correct: Since the operator() of std::function is a const member function, it is required to be thread-safe. If the functor passed in at construction has a const operator() member function, then the std::function object can assume it to be thread-safe and just forward the call. However, if the functor passed to it at construction has a mutable operator(), then this operation does not need to be thread-safe, but std::function still needs to be, because the call operator remains const. Hence std::function must externally synchronize calls to the stored mutable functor and hence use a mutex. This implies a performance overhead in case of passing mutable lambdas to the constructor of an std::function.

Is this reasoning correct? If so, are current implementations compliant?

هل كانت مفيدة؟

المحلول

The behavior that Herb is talking about with respect to the standard library's guarantees on data race safety is specified in C++11 §17.6.5.9:

17.6.5.9 Data race avoidance [res.on.data.races]

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.

2 A C++ standard library function shall not directly or indirectly access objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s arguments, including this.

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.

4 [ Note: This means, for example, that implementations can’t use a static object for internal purposes without synchronization because it could cause a data race even in programs that do not explicitly share objects between threads. —end note ]

5 A C++ standard library function shall not access objects indirectly accessible via its arguments or via elements of its container arguments except by invoking functions required by its specification on those container elements.

6 Operations on iterators obtained by calling a standard library container or string member function may access the underlying container, but shall not modify it. [ Note: In particular, container operations that invalidate iterators conflict with operations on iterators associated with that container. —end note ]

7 Implementations may share their own internal objects between threads if the objects are not visible to users and are protected against data races.

8 Unless otherwise specified, C++ standard library functions shall perform all operations solely within the current thread if those operations have effects that are visible (1.10) to users.

9 [ Note: This allows implementations to parallelize operations if there are no visible side effects. —end note ]

Say you pass a lambda closure to std::function - via, e.g., the constructor or assignment operator - and then invoke that function's operator(). By paragraph 1, operator () is allowed to access the closure object "directly or indirectly via the function’s arguments, including this." By paragraph 2, it may not change the state of the std::function object itself or the closure object since they are both "accessed directly or indirectly via the function’s non-const arguments, including this." This behavior is easily achieved without any kind of protection against simultaneous thread accesses, i.e., locking.

operator () then invokes the lambda closure's operator(), and the rules change: your lambda's operator () is NOT a standard library function, and hence not subject to the rules that specify the behavior of standard library functions. You can do whatever you like to the closure object subject to the rules of the language.

The Standard Library guarantees that it won't introduce any data races through its actions, but you are responsible for any data races that might be introduced by your code.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top