Как позволить std::thread автоматически удалять объект после выполнения его функции-члена

Я хочу реализовать класс cmmand, который выполняет некоторую работу в другом потоке, и я не хочу, чтобы пользователи удаляли этот объект вручную. Мой класс command выглядит следующим образом:

class Cmd {
 public:
  void excute() {
    std::cout << "thread begins" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));  // do some work
    std::cout << "thread ends" << std::endl;
  }

  void run() {
    // I want std::unique_ptr to delete 'this' after work is done,but does't work
    std::thread td(&Cmd::excute, std::unique_ptr<Cmd>(this));
    td.detach();
  }

  // test if this object is still alive
  void ok() { std::cout << "OK" << std::endl; }
};

Я использую это так:

int main() {
  Cmd *p = new Cmd();
  p->run();

  // waiting for cmd thread ends
  std::this_thread::sleep_for(std::chrono::seconds(3));

  p->ok();  // I thought p was deleted but not

  return 0;
}

Как и в комментариях, объект все еще жив после завершения потока cmd, я хочу знать, как реализовать такую ​​функциональность.

ИЗМЕНИТЬ

пользователи cmd не знают, когда закончится cmd, поэтому текущий вариант использования приведет к UB.

std::unique_ptr<Cmd> up(new Cmd);  // or just Cmd c;
up->run();
// cmd will be deleted after out of scope but cmd::excute may still need it

ЗАКРЫТО

Я ошибся с тестом, на самом деле объект удаляется после завершения потока. Это более понятно со следующим тестом с дополнительной переменной-членом int i.

#include <functional>
#include <iostream>
#include <stack>
#include <thread>

using namespace std;

class Cmd {
 public:
  ~Cmd() { std::cout << "destructor" << std::endl; }

  void excute() {
    std::cout << i << " thread begins" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));  // do some work
    std::cout << i << " thread ends" << std::endl;
  }

  void run() {
    // I want std::unique_ptr to delete 'this' after work is done,but it seems
    // not working
    std::thread td(&Cmd::excute, std::unique_ptr<Cmd>(this));
    td.detach();
  }

  // test if this object is still alive
  void ok() { std::cout << i << " OK" << std::endl; }

  int i;
};

int main() {
  Cmd *p = new Cmd();
  p->i = 10;
  p->run();

  // waiting for cmd thread ends
  std::this_thread::sleep_for(std::chrono::seconds(3));

  p->ok();  // I thought p was deleted but not

  return 0;
}

Выходные данные Flowwing подтвердили, что объект удален.

10 thread begins
10 thread ends
destructor
-572662307 OK

Но, как подсказывают некоторые добрые ребята, это не очень хороший дизайн, избегайте его, как можете.


person maidamai    schedule 24.04.2019    source источник
comment
но не работает ... Давай, расскажи нам кровавые подробности. Что случилось? Вы бы удивились, если бы Cmd *p = new Cmd(); delete p; p->ok() напечатал ваше сообщение?   -  person Wyck    schedule 24.04.2019
comment
Я был бы удивлен, если бы p->ok() напечатал мое сообщение в вашем случае, по крайней мере, это UB.   -  person maidamai    schedule 24.04.2019
comment
Я ожидаю, что thread удалит cmd в моем случае.   -  person maidamai    schedule 24.04.2019
comment
Вы проходите тестирование на UB. Но если у вас есть UB, вы не можете полагаться на свой тест. Вызов функции-члена для удаленного объекта является UB и не требуется для сбоя. ok() не справляется со своей работой.   -  person François Andrieux    schedule 24.04.2019
comment
Крайне необычно, чтобы объект спонтанно лишился права собственности, а затем закончил свое существование. Этот тип легко использовать неправильно, что делает дизайн, которого следует избегать. Одно из основных правил хорошего дизайна программного обеспечения — сделать код простым для правильного использования и трудным для неправильного.   -  person François Andrieux    schedule 24.04.2019
comment
Я предполагаю, что вы попробовали это сейчас, когда я предложил это. Вы видите, что он не может надежно определить, удален ли объект, верно? Сообщение OK по-прежнему будет печататься при удалении объекта. Я бы посоветовал не развивать этот шаблон выстрелил-забыл. Просто используйте интеллектуальный указатель и присоединитесь к потоку перед очисткой. Вполне разумно, чтобы вызывающая сторона была уведомлена о завершении команды. Сколько вы действительно можете сделать, если у вас есть нить в полете? Я сомневаюсь, что вы могли бы безопасно выйти. И если бы вы могли, то, возможно, вам нужен процесс, а не поток.   -  person Wyck    schedule 24.04.2019
comment
Я просто хочу проверить, удален ли cmd, вместо этого я проверю это в деструкторе, спасибо за ваш совет. @ François Andrieux   -  person maidamai    schedule 24.04.2019
comment
@maidamai, плохая идея проверить, удален ли cmd. stackoverflow.com/questions/15730827/ Лучше просто присвоить nullptr переменной cmd после вызова run. Или, что еще лучше, создайте интеллектуальный класс указателя команд, который будет следовать этой политике, чтобы вам не приходилось управлять необработанным указателем Cmd*.   -  person Wyck    schedule 24.04.2019


