Concepts / Starting and Joining Threads

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.

  • Programming

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?

  • The change appears in main's original vector

  • The thread works on its own separate copy, so main's vector is unchanged

Reveal answer
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.

Braced argumentlistFirst element: fnames the work to runFunction objectbuilt by the compilerThread runs f withinputsRemaining elementsinputs for the workread firstread afterbecomesexecuted by threadsupplied as inputs
Where does each argument in the braced list go, and which one is the function the thread runs?

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.

some_vec in mainThread's own copyChange stays in copysome_vec in mainThread shares thesame objectChange lands onoriginalcopied as valuemodifiedwrapped with ref()modified
When the thread modifies its argument, does the change land on the caller's original object or on a separate copy?

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.

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

Key point

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.

main creates t1first argument names fmain creates t2first argument is F objectt1 runs fworks on original via ref()t2 runs F operatorworks on stored vectormain waits for bothjoin pointResults usable inmainnextmain continuesthread startsthread startsafter waiting
In the timeline of the main thread and the worker threads, at which point does the result become available or the error first show up?

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 like

    std::thread t1{f, some_vec}; where f changes its vector parameter

    Why it fails

    The variadic template treats an argument as a value by default, so the thread receives its own version.

    Do this instead

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

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

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

    The source states that a function and a function object performing the same algorithm are roughly equivalent as thread work.

    Do this instead

    Choose 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

MEDIUM

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.

Related concepts

More concepts starting with S