Поток C++: обернуть функцию потока в лямбда-выражение

#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 в лямбду.

Эквивалентны ли эти два способа создания потоков?

Я думаю, t1 копирует obj один раз (функция принимает его по значению) и t2 копирует его дважды (лямбда берет копию, а функция снова принимает ее по значению).

mch 29.04.2024 14:37

@mch, в std::thread::thread есть дополнительная копия (в вызывающей ветке), так что я думаю, что в обеих есть по 2 копии.

Caleth 29.04.2024 14:39

Если мы забудем о возможных различных операциях копирования/перемещения, программа будет вести себя одинаково, верно?

Mathieu 29.04.2024 14:43

@Матье, если игнорировать проблемы копирования/перемещения, то да.

wohlstad 29.04.2024 14:49

@wohlstad ХотяthreadFunctionaccepts по значению, я все равно могу захватить по ссылке в лямбде [&], потому что копия создается в лямбде. Имеет смысл?

Mathieu 29.04.2024 14:53

Он по-прежнему будет копироваться, когда функция потока будет вызываться в новом потоке. Но в любом случае вы специально прокомментировали, что забыли о копировании/перемещении. Это важно для вас или нет? Кстати, хороший способ контролировать операции копирования/перемещения — добавить методы для конструкторов копирования и перемещения и поместить в них несколько отпечатков. Вы сразу увидите, что вызывается в каждом варианте. Смотрите демо .

wohlstad 29.04.2024 14:56

@wohlstad Да. Я заметил, что std::thread t1(threadFunction, obj)вызовет конструктор копирования+перемещения, тогда как std::thread t2([&]() { threadFunction(obj); })вызовет только конструктор копирования. Поэтому я предпочитаю последний подход.

Mathieu 29.04.2024 15:02

Захват ссылки и копирование ее внутри лямбды имеет состояние гонки: если поток создан и вызывает лямбду, которая затем создаст копию, вполне возможно, что объект уже был уничтожен в основном потоке, когда (или во время) копия сделана. Это НЕ происходит в вашем примере, потому что вы присоединяетесь, но это может быть актуально для более сложного кода, где ваш основной объект фактически продолжит делать другие вещи.

Christian Stieber 29.04.2024 15:11

@ChristianStieber, это хороший момент. Добавил примечание к моему ответу об этом.

wohlstad 29.04.2024 15:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Единственная разница между альтернативами связана с копированием или перемещением объекта 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 еще жив.

Другие вопросы по теме