#include <iostream>
#include <thread>
class MyClass {
public:
MyClass(int val) : val_(val) {}
int val_{0};
};
void threadFunction(MyClass myObj) {
// do something ...
}
int main() {
MyClass obj(42);
std::thread t1(threadFunction, obj);
std::thread t2([=]() {
threadFunction(obj);
});
t1.join();
t2.join();
return 0;
}
В треде t1
я вызывал threadfunction
напрямую, тогда как в треде t2
я помещал вызов threadFunction
в лямбду.
Эквивалентны ли эти два способа создания потоков?
@mch, в std::thread::thread
есть дополнительная копия (в вызывающей ветке), так что я думаю, что в обеих есть по 2 копии.
Если мы забудем о возможных различных операциях копирования/перемещения, программа будет вести себя одинаково, верно?
@Матье, если игнорировать проблемы копирования/перемещения, то да.
@wohlstad ХотяthreadFunction
accepts по значению, я все равно могу захватить по ссылке в лямбде [&]
, потому что копия создается в лямбде. Имеет смысл?
Он по-прежнему будет копироваться, когда функция потока будет вызываться в новом потоке. Но в любом случае вы специально прокомментировали, что забыли о копировании/перемещении. Это важно для вас или нет? Кстати, хороший способ контролировать операции копирования/перемещения — добавить методы для конструкторов копирования и перемещения и поместить в них несколько отпечатков. Вы сразу увидите, что вызывается в каждом варианте. Смотрите демо .
@wohlstad Да. Я заметил, что std::thread t1(threadFunction, obj)
вызовет конструктор копирования+перемещения, тогда как std::thread t2([&]() { threadFunction(obj); })
вызовет только конструктор копирования. Поэтому я предпочитаю последний подход.
Захват ссылки и копирование ее внутри лямбды имеет состояние гонки: если поток создан и вызывает лямбду, которая затем создаст копию, вполне возможно, что объект уже был уничтожен в основном потоке, когда (или во время) копия сделана. Это НЕ происходит в вашем примере, потому что вы присоединяетесь, но это может быть актуально для более сложного кода, где ваш основной объект фактически продолжит делать другие вещи.
@ChristianStieber, это хороший момент. Добавил примечание к моему ответу об этом.
Единственная разница между альтернативами связана с копированием или перемещением объекта MyClass obj
.
Это можно наблюдать, добавив конструкторы копирования и перемещения с отпечатками:
class MyClass {
public:
MyClass(int val) : val_(val) {}
MyClass(MyClass const & other)
{
std::cout << "copy\n";
val_ = other.val_;
}
MyClass(MyClass && other)
{
std::cout << "move\n";
val_ = other.val_;
}
int val_{0};
};
В первом случае конструктор std::thread
создаст копию obj
, а затем переместит ее, когда функция будет вызвана новым потоком.
См. demo1, с выводом:
copy
move
Во втором случае появится дополнительная копия, когда лямбда выполнится и вызовет threadFunction
.
См. demo2, с выводом:
copy
move
copy
Третья альтернатива — использование лямбды с захватом по ссылке.
Это сократит его до одной копии (когда лямбда вызовет threadFunction
).
См. demo3, с выводом:
copy
Обратите внимание, что этот третий вариант основан на том факте, что obj
не уничтожается во время запуска потока (в противном случае возникнет состояние гонки). В вашем случае это нормально, потому что потоки join
создаются, пока obj
еще жив.
Я думаю,
t1
копируетobj
один раз (функция принимает его по значению) иt2
копирует его дважды (лямбда берет копию, а функция снова принимает ее по значению).