20

有时我需要创建其构造函数需要很长时间才能执行的对象。这会导致 UI 应用程序中的响应性问题。

所以我想知道编写一个设计为异步调用的构造函数是否明智,方法是向它传递一个回调,它会在对象可用时提醒我。

下面是一个示例代码:

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 的回答的提示,我尝试编写一个工厂,负责以同步或异步方式创建对象:

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));
    }
};
4

7 回答 7

17

您的设计似乎非常具有侵入性。我看不出类必须知道回调的原因。

就像是:

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 模板不应该花费太多精力。

于 2013-02-04T11:37:46.173 回答
9

封装你的问题。不要考虑异步构造函数,只考虑封装对象创建的异步方法。

于 2013-02-04T11:09:14.620 回答
4

看起来您应该使用std::future而不是构建消息队列。 std::future是一个模板类,它保存一个值并且可以检索值阻塞、超时或轮询:

std::future<int> fut = ans;
fut.wait();
auto result = fut.get();
于 2013-02-04T11:15:00.877 回答
4

我将建议使用线程和信号处理程序进行破解。

1) 产生一个线程来完成构造函数的任务。让我们称之为子线程。该线程将初始化您类中的值。

2)构造函数完成后,子线程使用kill系统调用向父线程发送信号。(提示:SIGUSR1)。接收到 ASYNCHRONOUS 处理程序调用的主线程将知道已创建所需的对象。

当然,您可以使用像 object-id 这样的字段来区分创建中的多个对象。

于 2013-02-04T11:19:28.263 回答
3

我的建议...

仔细想想为什么你需要在构造函数中做这么长的操作。

我发现通常最好将对象的创建分成三个部分

a) 分配 b) 构造 c) 初始化

对于小物体,在一个“新”操作中完成所有三个操作是有意义的。但是,重量级的物体,你真的要分开舞台。弄清楚你需要多少资源并分配它。将内存中的对象构造为有效但为空的状态。

然后...对已经有效但为空的对象执行长时间加载操作。

我想我很久以前从一本书中得到了这种模式(也许是斯科特迈尔斯?)但我强烈推荐它,它解决了各种各样的问题。例如,如果您的对象是一个图形对象,您可以计算出它需要多少内存。如果失败,请尽快向用户显示错误。如果未将对象标记为尚未读取。然后您可以在屏幕上显示它,用户也可以对其进行操作等。使用异步文件加载初始化对象,当它完成时,在对象中设置一个标记为“已加载”的标志。当你的更新函数看到它被加载时,它可以绘制图形。

它也确实有助于解决诸如构造顺序之类的问题,其中对象 A 需要对象 B。你突然发现你需要在 B 之前制作 A,哦不!很简单,做一个空的 B,并将其作为引用传递,只要 A 足够聪明,知道 be 是空的,并在使用它之前等待它不是,一切都很好。

而且......不要忘记......你可以在破坏上做相反的事情。首先将您的对象标记为空,因此没有新的使用它(反初始化)释放资源,(破坏)然后释放内存(释放)

同样的好处也适用。

于 2013-02-04T17:39:11.613 回答
2

部分初始化的对象可能会导致错误或不必要的复杂代码,因为您必须检查它们是否已初始化。

我建议使用单独的线程进行 UI 和处理,然后使用消息队列在线程之间进行通信。让 UI 线程只处理 UI,这样它就会一直响应更快。

将请求创建对象的消息放入工作线程等待的队列中,然后在创建对象后,工作人员可以将消息放入 UI 队列中,指示对象现在已准备好。

于 2013-02-04T11:24:57.653 回答
0

这是另一种可供考虑的模式。它利用了这样一个事实,即在 future<> 上调用 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;
}
于 2013-02-05T23:39:13.927 回答