Concepts / Data Races and Shared Data

Data Races and Shared Data

The data is not copied, so nothing is lost, and nothing can go wrong.

  • Programming

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
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()

Definition

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.

cpp
Notes on the code
  • 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.

mainvec2F objectworker thread1. creates vec22. creates F with reference to vec23. starts thread with F4. calls operator()5. reads and writes through member
In what order do main's creation of the function object, the thread start, and the later operator() call happen, and which vector does operator() work on?

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.

mainworker threadvec2one vector objectF member vreference to vec2ownsruns operator()refers to
When a reference is passed to a thread, how do two tasks end up reading and writing the same variable?

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.

vec2 (one sharedobject)main's vec2worker reads throughreferenceworker's own copy
How does the same program differ when the thread receives its own copy of the variable instead of a reference to the original?

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.

cpp
Output
0
Notes on the code
  • 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 fails

    Not 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 instead

    Treat 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 fails

    A reference leads back to the original object, so the worker's view changes whenever that object changes.

    Do this instead

    Only 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 fails

    The source states that the cure is not a cleverer reference but a different decision about what gets passed.

    Do this instead

    Change 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 fails

    The 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 instead

    Verify the rest of the program does not touch the shared vector during the thread's work.

Checking Your Understanding

MEDIUM

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

  1. 1
    A function object is a struct or class that works like a function because it overloads operator(), so calling obj(args) runs the code defined inside that operator.
  2. 2
    A function object can store a reference to a variable in a member, letting it carry data into a separate thread.
  3. 3
    Passing 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.
  4. 4
    Passing 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 like obj(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.

Related concepts

More concepts starting with D