Concepts / Starting Threads with Functions

Starting Threads with Functions

A function object is a struct or class that works like a function because it overloads the application operator, operator().

  • Programming

A Belief That Feels Safe

Imagine you have some data and you want a worker thread to process it. A natural first thought goes like this: if I give the worker a reference to my data, the worker sees my data, and that is all there is to it. Nothing is copied, so nothing is lost, and nothing can go wrong. This belief feels reasonable, and it is exactly the belief this article is designed to correct.

What do you think happens?

Before reading on, commit to a guess: if the main thread and a worker thread both reach the same vector, what decides which one sees which value at any given moment?

  • The order in which the lines were written in the source file

  • The timing of the two threads, since nothing orders their accesses

  • Whichever thread was started first always wins

Reveal answer
Answer:

The timing of the two threads, since nothing orders their accesses

When two tasks share one object and nothing fixes which one acts first, the result depends on timing. That is the core of a data race concern, and the rest of this article traces why.

Giving a Struct the Ability to Be Called

The usual way to think about a function is that you give it arguments and it returns a result. A function object reverses the emphasis a little. The object is a value that carries its own data, and calling it runs the code you defined inside its 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 each time.

Definition

A function object is a struct or class that works like a function because it overloads the application operator, operator().

This ability to carry data is what makes function objects useful for threads. The source states that a function object can be used to execute code in a separate thread, and that a function object can store references to variables.

Anatomy of a Function Object

A compact struct shows all the pieces a function object needs. Read it as three jobs: a member that holds a reference, a constructor that stores the reference, and the operator that does the work when the object is called. The short illustrative struct below uses those three jobs. It is a teaching sketch written for this article, not the source program itself.

cpp
Output
vec2 now contains one element, 1
Notes on the code
  • The member v is a reference, so it does not own a copy of the vector
  • The constructor fills the member when the object is created
  • The line obj() works because the struct overloads operator()

The operator() declaration is the part that turns F into something you can call. The body of that operator is defined elsewhere in the source program. What matters is the shape: the constructor fills the member, and the operator uses it. When code writes obj(), the compiler calls the struct's operator(), which then works on the data the object already carries.

Tracing the Two Threads in the Source Program

The source program contains both kinds of task side by side. A regular function f takes a reference to a vector as a parameter. A 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.

Now follow the data through main. Main 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 comments state 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.

mainvec2owned by mainworker threadoperator()uses member referenceF{vec2}temporary function objectcreatesmember refers tohanded to new threadcallsreads and writes through member
Trace how the caller's vector reaches operator() inside the worker thread. Each arrow shows a link, and the final link leads back to the vector main still owns.

Read the diagram as a shared path. The function object inside the worker thread leads back to the same vec2 that main still owns. The diagram shows a single shared object reached by two paths, one from main and one from the function object. Nothing in the picture says which path acts first. That missing ordering information is the core of the data race concern.

When the Reference Is Used Later

The reference is captured when the function object is built, but the data is not read at that moment. The worker reads and writes through the member only when operator() runs inside the thread. Between those two points, main may still be running, and the vector may be changed or otherwise touched. The timeline below lays out that gap.

vec2 createdF holds referencethread startedmain keeps runningmay touch vec2operator() reads vec2same objectthread joinedreference capturedobject handed offparallellater accessjoin
The reference is stored early, but the data is only accessed later, when the thread runs operator(). Any change made in between is visible through the same object.

What do you think happens?

If main changes vec2 after the thread has been started, can the worker's function object see that change?

Reveal answer
Answer:

Yes, the function object refers to the same vec2, so it sees whatever value vec2 holds when the worker reads it

Because the member is a reference, there is no separate copy to protect. Whether the worker sees the old or new contents depends on timing, which is exactly why the shared access is risky.

Why a Shared Reference Creates Data Races

Passing an argument by reference gives a thread access to the original data. 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 it at the same time, the result depends on timing. This is a data race. The source is clear that the cure is not a cleverer reference. The cure is a different decision about what gets passed.

Before

Reference passing: worker holds a reference to vec2 and main also holds vec2

One shared vec2, reached by main and the worker, so accesses can collide

After

Value passing: worker holds its own copy of the vector, separate from main's vec2

Two separate objects, one per task, so the worker does not share main's data

The before and after views make the design choice concrete. 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 this eliminates the risk of data races.

vec2one shared objectmainvec2main's own datamainworker referenceworker copyits own datareads and writesreads and writesownsowns
Left: a reference makes the worker and main touch one object. Right: passing by value gives each task its own data.

Mistakes Learners Make with Thread Arguments

  • Common mistake

    Assuming a reference is harmless because nothing is copied

    Why it fails

    Not copying means the data is shared, and shared data touched by two tasks at once depends on timing.

    Do this instead

    Treat a reference as a shared path to the original object and check who else can reach it.

  • Common mistake

    Believing the fix is a cleverer reference

    Why it fails

    The source says the cure is a different decision about what gets passed, not a different way of writing the reference.

    Do this instead

    Decide whether the task should receive its own version of the data, and pass by value when it should.

  • Common mistake

    Thinking the function object reads the data when it is created

    Why it fails

    The constructor only stores the reference. The data is accessed later, when operator() runs.

    Do this instead

    Separate capture time from use time, and ask what may change between them.

Checking Your Understanding

MEDIUM

A worker thread receives a function object F that holds a reference to a vector called data owned by main. Main keeps modifying data while the worker runs. Explain in your own words why this is a data race risk, and state one change to how the argument is passed that removes the risk.

Hints
  • Ask who else can reach the same object at the same time
  • The source says passing by value gives each task its own data

Key Takeaways

  • A function object is a struct or class that works like a function because it overloads operator(), so an object written as obj(args) calls that operator.
  • A function object can keep a reference to a variable in a member, set by its constructor, and use that reference later when the thread runs operator().
  • Passing by reference gives a thread the original data, which is shared with other tasks and can cause data races when they touch it at the same time.
  • Passing by value gives each task its own data, and the source states that this eliminates the risk of data races.

Related concepts

More concepts starting with S