2013-02-04 32 views
20

Thỉnh thoảng tôi cần phải tạo các đối tượng có các nhà xây dựng mất nhiều thời gian để thực thi. Điều này dẫn đến các vấn đề về khả năng đáp ứng trong các ứng dụng UI. Vì vậy, tôi đã tự hỏi nếu nó có thể là hợp lý để viết một nhà xây dựng được thiết kế để được gọi là không đồng bộ, bằng cách đi qua một cuộc gọi lại với nó mà sẽ cảnh báo cho tôi khi đối tượng có sẵn.Nhà xây dựng không đồng bộ trong C++ 11

Dưới đây là một số mẫu mã:

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

Bạn có thấy bất kỳ nhược điểm trong thiết kế này? Hoặc bạn có biết nếu có một cách tiếp cận tốt hơn?

EDIT

Tiếp theo gợi ý của câu trả lời JoergB, tôi đã cố gắng để viết một nhà máy này sẽ chịu trách nhiệm để tạo ra một đối tượng trong một sync hoặc async cách:

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

bạn đã thử dùng std :: async bên trong hàm tạo. tôi tưởng tượng bạn có thể chỉ cần đặt async vào callback và lưu trữ kết quả là một thành viên của chính lớp đó. – thang

+0

@thang Tôi muốn thử nó ... vấn đề với tôi là bạn có nguy cơ có một đối tượng được tạo nhưng chưa sẵn sàng sử dụng. Phương thức isValid() có thể giúp ích trong trường hợp này, có lẽ ... – Cristiano

+0

vâng bạn có thể thêm isValid hoặc waitValid hoặc một cái gì đó vào hiệu ứng đó. theo cách đó mọi thứ được đóng gói vào trong lớp ... cùng chức năng, chỉ cần một chút neater. – thang

Trả lời

17

Thiết kế của bạn có vẻ rất xâm nhập. Tôi không thấy lý do tại sao lớp học sẽ phải nhận thức được gọi lại.

Cái gì như:

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

hoặc đơn giản là

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

hoặc chỉ (không có một tương lai nhưng một callback dùng một tham số):

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

nên làm chỉ là tốt, phải không? Chúng cũng đảm bảo rằng callback chỉ được gọi khi một đối tượng hoàn chỉnh được xây dựng (khi bắt nguồn từ C).

Không nên quá nhiều nỗ lực để thực hiện bất kỳ việc nào trong số này thành mẫu async_construct chung.

+0

Vâng, bạn sẽ cần một số đồng bộ hóa để thông báo cho chuỗi giao diện người dùng của bạn rằng đối tượng đã sẵn sàng. Các giải pháp cuối cùng lá tất cả các trách nhiệm để gọi lại. Điều đó có thể cho phép ngay cả tín hiệu không khóa. Các phương thức khác để vận chuyển kết quả đến 'tương lai' và tách biệt kết quả đó khỏi tín hiệu. Tất nhiên có một khoảng cách giữa tín hiệu và lối ra luồng, điều này làm cho 'tương lai' sẵn sàng. Nhưng việc bạn sử dụng khóa trong gọi lại có tác dụng tương tự: có một thời điểm mà chủ đề chính có thể chặn trên khóa đó. – JoergB

9

Encapsulate vấn đề của bạn . Đừng nghĩ về các nhà xây dựng không đồng bộ, chỉ các phương thức không đồng bộ đóng gói việc tạo đối tượng của bạn.

+0

Vì vậy, bạn đề nghị một cái gì đó giống như một nhà máy async? – Cristiano

+2

@Cristiano Đây là một tùy chọn. Tôi đã thực sự chỉ nói rằng tôi không thích sự không đồng bộ được gắn với bản thân nhà xây dựng bản địa. –

+4

Thứ hai này. Nếu bạn tạo một đối tượng, thì cách tiếp cận thông thường là nó được xây dựng đầy đủ khi hàm khởi tạo trả về - không phải cái gì khác. Hoặc là nhà xây dựng ném, hoặc bạn có một đối tượng hợp lệ, không có phỏng đoán. Mặt khác, không có gì sai khi tạo một nhiệm vụ không đồng bộ để xây dựng một đối tượng (đồng bộ, từ quan điểm của nó). – Damon

4

Có vẻ như bạn nên sử dụng std::future thay vì xây dựng hàng đợi thư. std::future là một lớp mẫu chứa một giá trị và có thể lấy giá trị chặn, thời gian chờ hoặc bỏ phiếu:

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

Hàng đợi tin nhắn chỉ là tiếng ồn ở đây ... chỉ để bắt chước một vòng lặp thông báo UI điển hình. – Cristiano

+0

điều này là tốt ngoại trừ nếu bạn có một số đối tượng đang chờ tạo ... chờ đợi sẽ chặn. – thang

+0

@thang có, đây là sự thiếu hụt của 'std :: future'. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3428.pdf đề xuất 'when_any'; nhiều thư viện tương lai có một cái gì đó tương tự hoặc bạn có thể đặt nó với nhau từ nguyên thủy (như biến điều kiện của @ Cristiano). – ecatmur

4

Tôi sẽ đề nghị một hack sử dụng thread và xử lý tín hiệu.

1) Tạo ra một chuỗi để thực hiện nhiệm vụ của hàm tạo. Cho phép gọi nó là chủ đề con. Chủ đề này sẽ intialise các giá trị trong lớp học của bạn.

2) Sau khi hàm tạo được hoàn tất, chuỗi con sử dụng lệnh gọi hệ thống giết để gửi tín hiệu đến chuỗi gốc. (Gợi ý: SIGUSR1). Chuỗi chính khi nhận cuộc gọi xử lý ASYNCHRONOUS sẽ biết rằng đối tượng được yêu cầu đã được tạo.

