2013-02-04 84 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