Подтвердить что ты не робот

Асинхронный конструктор в С++ 11

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

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

Ниже приведен пример кода:

class C
{
public:
    // Standard ctor
    C()
    {
        init();
    }

    // Designed for async ctor
    C(std::function<void(void)> callback)
    {
        init();
        callback();
    }

private:
    void init() // Should be replaced by delegating costructor (not yet supported by my compiler)
    {
        std::chrono::seconds s(2);
        std::this_thread::sleep_for(s);
        std::cout << "Object created" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    auto msgQueue = std::queue<char>();
    std::mutex m;
    std::condition_variable cv;
    auto notified = false;

    // Some parallel task
    auto f = []()
    {
        return 42;
    };

    // Callback to be called when the ctor ends
    auto callback = [&m,&cv,&notified,&msgQueue]()
    {
        std::cout << "The object you were waiting for is now available" << std::endl;
        // Notify that the ctor has ended
        std::unique_lock<std::mutex> _(m);
        msgQueue.push('x');
        notified = true;
        cv.notify_one();
    };

    // Start first task
    auto ans = std::async(std::launch::async, f);

    // Start second task (ctor)
    std::async(std::launch::async, [&callback](){ auto c = C(callback); });

    std::cout << "The answer is " << ans.get() << std::endl;

    // Mimic typical UI message queue
    auto done = false;
    while(!done)
    {
        std::unique_lock<std::mutex> lock(m);
        while(!notified)
        {
            cv.wait(lock);
        }
        while(!msgQueue.empty())
        {
            auto msg = msgQueue.front();
            msgQueue.pop();

            if(msg == 'x')
            {
                done = true;
            }
        }
    }

    std::cout << "Press a key to exit..." << std::endl;
    getchar();

    return 0;
}

Вы видите недостаток в этом проекте? Или вы знаете, есть ли лучший подход?

ИЗМЕНИТЬ

Следуя подсказкам ответа JoergB, я попытался написать factory, который несет ответственность за создание объекта синхронно или асинхронно:

template <typename T, typename... Args>
class FutureFactory
{
public:
    typedef std::unique_ptr<T> pT;
    typedef std::future<pT> future_pT;
    typedef std::function<void(pT)> callback_pT;

public:
    static pT create_sync(Args... params)
    {
        return pT(new T(params...));
    }

    static future_pT create_async_byFuture(Args... params)
    {
        return std::async(std::launch::async, &FutureFactory<T, Args...>::create_sync, params...);
    }

    static void create_async_byCallback(callback_pT cb, Args... params)
    {
        std::async(std::launch::async, &FutureFactory<T, Args...>::manage_async_byCallback, cb, params...);
    }

private:
    FutureFactory(){}

    static void manage_async_byCallback(callback_pT cb, Args... params)
    {
        auto ptr = FutureFactory<T, Args...>::create_sync(params...);
        cb(std::move(ptr));
    }
};
4b9b3361

Ответ 1

Ваш дизайн кажется очень назойливым. Я не вижу причин, по которым класс должен был бы знать обратный вызов.

Что-то вроде:

future<unique_ptr<C>> constructedObject = async(launchopt, [&callback]() {
      unique_ptr<C> obj(new C());
      callback();
      return C;
})

или просто

future<unique_ptr<C>> constructedObject = async(launchopt, [&cv]() {
      unique_ptr<C> ptr(new C());
      cv.notify_all(); // or _one();
      return ptr;
})

или просто (без будущего, но обратный вызов принимает аргумент):

async(launchopt, [&callback]() {
      unique_ptr<C> ptr(new C());
      callback(ptr);
})

должен делать то же самое, не так ли? Они также обеспечивают, чтобы обратный вызов вызывался только когда строился полный объект (при получении из C).

Не должно быть слишком много усилий, чтобы сделать любой из них общим шаблоном async_construct.

Ответ 2

Инкапсулируйте свою проблему. Не думайте о асинхронных конструкторах, просто асинхронных методах, которые инкапсулируют создание вашего объекта.

Ответ 3

Похоже, вы должны использовать std::future вместо того, чтобы создавать очередь сообщений. std::future - это шаблонный класс, который содержит значение и может извлекать блокировку значений, тайм-аут или опрос:

std::future<int> fut = ans;
fut.wait();
auto result = fut.get();

Ответ 4

Я предлагаю взломать использование потока и обработчика сигналов.

1) Создайте поток для выполнения задачи конструктора. Позволяет называть его дочерним потоком. Этот поток будет инициализировать значения в вашем классе.

