2013-02-04 141 views
20

有时我需要创建构造函数花费很长时间执行的对象。 这会导致UI应用程序中的响应问题。C++中的异步构造函数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的答案的提示,我试着写一个工厂,将承担相应的责任在同步或异步的方式创建一个对象:

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)); 
    } 
}; 
+0

你是否尝试过在构造函数中使用std :: async。我想你可以将异步放入回调中,并将结果存储为类本身的成员。 – thang

+0

@thang我想尝试一下......对我而言,问题在于你冒着创建对象的风险,但尚未准备好使用。一个isValid()方法可以帮助在这种情况下,也许... – Cristiano

+0

是的,你可以添加isValid或waitValid或者那个效果。这样,所有东西都被封装进类中......相同的功能,只是一个小整齐。 – thang

回答

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模板应该不会太费事。

+0

那么,您需要一些同步来通知您的UI线程该对象已准备就绪。最后的解决方案将所有的责任留给回调。这可能甚至允许无锁信令。其他方法将结果传送到“未来”并将其从信令中分离出来。当然,信令和线程出口之间还有一段距离,这就使未来成为可能。但是,在回调中使用锁有类似的效果:主线程可能会阻塞该锁。 – JoergB

9

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

+0

所以你建议像异步工厂? – Cristiano

+2

@Cristiano这是一个选项。其实我只是说我不喜欢将异步性绑定到本地构造函数本身。 –

+4

其次。如果你创建了一个对象,那么正常的缓冲就是它在构造函数返回时完全构造 - 而不是别的。要么构造函数抛出,要么你有一个有效的对象,不要猜测。另一方面,创建构建对象的异步任务(从其视角同步)没有任何问题。 – Damon

4

看起来你应该使用std::future而不是构建一个消息队列。 std::future是一个模板类,它包含一个值和可以检索值阻塞,超时或轮询:

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

消息队列在这里只是噪声......只是为了模仿一个典型的UI消息循环。 – Cristiano

+0

这是很好的,除非如果你有几个对象挂起创建......等待会阻止。 – thang

+0

@thang是的,这是'std :: future'的缺陷。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3428.pdf提出'when_any';很多期货图书馆都有类似的东西,或者你可以把它们从原始图表(比如@ Cristiano的条件变量)中组合出来。 – ecatmur

4

我会建议使用线程和信号处理黑客攻击。

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

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

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

+2

在我看来,除了信号之外,这正是我的示例代码所做的。 – Cristiano

2

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

我建议使用单独的线程进行UI和处理,然后使用消息队列在线程之间进行通信。留下用户界面线程来处理用户界面,这将随时响应。

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

+0

你说得对。这就是为什么我想在一个单独的线程中创建对象的原因。这正是我想要做的,除非我没有“显式”线程,因为我正在使用std :: async工具。 – Cristiano

+0

很明显,你可以从用std :: async启动的异步ctor插入一条消息到UI消息队列中,所以它更多的是你是否想要控制事物的问题。例如,开发两个异步处理器:你们是不是要同时运行(两个线程),还是一个接一个地运行?你不需要检查std :: future。 –

4

我的建议......

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

我常常觉得这是更好地对象的创作分为三个部分

一)分配 二)建设 C)初始化

对于小物体是情理之中的事在所有三个一个“新”操作。然而,重量级的物体,你真的想分开的阶段。找出你需要多少资源并分配它。将内存中的对象构建为有效但空的状态。

然后......将您的长时间加载操作放入已经有效但空的对象中。

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

它也真的帮助像施工顺序,对象A需要对象B的问题。你突然发现你需要在B之前做A,哦,不!简单来说,做一个空B,并将其作为参考传递给它,只要A足够聪明以知道它是空的,并且在它使用它之前等待它不是,一切都很好。

而...不要忘记..你可以在破坏时做相反的事情。 标记您的对象作为空第一,所以没有什么新的使用它(去初始化) 免费的资源,(破坏) 然后释放内存(释放)

同益适用。

+0

这是一个明智的建议,感谢分享! – Cristiano

0

这是另一种考虑的模式。它利用了在未来<>上调用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; 
} 
+0

我觉得这个解决方案有点吓人,因为如果我忘记在每个方法的开始处等待,就会发生不好的事情。此外,这会导致调用者在调用第一个方法时阻塞,并在调用其他任何方法时引入不必要的开销。 – Cristiano