Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » std::thread and std::vector confusion?

This thread is locked; no one can reply to it. rss feed Print
 1   2 
std::thread and std::vector confusion?
Chris Katko
Member #1,881
January 2002
avatar

#SelectExpand
1 2void task1(std::string msg) 3{ 4 printf("task1 says: %s\n", msg.c_str()); 5} 6 7void main() 8 { 9 std::vector<std::thread> threads; 10 11 for(int i = 0; i < 5; ++i) 12 { 13 std::thread t1(task1, "Hello"); 14 threads.push_back(t1); //does not COMPILE 15 16 threads.push_back(std::thread(task1, "Hello")); //WORKS 17 } 18 printf("Started.\n"); 19 20 for(auto& t : threads) 21 { 22 t.join(); 23 } 24 25 printf("Finished.\n"); 26 }

Any ideas?

Also, I thought you had to "join" the thread to actually run it yet oddly enough they seem to run before join (they print before it prints "Started.") and I also thought join meant "pause main, run task until completion and then continue here." which would also imply they should be after started, and in order.

[edit]

It should be noted I'm using GCC 4.9.2 on Linux which may have faulty support for std::threads. I learned that the damn hard way after their regex was completely broken while I tried to learn it.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs
"Political Correctness is fascism disguised as manners" --George Carlin

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I am making a wild guess here but it may be because the first std::thread is created on the stack and has to be copied but the second thread is a temporary object. It may be using move semantics in the push_back function when there is an rvalue instead of an lvalue.

Chris Katko
Member #1,881
January 2002
avatar

Please see my note, I posted before I fixed it. The first one^H^H^HLINE doesn't compile with a huge template error about allocater.

#SelectExpand
1In file included from /usr/include/x86_64-linux-gnu/c++/4.9/bits/c++allocator.h:33:0, 2 from /usr/include/c++/4.9/bits/allocator.h:46, 3 from /usr/include/c++/4.9/string:41, 4 from main.cpp:4: 5/usr/include/c++/4.9/ext/new_allocator.h: In instantiation of void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = std::thread; _Args = {const std::thread&}; _Tp = std::thread]: 6/usr/include/c++/4.9/bits/alloc_traits.h:253:4: required from static std::_Require<typename std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::type> std::allocator_traits<_Alloc>::_S_construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = std::thread; _Args = {const std::thread&}; _Alloc = std::allocator<std::thread>; std::_Require<typename std::allocator_traits<_Alloc>::__construct_helper<_Tp, _Args>::type> = void] 7/usr/include/c++/4.9/bits/alloc_traits.h:399:57: required from static decltype (_S_construct(__a, __p, (forward<_Args>)(std::allocator_traits::construct::__args)...)) std::allocator_traits<_Alloc>::construct(_Alloc&, _Tp*, _Args&& ...) [with _Tp = std::thread; _Args = {const std::thread&}; _Alloc = std::allocator<std::thread>; decltype (_S_construct(__a, __p, (forward<_Args>)(std::allocator_traits::construct::__args)...)) = <type error>] 8/usr/include/c++/4.9/bits/stl_vector.h:918:34: required from void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::thread; _Alloc = std::allocator<std::thread>; std::vector<_Tp, _Alloc>::value_type = std::thread] 9main.cpp:33:23: required from here 10/usr/include/c++/4.9/ext/new_allocator.h:120:4: error: use of deleted function std::thread::thread(const std::thread&) 11 { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); } 12 ^ 13In file included from main.cpp:5:0: 14/usr/include/c++/4.9/thread:126:5: note: declared here 15 thread(const thread&) = delete; 16 ^

And per my thoughts, this SO post confirms what join does. But I'll look into your comments.

I'm not used to people replying so fast! ;D

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs
"Political Correctness is fascism disguised as manners" --George Carlin

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

SiegeLord
Member #7,827
October 2006
avatar

Yeah, it's what Edgar said. The copy constructor of std::thread is deleted (as it makes no sense to copy a thread, I suppose), so you need to move the variable using std::move. I.e. you'd do threads.push_back(std::move(t1));.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Thomas Fjellstrom
Member #476
June 2000
avatar

I suppose it's helpful that c++11 does that move stuff, but I'd still avoid storing a static std::thread in a container. I generally store non trivial objects in std containers as pointers.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

SiegeLord
Member #7,827
October 2006
avatar

The idiomatic pointer type for this purpose would be std::unique_ptr which also needs to be moved ;). These days I basically never use pointers unless I'm looking for some dynamic polymorphism.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

beoran
Member #12,636
March 2011

Why not use Allegro's threads in stead?
https://www.allegro.cc/manual/5/ALLEGRO_THREAD

Thomas Fjellstrom
Member #476
June 2000
avatar

SiegeLord said:

The idiomatic pointer type for this purpose would be std::unique_ptr which also needs to be moved ;). These days I basically never use pointers unless I'm looking for some dynamic polymorphism.

Eh. I also tend not to mess with std::unique_ptr much either. ;)

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

l j
Member #10,584
January 2009
avatar

It might also be useful to note that moving a variable will destroy the original and should not be used afterwards, much like in Rust, except Rust enforces it and C++ should but doesn't.

Afaik, all std::threads must end before they go out of scope, so you should call thread::join before they get destroyed.

for(int i = 0; i < 5; ++i)
    {
    std::thread t1(task1, "Hello");
    threads.push_back(t1); // Tries to invoke the deleted copy constructor.
        
    threads.push_back(std::thread(task1, "Hello")); // Returns a rvalue.
        // Gets moved implicitly, you might want to use emplace_back to construct the thread directly in the vector and avoid any unnecessary copies.
    std::thread t2(task1, "Hello");
    threads.push_back(std::move(t2)); // Explicitly move t2
        // t2 should be considered 'consumed' and no longer be used.
    }

