【问题标题】:Async constructor in C++11C++11 中的异步构造函数
【发布时间】:2013-01-19 01:36:17
【问题描述】:

有时我需要创建其构造函数需要很长时间才能执行的对象。 这会导致 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));
    }
};

【问题讨论】:

  • 您是否尝试在构造函数中使用 std::async 。我想您可以将异步放入回调并将结果存储为类本身的成员。
  • @thang 我想尝试一下……对我来说,问题是您可能会创建一个对象但尚未准备好使用。在这种情况下,isValid() 方法可能会有所帮助,也许......
  • 是的,您可以添加 isValid 或 waitValid 或其他类似的东西。这样,所有内容都被封装到类中......功能相同,只是更整洁一些。

标签: c++ asynchronous c++11 constructor


【解决方案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 模板应该不会太费力。

【讨论】:

  • 好吧,您需要一些同步来通知您的 UI 线程对象已准备好。最后一个解决方案将所有责任留给回调。这甚至可以允许无锁信号。其他方法将结果传输到future 并将其与信令分开。当然,信号和线程退出之间存在间隙,这使得future 准备就绪。但是您在回调中使用锁具有类似的效果:主线程有时可能会阻塞该锁。
【解决方案2】:

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

【讨论】:

  • 所以你建议像异步工厂这样的东西?
  • @Cristiano 这是一个选项。我实际上只是说我不喜欢将异步绑定到本机构造函数本身。
  • 第二个。如果你创建一个对象,正常的期望是它在构造函数返回时被完全构造——而不是别的东西。构造函数抛出,或者你有一个有效的对象,没有猜测。另一方面,创建一个构造对象的异步任务并没有错(从它的角度来看是同步的)。
  • @Damon 是的,这就是为什么我想避免在 ctor 内部使用异步调用的原因。对我来说,ctor 应该只设计为允许异步调用它。
【解决方案3】:

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

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

【讨论】:

  • 消息队列在这里只是噪音......只是为了模仿典型的 UI 消息循环。
  • 这很好,但如果您有多个待创建的对象...等待将阻塞。
  • @thang 是的,这是std::future 的不足之处。 open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3428.pdf提议when_any;许多期货库都有类似的东西,或者您可以将它们从原语(如@Cristiano 的条件变量)组合在一起。
【解决方案4】:

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

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

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

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

【讨论】:

  • 在我看来,除了信号,这正是我的示例代码所做的。
【解决方案5】:

我的建议...

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

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

a) 分配 b) 建设 c) 初始化

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

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

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

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

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

同样的好处也适用。

【讨论】:

    【解决方案6】:

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

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

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

    【讨论】:

    • 你是对的。这就是我想在单独的线程中创建对象的原因。这正是我想要做的,除了我没有“显式”线程,因为我使用的是 std::async 工具。
    • 显然您可以从使用 std::async 启动的异步 ctor 将消息插入 UI 消息队列,因此您是否想控制事物更多的是一个问题。例如,启动两个异步 ctor:您不希望它们都并行运行(两个线程),还是一个接一个地运行?而且你不需要检查 std::future。
    【解决方案7】:

    这是另一种可供考虑的模式。它利用了这样一个事实,即在 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;
    }
    

    【讨论】:

    • 我会觉得这个解决方案有点吓人,因为如果我忘记在每个方法开始时等待,就会发生不好的事情。此外,这将导致调用者在调用第一个方法时阻塞,并在调用任何其他后续方法时引入不必要的开销。
    猜你喜欢
    • 2017-04-14
    • 2012-08-05
    • 2016-06-15
    • 1970-01-01
    • 2013-12-27
    • 2014-10-28
    • 2018-06-26
    • 1970-01-01
    • 2018-01-23
    相关资源
    最近更新 更多