C++11 vector argument to thread appears uninitialized

Viewed 156

I am trying to create a proof of concept for inter-thread communication by meanings of shared state: the main thread creates worker threads giving each a separate vector by reference, lets each do its work and fill its vector with results, and finally collects the results.

However, weird things are happening for which I can't find an explanation other than some race between the initialization of the vectors and the launch of the worker threads. Here is the code.

#include <iostream>
#include <vector>
#include <thread>


class Case {
public:
    int val;
    Case(int i):val(i) {}
};

void
run_thread (std::vector<Case*> &case_list, int idx)
{
    std::cout << "size in thread " << idx <<": " << case_list.size() << '\n';
    for (int i=0; i<10; i++) {
        case_list.push_back(new Case(i));
    }
}

int
main(int argc, char **argv)
{
    int nthrd = 3;
    std::vector<std::thread> threads;
    std::vector<std::vector<Case*>> case_lists;

    for (int i=0; i<nthrd; i++) {
        case_lists.push_back(std::vector<Case*>());
        std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
        threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
    }

    std::cout << "All threads lauched.\n";

    for (int i=0; i<nthrd; i++) {
        threads[i].join();
        for (const auto cp:case_lists[i]) {
            std::cout << cp->val << '\n';
        }
    }
    return 0;
}

Tested on repl.it (gcc 4.6.3), the program gives the following result:

size of 0 in main:0
size of 1 in main:0
size of 2 in main:0
All threads lauched.
size in thread 0: 18446744073705569740
size in thread 2: 0
size in thread 1: 0
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
exit status -1 

On my computer, besides something like the above, I also get:

Segmentation fault (core dumped)

It appears thread 0 is getting a vector that hasn't been initialized, although the vector appears properly initialized in main.

To isolate the problem, I have tried going single threaded by changing the line:

threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );

to

run_thread(case_lists[i], i);

and commenting out:

threads[i].join();

Now the program runs as expected, with the "threads" running one after another before the main collects the results.

My question is: what is wrong with the multi-threaded version above?

1 Answers

References (and iterators) for a vector are invalidated any time the capacity of the vector changes. The exact rules for overallocation vary by implementation, but odds are, you've got at least one capacity change between the first push_back and the last, and all the references made before that final capacity increase are garbage the moment it occurs, invoking undefined behavior.

Either reserve your total vector size up front (so push_backs don't cause capacity increases), initialize the whole vector to the final size up front (so no resizes occur at all), or have one loop populate completely, then launch the threads (so all resizes occur before you extract any references). The simplest fix here would be to initialize it to the final size, changing:

std::vector<std::vector<Case*>> case_lists;

for (int i=0; i<nthrd; i++) {
    case_lists.push_back(std::vector<Case*>());
    std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
    threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
}

to:

std::vector<std::vector<Case*>> case_lists(nthrd);  // Default initialize nthrd elements up front

for (int i=0; i<nthrd; i++) {
    // No push_back needed
    std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
    threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
}

You might be thinking that vectors would overallocate fairly aggressively, but at least on many popular compilers, this is not the case; both gcc and clang follow a strict doubling pattern, so the first three insertions reallocate every time (capacity goes from 1, to 2, to 4); the reference to the first element is invalidated by the insertion of the second, and the reference to the second is invalidated by the insertion of the third.

Related