Data Races and Shared Data
The data is not copied, so nothing is lost, and nothing can go wrong.
Why Giving a Thread Your Data Feels Safe
When you first start working with threads, a natural line of reasoning appears. If I give a worker thread a reference to my data, then the worker sees my data, and that is all there is to it. The data is not copied, so nothing is lost, and nothing can go wrong. The belief feels reasonable. This article is designed to show exactly where it breaks down.
The reference is the reason this approach works at all, and it is also the source of the risk. Once a task holds a reference to a vector, that task and whoever else holds the same vector are looking at one shared object. If two tasks touch that object at the same time, the result depends on timing. That situation is a data race. The cure is not a cleverer reference. The cure is a different decision about what gets passed to the task.
What do you think happens?
A worker thread holds a reference to vec2, and the main thread keeps changing vec2 while the worker runs. What does the worker see?
Reveal answer
It depends on timing. Both paths lead to the same vector, so whether the worker sees the old or the new contents depends on when each access happens relative to the other.
Nothing in the reference itself orders the two accesses. When two tasks touch the same object at the same time, the outcome depends on timing, and that is what makes it a data race.
A Function Object as a Struct with operator()
A function object is a struct or class that works like a function because it overloads the application operator, operator().
The usual way to think about a function is that you give it arguments and it returns a result. A function object shifts the emphasis a little. The object is a value that carries its own data, and calling it runs the code you defined inside operator(). Because the object can hold data between its creation and its call, it can remember things that a plain function would need to receive again every time it runs.
This ability to carry data is what makes function objects useful for threads. A function object can be used to execute code in a separate thread, and it can store references to variables in its members. Those two properties together let the object carry data into a thread.
A complete function object needs three pieces, and it helps to read them as three jobs. The first job is a member that holds a reference to the data. The second job is a constructor that stores that reference into the member. The third job is the operator() that does the work when the object is called. The operator() declaration is the part that turns the struct into something you can call, and its body uses the member that the constructor filled.
- std::vector<int>& v is the member that holds a reference to the data (job one)
- The constructor stores the reference into the member (job two)
operator()is the part that does the work when the object is called (job three)
Tracing vec2 Through the Source Program
Now follow the data through the source program. The main function creates two vectors, some_vec and vec2, and then starts two threads. The first thread runs the regular function f with some_vec passed by reference. The second thread is started with F{vec2}, a temporary function object whose member refers to vec2. The source notes that F(vec2)() executes in a separate thread and that f(some_vec) executes in a separate thread. Each thread is joined back into main before the program finishes.
The source program contains both kinds of task side by side. The regular function f takes a reference to a vector as a parameter. The function object F keeps a reference to a vector as a member. Both can do work on the same data, but they receive that data at different moments and in different ways.
Read the sequence from top to bottom. Main creates vec2 first, then creates the function object with a reference to it, then hands that object to a new thread. When the thread calls operator(), the call reaches vec2 through the member. The value that operator() sees is whatever vec2 contains at the moment it is accessed, and the diagram does not show which step runs first when main also touches vec2. That gap is where the concern lives.
Where the Shared Path Lives
The flow of data is easiest to see as a set of links. Main creates vec2. Main creates the function object F with a reference to vec2. The object is handed to a new thread. Inside that thread, the object reads and writes through its member, which leads back to the same vec2 that main still owns. The links form a shared path, and that shared path is where the risk lives.
The diagram shows a single shared object, vec2, reached by two paths: one from main and one from the function object inside the worker thread. Nothing in the picture says which path acts first. That missing ordering information is the core of the data race concern. If two tasks touch the vector at the same time, the result depends on timing, and that is what a data race is.
Passing by Value Instead
The source treats the choice between reference and value as the main design decision. Passing by reference lets a task use the original data, and it introduces the risk that another task reaches that data at the same time. Passing by value means the task receives its own version of the data. The source states that passing by value gives each task its own data and eliminates the risk of data races.
In the before state, the worker and main both work on one object, so the access can collide. In the after state, each task holds its own data, so there is no shared object for the two tasks to contend over. The program logic may look nearly identical, but the decision about what gets passed has changed which data is shared.
0- The member is a vector, not a reference, so the object owns its own copy
- The constructor receives the vector by value and stores a copy in v
- The call modifies the copy only, so vec2 in main is unchanged
Mistakes Beginners Make with References to Threads
Common mistake Believing that a reference is harmless because nothing is copied
Why it failsNot copying the data means the worker and the owner share one object, so the copy-free design is exactly what creates the shared path.
Do this insteadTreat a reference passed to a thread as shared access and ask whether any other task can touch that object at the same time.
Common mistake Assuming the worker sees a fixed snapshot of the data
Why it failsA reference leads back to the original object, so the worker's view changes whenever that object changes.
Do this insteadOnly assume a stable value if you pass by value or otherwise guarantee that no other task writes to the object.
Common mistake Thinking the fix is a better reference
Why it failsThe source states that the cure is not a cleverer reference but a different decision about what gets passed.
Do this insteadChange the design so each task receives its own version of the data, or otherwise ensure the shared object is not accessed concurrently.
Common mistake Assuming the source program's comment guarantees safety
Why it failsThe source says the function object hopes no other task accesses vec2 while F is executing, which means the guarantee comes from the surrounding program.
Do this insteadVerify the rest of the program does not touch the shared vector during the thread's work.
Checking Your Understanding
A function object F stores a reference to vec2 in its member. Main starts a thread with F{vec2} and then, while the thread is still running, main also pushes values into vec2. Explain why this can be a data race, and state one change to the design that removes the risk.
Hints
- Ask whether the thread and main reach the same vector object.
- Think about whether the thread's member is a copy or a reference to vec2.
- The source says passing by value gives each task its own data.
Summary of Shared Data Risk
- 1A function object is a struct or class that works like a function because it overloads
operator(), so callingobj(args)runs the code defined inside that operator. - 2A function object can store a reference to a variable in a member, letting it carry data into a separate thread.
- 3Passing by reference gives a thread access to the original data, and two tasks touching that data at the same time can cause a data race.
- 4Passing by value gives each task its own data, which the source states eliminates the risk of data races.
At a glance
- A function object is a struct or class that overloads
operator(), so an expression likeobj(args)runs the code defined inside that operator. - A function object can store a reference to a variable in a member, which lets it carry data into a separate thread.
- Passing an argument by reference gives a thread access to the original data, and simultaneous access by another task creates a data race.
- Passing by value gives each task its own data, which the source states eliminates the risk of data races.