2) После завершения конструктора дочерний поток использует системный вызов kill для отправки сигнала в родительский поток. (Подсказка: SIGUSR1). Основной поток при получении вызова обработчика ASYNCHRONOUS будет знать, что был создан требуемый объект.

Конечно, вы можете использовать поля типа object-id для различения нескольких объектов в создании.

Ответ 5

Мой совет...

Подумайте о том, зачем вам нужно делать такую ​​длинную операцию в конструкторе.

Я часто нахожу, что лучше разделить создание объекта на три части.

a) распределение б) строительство c) инициализация

Для небольших объектов имеет смысл делать все три в одной "новой" операции. Тем не менее, объекты с тяжелым весом, вы действительно хотите отделить этапы. Выясните, сколько ресурсов вам нужно и выделите. Постройте объект в памяти в действительное, но пустое состояние.

Затем выполните свою длинную операцию загрузки в уже действительный, но пустой объект.

Я думаю, что давным-давно я получил эту картину от чтения книги (Скотт Майерс, возможно?), но я очень рекомендую ее, она решает всевозможные проблемы. Например, если ваш объект является графическим объектом, вы определяете, сколько памяти ему нужно. Если это не удается, покажите пользователю ошибку как можно скорее. Если не отметить объект как еще не прочитанный. Затем вы можете показать его на экране, пользователь может также манипулировать им и т.д. Инициализируйте объект с асинхронной загрузкой файла, когда он будет завершен, установите флаг в объекте, который говорит "загружен". Когда ваша функция обновления увидит, что она загружена, она может нарисовать графику.

Он также ДЕЙСТВИТЕЛЬНО помогает с такими проблемами, как порядок построения, где объект A нуждается в объекте B. Вы вдруг обнаружите, что вам нужно сделать A до B, о нет! Простой, сделайте пустой B и передайте его как ссылку, если A достаточно умный, чтобы знать, что be пусто, и ждать, пока он не будет использоваться, все хорошо.

И... Не забывая.. Вы можете сделать обратное при уничтожении. Сначала пометьте свой объект как пустой, поэтому ничего нового не использует (деинициализация) Освободите ресурсы, (уничтожение) Затем освободите память (освобождение)

Используются те же преимущества.

Ответ 6

Частично инициализированные объекты могут привести к ошибкам или ненужно сложному коду, поскольку вам нужно будет проверить, инициализированы ли они или нет.

Я бы рекомендовал использовать отдельные потоки для пользовательского интерфейса и обработки, а затем использовать очереди сообщений для связи между потоками. Оставьте поток пользовательского интерфейса для работы с пользовательским интерфейсом, который будет более гибким все время.

Поместите сообщение с запросом на создание объекта в очередь ожидания рабочего потока, а затем после создания объекта рабочий может поместить сообщение в очередь UI, указав, что объект теперь готов.

Ответ 7

Вот еще один образец для рассмотрения. Он использует тот факт, что вызов wait() в будущем < > не делает его недействительным. Итак, до тех пор, пока вы никогда не называете get(), вы в безопасности. Этот шаблонный компромисс заключается в том, что вы накладываете обременительные накладные расходы на вызов wait() при вызове функции-члена.

class C
{
    future<void> ready_;

public:
    C()
    {
        ready_ = async([this]
        {
            this_thread::sleep_for(chrono::seconds(3));
            cout << "I'm ready now." << endl;
        });
    }

    // Every member function must start with ready_.wait(), even the destructor.

    ~C(){ ready_.wait(); }

    void foo()
    {
        ready_.wait();

        cout << __FUNCTION__ << endl;
    }
};

int main()
{
    C c;

    c.foo();

    return 0;
}