ofcourse, bạn có thể sử dụng các trường như id đối tượng để phân biệt giữa nhiều đối tượng trong quá trình tạo.

+2

Dường như với tôi, ngoại trừ điều tín hiệu, đây chính xác là mã mẫu của tôi. – Cristiano

2

Có các đối tượng được khởi tạo một phần có thể dẫn đến lỗi hoặc mã phức tạp không cần thiết, vì bạn sẽ phải kiểm tra xem chúng có được khởi tạo hay không.

Tôi khuyên bạn nên sử dụng các luồng riêng biệt cho giao diện người dùng và xử lý, sau đó sử dụng hàng đợi tin nhắn để liên lạc giữa các chuỗi. Rời khỏi chuỗi giao diện người dùng để chỉ xử lý giao diện người dùng, sau đó sẽ phản hồi nhanh hơn mọi lúc.

Đặt thông báo yêu cầu tạo đối tượng vào hàng đợi mà chuỗi công nhân đợi, sau đó sau khi đối tượng đã được tạo, nhân viên có thể đặt thông báo vào hàng đợi UI cho biết đối tượng đã sẵn sàng.

+0

Bạn nói đúng. Đây là lý do tại sao tôi muốn tạo đối tượng trong một chuỗi riêng biệt. Đây là chính xác những gì tôi muốn làm, ngoại trừ việc tôi không có một "rõ ràng" thread, bởi vì tôi đang sử dụng std :: async cơ sở. – Cristiano

+0

Rõ ràng là bạn có thể chèn một tin nhắn vào hàng đợi thông điệp UI từ ctor async được khởi chạy với std :: async, do đó, nó là một câu hỏi cho dù bạn muốn kiểm soát mọi thứ. Ví dụ, ca ngợi hai cync không đồng bộ: bạn có wan't họ cả hai chạy song song (hai chủ đề), hoặc cái khác? Và bạn không cần phải kiểm tra std :: future. –

4

Lời khuyên của tôi ...

Hãy suy nghĩ kỹ về lý do bạn cần thực hiện một hoạt động lâu dài trong một hàm tạo.

tôi thường thấy nó là tốt hơn để chia việc tạo ra một đối tượng thành ba phần

a) phân bổ b) xây dựng c) khởi

Đối với đối tượng nhỏ nó làm cho tinh thần để làm tất cả ba trong một hoạt động "mới". Tuy nhiên, đối tượng trọng lượng nặng, bạn thực sự muốn tách các giai đoạn. Tìm ra bao nhiêu tài nguyên bạn cần và phân bổ nó. Xây dựng đối tượng trong bộ nhớ vào trạng thái hợp lệ nhưng trống.

Sau đó ... thực hiện thao tác tải dài của bạn vào đối tượng đã có giá trị nhưng trống.

Tôi nghĩ rằng tôi đã nhận được mô hình này một thời gian dài trước khi đọc một cuốn sách (Scott Myers có lẽ?) Nhưng tôi khuyên bạn nên nó, nó giải quyết tất cả các loại vấn đề. Ví dụ, nếu đối tượng của bạn là một đối tượng đồ họa, bạn tìm ra dung lượng bộ nhớ cần. Nếu không thành công, hãy hiển thị lỗi cho người dùng càng sớm càng tốt. Nếu không đánh dấu đối tượng là chưa đọc. Sau đó, bạn có thể hiển thị nó trên màn hình, người dùng cũng có thể thao tác nó, v.v. Khởi tạo đối tượng với tải tệp không đồng bộ, khi hoàn thành, đặt cờ trong đối tượng có nội dung "đã tải". Khi chức năng cập nhật của bạn nhìn thấy nó được tải, nó có thể vẽ đồ họa.

Nó cũng thực sự giúp với các vấn đề như trật tự xây dựng, nơi đối tượng A cần đối tượng B. Bạn đột nhiên thấy bạn cần phải thực hiện A trước khi B, oh không !! Đơn giản, tạo một chữ B trống và chuyển nó thành một tham chiếu, miễn là A đủ thông minh để biết rằng nó trống rỗng và đợi nó không phải trước khi nó sử dụng nó, tất cả đều tốt.

Và ... Không quên .. Bạn có thể làm ngược lại với sự hủy diệt. Đánh dấu đối tượng của bạn là trống đầu tiên, vì vậy không có gì mới sử dụng nó (de-khởi động) miễn phí các nguồn lực, (phá hủy) Sau đó giải phóng bộ nhớ (deallocation)

Những lợi ích tương tự áp dụng.

+0

Đây là một gợi ý khôn ngoan, cảm ơn bạn đã chia sẻ! – Cristiano

0

Dưới đây là một mẫu khác để xem xét. Nó tận dụng lợi thế của một thực tế là gọi chờ đợi() trên một tương lai <> không làm mất hiệu lực nó. Vì vậy, miễn là bạn không bao giờ gọi get(), bạn an toàn. Sự cân bằng của mô hình này là bạn phải gánh chịu chi phí cuộc gọi chờ đợi() bất cứ khi nào chức năng thành viên được gọi.

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

Tôi sẽ cảm thấy một chút đáng sợ với giải pháp này, bởi vì những điều xấu có thể xảy ra nếu tôi chỉ quên chờ ở đầu mỗi phương pháp. Hơn nữa, điều này sẽ khiến người gọi chặn khi gọi phương thức đầu tiên và giới thiệu chi phí không cần thiết khi gọi bất kỳ phương thức nào khác sau đây. – Cristiano

Các vấn đề liên quan