Перемещение std::thread со ссылками

У меня есть класс, в котором есть такой поток:

// USING C++ 17
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

class MyClass {
    std::thread thread;
    std::string mystring;

public:
    bool done = false;

    MyClass(std::string string) {
        mystring = string;

        thread = std::thread(&MyClass::worker, std::ref(*this));
        thread.detach();
    }
    void worker() {
        // notice how mystring has the wrong value, some of the time
         std::cout << "mystring is: \"" << mystring << "\"" << std::endl;

         std::this_thread::sleep_for(std::chrono::milliseconds(1000));

         done = true;
    }
};

int main() {
    std::vector<MyClass> my_vec {};
    for (int i=0; i<15; i++) {
        my_vec.emplace_back("hello");
    }
    
    while (my_vec.size() > 0) {
        for (auto it = my_vec.begin(); it != my_vec.end();) {
            if (it->done) {
                it = my_vec.erase(it);
            } else {
                ++it;
            }
        }
        
    }

    return 0;
}

Я хочу хранить экземпляры MyClass в файле std::vector. Время от времени я перебираю вектор и удаляю завершенные экземпляры MyClass (done это правда).

Однако когда я emplace_back добавляю новый экземпляр MyClass в свой вектор, у экземпляров MyClass, уже находящихся в векторе, вызывается конструктор перемещения. В этом вызове ссылка worker на this не обновляется, поэтому mystring по-прежнему указывает на старый блок памяти.

Этот цикл является частью цикла событий графического интерфейса, поэтому я хочу иметь возможность использовать этот шаблон. - Есть ли способ это исправить или добиться такой закономерности?

Я попытался перезаписать конструктор перемещения MyClass, используя std::move для перемещения потока. Я также попробовал создать статический метод worker и передать ссылку на соответствующий экземпляр MyClass.

Можете ли вы заранее зарезервировать такой большой vector, чтобы размер никогда не приходилось перераспределять?

Jeffrey 04.07.2024 01:24

Есть несколько способов сделать что-то стабильным по адресу, но наверняка вам следует что-то сделать, чтобы иметь пустые std::thread, а не join их и вообще не иметь синхронизации между этими потоками?

Davis Herring 04.07.2024 01:25

Лучше превратить (готово) хотя бы в std::atomic<bool>!

Jeremy Friesner 04.07.2024 01:46

Когда ваш вектор растет, все ссылки и итераторы на элементы вектора становятся недействительными. Вы должны быть в состоянии справиться с этим. Либо используйте уровень косвенности (std::vector<std::uinique_ptr<Myclass>>), либо выберите контейнер со стабильным адресом.

NathanOliver 04.07.2024 03:12

@DavisHerring разве к треду нельзя присоединиться после отсоединения? такое ощущение, что ОП вообще не нужно отсоединяться

jupiterbjy 04.07.2024 03:45

@jupiterbjy: Именно, и detach — это уже крайняя мера (так как по сути делает невозможным безопасную очистку thread_local переменных и тому подобного).

Davis Herring 04.07.2024 03:56

@jupiterbjy потоки выполняются вместе с графическим интерфейсом, которому необходимо обрабатывать события ОС в основном потоке - поэтому они отключены.

Ethan 04.07.2024 08:40

@DavisHerring эти потоки разрезают большие видеофайлы на кадры, по одному видеофайлу на поток. поэтому я не думаю, что есть огромная необходимость что-либо синхронизировать.

Ethan 04.07.2024 08:43

@JeremyFriesner, возможно, это хорошая идея, но на практике это не должно иметь значения. Одна ветка пишет, другая читает. Не имеет значения, находится ли чтение потока done в состоянии гонки с записью потока done. done изменяется только один раз, поэтому читающий поток done заметит изменение на следующей итерации цикла, что в данном случае совершенно нормально.

Ethan 04.07.2024 09:52

@Ethan Даже если рассматриваемая гонка данных кажется «безобидной», технически это неопределенное поведение, и поэтому его следует избегать. (Компилятор может увидеть, что эта переменная вообще не записана в текущем потоке, и заменить весь цикл на while(false) или while(true) в зависимости от начального значения).

Mestkon 04.07.2024 15:14

@Итан Компьютеры (и компиляторы) в наши дни стали более сложными, поэтому интуиция о том, что «не должно иметь значения», может вводить в заблуждение. Вам может «повезти» в течение многих лет, и все будет работать так, как вы ожидаете, пока однажды вы не запустите на другом процессоре или не скомпилируете на другом компиляторе, и ваша программа внезапно не начнет работать неправильно, поэтому лучше не рисковать. По крайней мере, это приведет к тому, что инструменты обнаружения гонок (такие как Helgrind или ThreadSanitizer) будут выдавать кучу (действительных!) предупреждений, что потенциально сбивает с толку или скрывает другие проблемы с потокобезопасностью и затрудняет их понимание/исправление.

Jeremy Friesner 04.07.2024 15:56
Стоит ли изучать 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
11
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Добавление слоя косвенности (изменение my_vec с std::vector<MyClass> на std::vector<std::unique_ptr<MyClass>>) решило эту проблему благодаря комментарию @NathanOliver.

Я использую my_vec.emplace_back(std::make_unique<MyClass>("hello")); для создания экземпляра MyClass.

В примере, приведенном в задаче, когда в потоке возникает исключение, done никогда не устанавливается в true, и программа предполагает, что поток все еще работает.

Чтобы решить эту проблему, я изменил std::thread на std::future и запустил работника асинхронно с помощью

std::future<int> thread = std::async(
   std::launch::async,
   [this] {
       worker();
       return 0;
    });

Таким образом, я могу проверить, работает ли асинхронная функция, используя ответ, показанный здесь.

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