У меня есть класс, в котором есть такой поток:
// 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
.
Есть несколько способов сделать что-то стабильным по адресу, но наверняка вам следует что-то сделать, чтобы иметь пустые std::thread
, а не join
их и вообще не иметь синхронизации между этими потоками?
Лучше превратить (готово) хотя бы в std::atomic<bool>!
Когда ваш вектор растет, все ссылки и итераторы на элементы вектора становятся недействительными. Вы должны быть в состоянии справиться с этим. Либо используйте уровень косвенности (std::vector<std::uinique_ptr<Myclass>>
), либо выберите контейнер со стабильным адресом.
@DavisHerring разве к треду нельзя присоединиться после отсоединения? такое ощущение, что ОП вообще не нужно отсоединяться
@jupiterbjy: Именно, и detach
— это уже крайняя мера (так как по сути делает невозможным безопасную очистку thread_local
переменных и тому подобного).
@jupiterbjy потоки выполняются вместе с графическим интерфейсом, которому необходимо обрабатывать события ОС в основном потоке - поэтому они отключены.
@DavisHerring эти потоки разрезают большие видеофайлы на кадры, по одному видеофайлу на поток. поэтому я не думаю, что есть огромная необходимость что-либо синхронизировать.
@JeremyFriesner, возможно, это хорошая идея, но на практике это не должно иметь значения. Одна ветка пишет, другая читает. Не имеет значения, находится ли чтение потока done
в состоянии гонки с записью потока done
. done
изменяется только один раз, поэтому читающий поток done
заметит изменение на следующей итерации цикла, что в данном случае совершенно нормально.
@Ethan Даже если рассматриваемая гонка данных кажется «безобидной», технически это неопределенное поведение, и поэтому его следует избегать. (Компилятор может увидеть, что эта переменная вообще не записана в текущем потоке, и заменить весь цикл на while(false)
или while(true)
в зависимости от начального значения).
@Итан Компьютеры (и компиляторы) в наши дни стали более сложными, поэтому интуиция о том, что «не должно иметь значения», может вводить в заблуждение. Вам может «повезти» в течение многих лет, и все будет работать так, как вы ожидаете, пока однажды вы не запустите на другом процессоре или не скомпилируете на другом компиляторе, и ваша программа внезапно не начнет работать неправильно, поэтому лучше не рисковать. По крайней мере, это приведет к тому, что инструменты обнаружения гонок (такие как Helgrind или ThreadSanitizer) будут выдавать кучу (действительных!) предупреждений, что потенциально сбивает с толку или скрывает другие проблемы с потокобезопасностью и затрудняет их понимание/исправление.
Добавление слоя косвенности (изменение 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;
});
Таким образом, я могу проверить, работает ли асинхронная функция, используя ответ, показанный здесь.
Можете ли вы заранее зарезервировать такой большой
vector
, чтобы размер никогда не приходилось перераспределять?