Starting and Joining Threads
A thread does not care whether its work was written as a plain function or as a function object. What matters is that the first constructor argument resolves to something the thread can execute, and that the remaining arguments carry the data the work needs.
Three Decisions Hidden in a Thread Launch
A small threaded program usually does three things: it creates a thread object, it hands that object some arguments, and it waits for the thread to finish. The program in this lesson starts two threads and then waits for both of them. Before reading any explanation, follow the trace of that program and notice what work each thread is responsible for. You do not need to memorize every line. You need to notice where the program decides what each thread will do.
Keep one question in mind as you read. When a thread is created, where does the data it works on come from? Does the thread get the original object, or only a copy of its value?
What do you think happens?
A main function creates a thread and passes it a vector named some_vec as a plain argument. When the thread changes the vector, what does main see?
Reveal answer
The thread works on its own separate copy, so main's vector is unchanged
By default the variadic template treats an argument as a value. The thread therefore receives its own version of the object. To make the thread work on the original, the argument must be wrapped with ref(), which is covered later in this article.
Reading the Braced Argument List
The thread constructor is a variadic template. In plain terms, this means the constructor does not fix how many arguments it accepts. It can take 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. The remaining elements are the inputs that will be supplied to it. The constructor does not need to know in advance how many inputs there will be, because the variadic design accepts whatever sequence is written.
The source states the key rule plainly: the first argument names the work to run, and the compiler builds the function object the thread will execute. Everything after that first argument is handed along as input for that work. Reading the list from left to right is the most reliable way to predict what a thread will receive.
Passing the Original Object 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 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 itself calls this wrapper unfortunate. It is needed because 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 share the object.
Trace 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. The diagram shows only the path of this one argument.
- The functional header provides std::ref, the standard-library form of the
ref()wrapper described in the source. - The parameter of f is a reference to a vector of doubles, so f works on the object main created.
- The braced list reads in order: f first, then the wrapped vector as its input.
Function Versus Function Object
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 the thread is constructed from an object like this, the thread runs that object as if it were called. In other words, the thread executes the operator defined in F on the stored vector.
The source makes an important comparison. If F's operator 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.
A thread does not care whether its work was written as a plain function or as a function object. What matters is that the first constructor argument resolves to something the thread can execute, and that the remaining arguments carry the data the work needs.
Finding Where a Result or Mistake Appears
The threaded program in this lesson has three points where it 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. A result produced by a worker thread becomes reliable for main only after main has waited for that thread. Reading the timeline of both threads tells you where to look first when something is wrong.
When you trace a program like this, start from the thread creation lines and check two things: what the first element of each braced list is, and whether each argument that should be shared is wrapped with ref(). Then move to the point where main waits for the threads. A wrong value seen in main after that wait points back to the work the thread performed. A wrong argument decision points back to the creation line.
Mistakes Beginners Make With Thread Arguments
Common mistake Assuming the thread shares the caller's object by default
What it looks likestd::thread t1{f, some_vec}; where f changes its vector parameter
Why it failsThe variadic template treats an argument as a value by default, so the thread receives its own version.
Do this insteadWrap the argument with ref() (std::ref in the standard library) when the thread must work on the original object.
Common mistake Reading the braced list in the wrong order
Why it failsThe first element is the work to run. Treating a data argument as the function leads to a thread that cannot run as intended.
Do this insteadRead the list from left to right: first the callable, then the inputs supplied to it.
Common mistake Believing a function object and a function behave differently inside a thread
Why it failsThe source states that a function and a function object performing the same algorithm are roughly equivalent as thread work.
Do this insteadChoose between them based on how the code is organized, not on what the thread does.
Common mistake Reading main's results before the wait point, when the worker thread may not have finished yet
Practice Tracing a Thread Launch
A program passes a vector to a thread as a plain argument, and the thread appends a value to its parameter. After the thread has been joined, main prints the size of its own vector and sees no change. Explain why, and say which line you would edit to fix the behavior.
Hints
- Ask whether the constructor received the original object or its own version of it.
- Look at how the argument is written inside the braces at the thread creation line.
Key Takeaways
- The thread constructor is a variadic template: it accepts an arbitrary sequence of arguments, and the first one names the work the thread runs.
- By default an argument is treated as a value; wrapping it with
ref()(std::ref in the standard library) lets the thread work on the original object. - A plain function and a function object that performs the same algorithm are roughly equivalent as thread work, and the compiler builds a function object in both cases.
- To locate a result or mistake, trace the thread creation lines for the arguments and the point where main waits for the threads to finish.