Ответы (2)


Вместо потока вы можете использовать std::future для сигнализации о состоянии. Затем вы можете либо дождаться завершения задачи, либо полностью игнорировать будущее.

#include <future>
#include <chrono>
#include <mutex>
#include <iostream>

class Cmd {
public:
    std::future<void> run() {
        std::lock_guard<std::mutex> lock(startMutex);
        if (started) {
            throw std::logic_error("already started");
        }
        started = true;

        // Take copies here, so that it doesn't matter if Cmd is destroyed
        int i_ = i;
        return std::async(std::launch::async, [i_]() {
            std::this_thread::sleep_for(std::chrono::seconds(2));
            std::cout << i_ << std::endl;
        });
    }

    int i = 0;

private:
    std::mutex startMutex;
    bool started = false;
};

int main() {
    auto p = std::make_unique<Cmd>();
    p->i = 10;
    auto f = p->run();
    p.reset();

    // Do some other work

    // Wait for the task to finish (or use f.get() if there is no need to
    // do some other work while waiting)
    if (f.valid()) {
        std::future_status operation;
        do {
            // Do some other work

            operation = f.wait_for(std::chrono::milliseconds(1));
        } while (operation != std::future_status::ready);
    }
}
person vll    schedule 24.04.2019

Существует два способа автоматического удаления памяти, созданной динамически.

  1. В клиентском коде (основная функция) вы должны использовать умный указатель, например unique_pointer. , чтобы как только объект вышел из области видимости, он автоматически освободился в деструкторе unique_poiter

  2. Вы можете создать свой собственный умный указатель, это будет своего рода оболочка для класса Cmd . И приходится перегружать несколько операторов. Этот интеллектуальный указатель также будет заботиться о динамически выделяемой памяти в деструкторе. Клиентский код должен использовать этот интеллектуальный указатель при динамическом создании объекта Cmd.

person santosh kumar    schedule 24.04.2019
comment
клиентский код не знает, когда cmd завершится, а автоматические переменные unique_ptror удалят объект преждевременно, что приведет к неопределенному поведению. - person maidamai; 24.04.2019
comment
@maidamai std::move() std::unique_ptr в std::thread или вместо этого используйте std::shared_ptr - person Remy Lebeau; 24.04.2019
comment
@maidamai Какое неопределенное поведение вы получаете, когда объект удаляется? Для функции-члена класса совершенно нормально вызывать delete this при условии, что после этого она не коснется своего указателя this. Это позволяет как бы последнюю политику выключения света. Вы можете удалить объект и продолжить выполнение кода в своем методе. Просто не вызывайте виртуальные методы и не обращайтесь к переменным-членам после удаления this. Вы должны быть в порядке. - person Wyck; 24.04.2019
comment
@Wyck, cmd thread нужно получить доступ к некоторой переменной-члену (я не демонстрирую в примере кода), но весь объект был удален, если вы используете автоматическую переменную unique_ptr, например EDIT часть в моем сообщении - person maidamai; 24.04.2019
comment
@RemyLebeau Кажется, без std::move() объект все равно удален, я ошибаюсь в тесте. - person maidamai; 24.04.2019
comment
@maidamai, вы не передаете unique_ptr в execute(), чтобы поддерживать его жизнь, поэтому он немедленно умирает. Вам нужно добавить параметр unique_ptr к execute(), иначе передача unique_ptr конструктору thread никуда не денется. void excute(unique_ptr p) {...}; void run() { std::thread td(&Cmd::execute, this, std::unique_ptr<Cmd>(this)); td.detach(); } - person Remy Lebeau; 24.04.2019
comment
@maidamai в противном случае сделайте это: void excute() { std::unique_ptr<Cmd> p(this); ...}; void run() { std::thread td(&Cmd::execute, this); td.detach(); } - person Remy Lebeau; 24.04.2019
comment
@RemyLebeau из тестового вывода в последней части моего сообщения, объект не умирает сразу, и я думал, что unique_ptr в std::thread(&Cmd::excute, unique_ptr<this>) является неявным параметром для Cmd::excute, я прав? - person maidamai; 25.04.2019
comment
@maidamai execute() — это нестатический метод, поэтому второй параметр конструктора std::thread устанавливает указатель this для execute(), который должен быть действительным указателем Cmd*. Вот почему я передал std::unique_ptr в 3-м параметре std::thread, чтобы передать его как явный входной параметр в execute(). Я могу ошибаться, и то, что вы сделали, может сработать, я не проверял. Будем надеяться, что std:::thread захватит std::unique_ptr и правильно вызовет execute(). Я думаю, вы переходите на территорию неопределенного поведения. Другие предложенные мной подходы не должны вызывать UB. - person Remy Lebeau; 25.04.2019