Took me a little while to figure these new move semantics out, but once you understand them they'll seem very elegant, at least that was my experience.

Kitty Cat
Member #2,815
October 2002
avatar

taron  said:

It might also be useful to note that moving a variable will destroy the original and should not be used afterwards

IIRC, moving a variable puts the original into a cleared/destructible state. The object is destructed at the end of its scope as normal, so it is still valid after a move. What its actual state is, though, is left undefined (you may or may not be able to do anything with it except let it destruct).

--
"Do not meddle in the affairs of cats, for they are subtle and will pee on your computer." -- Bruce Graham

l j
Member #10,584
January 2009
avatar

I realize my use of destroy might have been a little unclear, especially when I've used it with 2 different implied meanings.

But what I meant with the first instance of destroy is that it effectively can end up destroying the contents of the original and leaves it in an unspecified state, not that it calls the destructor, so you aren't supposed to use it in that state. Apparently an object you move from is called an xvalue (expiring value) now.

Chris Katko
Member #1,881
January 2002
avatar

I guess I understand std::move now. std::forward is confusing.

But geez. Everything keeps getting more and more complex. I mean... std::namespace names for actual language semantics now?

I mean, what's next, std::addition? I was going to joke and say std::cast, std::reference, std::pointer or std::const, but those actually exist with slight variations in spelling.

Which means you have to use the std::namespace to use the most basic functionality of C++. I guess that's not automatically a "bad" thing if you're "all in" for C++, but it calls into question the philosophy of "only pay for what you use" if you're including more and more complex parts of the STL just to run a program.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs
"Political Correctness is fascism disguised as manners" --George Carlin

SiegeLord
Member #7,827
October 2006
avatar

Nothing stops you from just doing what std::move does manually:

threads.push_back((std::thread&&)t1);

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Chris Katko
Member #1,881
January 2002
avatar

Okay, I think I finally get what join means.

A std::thread will start execution the moment you create it. However, if you want to pause the calling thread until a child thread is finished, you would call join to ensure execution of the parent doesn't continue until after the child finishes.

That is, if I have asset_manager.load_data() dispatching a bunch of worker tasks representing the loading of game assets, I certainly don't want to have asset_manager.load_data() returning or making decisions until that data has finished loading.

So when I'm playing around with tiny functions, they're very likely to finish before main() even makes a decision about them.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs
"Political Correctness is fascism disguised as manners" --George Carlin

Peter Hull
Member #1,136
March 2001

Apologies if you already know this, but you can put

using namespace std;

just under your #includes to bring everything from std:: into scope (so you can put vector instead of std::vector) (link) - but bear in mind your definitions might clash with something from std::. You can also do it on a name-by-name basis, e.g

using std::vector;
using std::thread;

And apologies again for thread hijack, but when iterating

for(auto& t : threads) 
{  
    t.join();  
}

I'm sure I saw somewhere it should be auto&& but I don't know why or what this means. Any ideas?

Pete

Thomas Fjellstrom
Member #476
June 2000
avatar

auto detects the type from the rhs, and && tells it to be an rvalue reference. Which can let you take a reference of a rvalue, or temporary, it's used in implementing move semantics and such.

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

SiegeLord
Member #7,827
October 2006
avatar

You don't want an auto&& in a for loop unless you're planning on moving out those elements.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

beoran
Member #12,636
March 2011

No really, what kind of an insane, cryptic mess is this all? :o

I really can't see how the C++ way with all those problems is easier than just a C array with malloc, realloc, and pointers to Allegro threads...

SiegeLord
Member #7,827
October 2006
avatar

The only reason C++ needs this nonsense is because it has overloadable assignment operator. For C, you could say it moves by default!

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

beoran
Member #12,636
March 2011

Yeah, I see, now that you mention it... That's why most languages that allow operator overloading still don' t allow the assignment operator to be overloaded. Ruby comes to mind as such a language...

And yes, C assignment copies the data by default, which is the correct thing to do for a low level language. :)

Chris Katko
Member #1,881
January 2002
avatar

Every time I think I'm getting a grasp on modern C++, it opens up a new steaming pile of complexity. :o

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs
"Political Correctness is fascism disguised as manners" --George Carlin

Thomas Fjellstrom
Member #476
June 2000
avatar

I've still not looked deep into move semantics. So far haven't needed it ;)

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Peter Hull
Member #1,136
March 2001

SiegeLord said:

You don't want an auto&& in a for loop unless you're planning on moving out those elements.

I think maybe it moves only if you give it an rvalue, otherwise it collapses `&&` to `&` and just takes a reference. Have a look at
http://en.cppreference.com/w/cpp/language/range-for
and
http://stackoverflow.com/questions/9162808/what-does-auto-do

beoran said:

No really, what kind of an insane, cryptic mess is this all?

Indeed.

If you look into `<memory>` or any of the standard headers it's all very complicated stuff - unfortunately for C++ (IMO) you do need to understand a little of how it works to use it efficiently - you can't just leave it all to the library writers.

I've still not looked deep into move semantics. So far haven't needed it

I mostly use it for things like

MyClass inst;
//... do something with inst ...
myvector.push_back(std::move(inst));

without the move it'll make a copy of inst to put into the vector, then inst itself will be destroyed some time later. If inst is big/complicated that could be expensive.

l j
Member #10,584
January 2009
avatar

I personally don't use auto&& outside of template or generic lambdas.

 1   2 


Go to: