Concepts / Returning Results From Threads

Returning Results From Threads

The thread constructor is a variadic template that accepts an arbitrary sequence of arguments.

  • Programming

A Two-Thread Program to Trace

This article starts with a small program that launches two threads and then waits for both of them to finish. Before reading any explanation, follow the execution trace of the program and watch which work each thread is responsible for. You do not need to memorize every line. Watch for three places where the program decides what a thread will do: the moment each thread object is created, the arguments handed over at that moment, and the point where main waits for the threads to finish.

Keep one question in mind while you read the code: when a thread is created, where does the data it works on come from, and does the thread get the original object or only a copy of its value?

cpp
Notes on the code
  • The first thread is built from the function f and the vector wrapped in std::ref
  • The second thread is built from a function object F that stores a reference to vec2
  • join() makes main wait until each worker has finished

What do you think happens?

After both threads finish, what does main print?

  • 1 1

  • 2 2

  • 1 2

Reveal answer
Answer:

2 2

Each worker doubles the vector it was given a reference to. Both some_vec and vec2 started with a first element of 1.0, so after both threads finish, each first element is 2.0. Because both threads work on the original objects rather than copies, main sees the doubled values.

Reading the Constructor's Argument List

The thread constructor is a variadic template. In plain terms, this means the constructor does not fix how many arguments it takes. It can accept an arbitrary sequence of arguments, so one brace-initialized list can contain a callable followed by whatever values that callable needs.

The list in braces is read in order. The first element identifies what the thread should execute, and the remaining elements are the inputs that will be supplied to it. The constructor does not need to know in advance how many inputs exist, because the variadic design accepts whatever sequence is written. The first argument names the work to run, and the compiler builds the function object that the thread will execute.

fthe work to runstd::ref(some_vec)passed on as the parameterof f
Which argument does the thread constructor treat as the work to run, and how do the remaining arguments map onto the parameters of that function?

The diagram shows the path of only one argument. The second thread follows a different path, which the next section explains. Notice that the first element is never handed to the function as an input. It is the callable itself, and everything after it is forwarded as input.

Sharing the Original Vector With ref()

By default the variadic template treats an argument as a value. For a thread that works on a large container, that default is often not what you want when the thread should operate on the original object. The ref() function, which comes from the functional header, tells the template to treat the object as a reference instead.

The source describes this wrapper as unfortunate, because it is needed at all. The constructor cannot otherwise tell that the caller wants the thread to share the object rather than receive its own version of it. Writing ref(some_vec) inside the braces is the explicit signal that the thread should work on the same object that main created.

some_vecvector created in mainparameter v of freference to the samevectorref(some_vec)
When some_vec is passed through ref(), does the thread work on the original object or on a separate copy, and how does that change what main sees afterward?

Follow the vector some_vec through the first thread. It starts as an ordinary object in main. The ref() wrapper marks it for the constructor, and the constructor passes it along as the argument to f. Inside f, the parameter is a reference to a vector of doubles, so the function operates on the same object that main created. This is why main can read the changed values after the thread finishes.

Function Objects as Thread Work

The second thread in the program is constructed from F{vec2} rather than from a plain function name. F is a struct that stores a reference to a vector in a member and defines an application operator. When a thread is constructed from an object like this, the thread runs that object as if it were called, which means the thread executes the operator defined in F on the stored vector.

AspectPlain function fFunction object F{vec2}
What is written in the constructorThe function name fAn object F that stores vec2 and defines an operator
Where the vector comes fromPassed in as an argument, wrapped with ref()Stored as a reference inside the object
What the compiler buildsA function object for the thread to executeA function object for the thread to execute
What the thread ends up doingRuns the algorithm written in fRuns the algorithm written in the operator of F

Two ways to give a thread its work, when both perform the same algorithm

Key point

If the operator of F and the function f perform the same algorithm, then handing either one to a thread is roughly equivalent. In both cases the compiler is building a function object for the thread to execute. The difference lies in how that object is written, not in what the thread ends up doing.

Locating Where a Result Appears

A result produced by a worker thread does not become visible at the moment the worker writes it. In the program above, main creates both threads, the workers run their operations on the shared data, and main then waits at join() for each thread to finish. The values that main reads afterward are the first place where the result is visible to main. If the wrong value appears there, the cause is in the work the thread performed or in what it was given, so that is where you should look first.

maincreates and waitsworker t1runs f on some_vecworker t2runs F on vec2construct with f and ref(some_vec)construct with F{vec2}finishes, join returnsfinishes, join returnsmain reads some_vec and vec2
In what order do the main thread and the worker threads run, and at which step does the wrong value or error first become visible?
Best practice

Read results only after the threads you depend on have finished. In the program, the waiting step is what separates the moment a worker writes from the moment main can rely on what it reads.

Practice Tracing the Argument Path

MEDIUM

Suppose you change the first thread to std::thread t1{f, some_vec}, without std::ref. Trace the path of some_vec through the constructor. Does the worker change the vector that main reads after join(), and which argument rule explains your answer?

Hints
  • Ask whether the constructor treats the argument as a value or as a reference by default.
  • Find the step where the worker's parameter is connected to the object created in main.
  • Compare what main reads after join() with the result from the original program.

Mistakes When Passing Data to Threads

  • Common mistake

    Assuming a worker thread changes the original object without ref()

    What it looks like
    std::thread t1{f, some_vec}; followed by reading some_vec in main after join()
    Why it fails

    By default the variadic template treats an argument as a value, so the thread works on its own version of the object.

    Do this instead

    Wrap the object with ref(some_vec) when the thread should operate on the original.

  • Common mistake

    Forgetting that the first argument is the work, not an input

    Why it fails

    The first element of the braced list identifies what the thread executes. Everything after it is forwarded as input to that callable.

    Do this instead

    Write the callable first, then the values it needs, in the order its parameters expect.

  • Common mistake

    Treating a function object and a function as different kinds of thread work

    Why it fails

    When both perform the same algorithm, the compiler builds a function object for the thread in either case, so the behavior is roughly equivalent.

    Do this instead

    Choose the form that makes the code clearer, such as a plain function for a simple task or a function object when it needs to store a reference to data.

  • Common mistake

    Reading a worker's result before the thread has finished

    Why it fails

    The value may not yet reflect the work the thread performs, so a wrong value appearing early does not mean the algorithm is broken.

    Do this instead

    Wait for the threads to finish, then check the values, so you can tell whether the error is in the work or in the timing.

Key Takeaways

  • The thread constructor is a variadic template: it accepts an arbitrary sequence of arguments, reads the first one as the work to run, and forwards the rest as inputs.
  • By default an argument is treated as a value; ref() marks it so the thread works on the original object.
  • A function and a function object can both serve as thread work, and when they perform the same algorithm they are roughly equivalent from the thread's point of view.
  • Results from a worker thread become reliable for main after the program waits for the threads to finish, so that waiting point is where to look first when a value is wrong.

Related concepts

More concepts starting with R