Tôi hiểu tình hình tốt hơn một chút bây giờ (với số lượng không nhỏ do các câu trả lời ở đây!), Vì vậy tôi nghĩ rằng tôi đã thêm một chút viết lên của riêng tôi.
Có hai khái niệm riêng biệt, mặc dù có liên quan, trong C ++ 11: Tính toán không đồng bộ (một hàm được gọi ở một nơi khác) và thực thi đồng thời (một luồng , một cái gì đó hoạt động đồng thời). Hai là một số khái niệm trực giao. Tính toán không đồng bộ chỉ là một hương vị khác nhau của lệnh gọi hàm, trong khi một luồng là bối cảnh thực thi. Chủ đề là hữu ích theo cách riêng của họ, nhưng với mục đích của cuộc thảo luận này, tôi sẽ coi chúng như một chi tiết thực hiện.
Có một hệ thống trừu tượng cho tính toán không đồng bộ. Ví dụ: vì giả sử chúng ta có một hàm có một số đối số:
int foo(double, char, bool);
Trước hết, chúng ta có mẫu std::future<T>
, đại diện cho một giá trị tương lai của loại T
. Giá trị có thể được lấy thông qua chức năng thành viên get()
, giúp đồng bộ hóa chương trình một cách hiệu quả bằng cách chờ kết quả. Ngoài ra, một hỗ trợ trong tương lai wait_for()
, có thể được sử dụng để thăm dò xem kết quả đã có sẵn hay chưa. Tương lai nên được coi là sự thay thế thả xuống không đồng bộ cho các loại lợi nhuận thông thường. Đối với chức năng ví dụ của chúng tôi, chúng tôi mong đợi a std::future<int>
.
Bây giờ, trên hệ thống phân cấp, từ cấp cao nhất đến cấp thấp nhất:
std::async
: Cách thuận tiện và đơn giản nhất để thực hiện tính toán không đồng bộ là thông qua async
mẫu hàm, trả về tương lai phù hợp ngay lập tức:
auto fut = std::async(foo, 1.5, 'x', false); // is a std::future<int>
Chúng tôi có rất ít quyền kiểm soát các chi tiết. Cụ thể, chúng ta thậm chí không biết liệu chức năng này có được thực hiện đồng thời, theo kiểu ser seri get()
hay bởi một số phép thuật đen khác. Tuy nhiên, kết quả dễ dàng thu được khi cần:
auto res = fut.get(); // is an int
Bây giờ chúng ta có thể xem xét làm thế nào để thực hiện một cái gì đó như thế nào async
, nhưng theo cách mà chúng ta kiểm soát. Ví dụ, chúng tôi có thể nhấn mạnh rằng hàm được thực thi trong một luồng riêng biệt. Chúng ta đã biết rằng chúng ta có thể cung cấp một luồng riêng biệt bằng phương tiện của std::thread
lớp.
Mức độ trừu tượng thấp hơn tiếp theo thực hiện chính xác điều đó : std::packaged_task
. Đây là một mẫu bao bọc một hàm và cung cấp một tương lai cho giá trị trả về của các hàm, nhưng bản thân đối tượng có thể gọi được và việc gọi nó theo ý của người dùng. Chúng ta có thể thiết lập nó như thế này:
std::packaged_task<int(double, char, bool)> tsk(foo);
auto fut = tsk.get_future(); // is a std::future<int>
Tương lai sẽ sẵn sàng khi chúng ta gọi nhiệm vụ và cuộc gọi hoàn thành. Đây là công việc lý tưởng cho một chủ đề riêng biệt. Chúng ta chỉ cần đảm bảo di chuyển tác vụ vào luồng:
std::thread thr(std::move(tsk), 1.5, 'x', false);
Các chủ đề bắt đầu chạy ngay lập tức. Chúng ta có thể detach
hoặc có join
nó ở cuối phạm vi hoặc bất cứ khi nào (ví dụ: sử dụng scoped_thread
trình bao bọc của Anthony Williams , thứ thực sự cần có trong thư viện chuẩn). Tuy nhiên, các chi tiết sử dụng std::thread
không liên quan đến chúng tôi ở đây; chỉ cần chắc chắn để tham gia hoặc tách ra thr
cuối cùng. Vấn đề là bất cứ khi nào chức năng gọi kết thúc, kết quả của chúng tôi đã sẵn sàng:
auto res = fut.get(); // as before
Bây giờ chúng tôi xuống mức thấp nhất: Làm thế nào chúng tôi sẽ thực hiện nhiệm vụ đóng gói? Đây là nơi std::promise
xuất hiện. Lời hứa là khối xây dựng để liên lạc với một tương lai. Các bước chính là:
Các chủ đề cuộc gọi làm cho một lời hứa.
Chuỗi cuộc gọi có được một tương lai từ lời hứa.
Lời hứa, cùng với các đối số chức năng, được chuyển thành một luồng riêng biệt.
Các chủ đề mới thực hiện chức năng và thực hiện lời hứa.
Các chủ đề ban đầu lấy kết quả.
Ví dụ, đây là "nhiệm vụ đóng gói" rất riêng của chúng tôi:
template <typename> class my_task;
template <typename R, typename ...Args>
class my_task<R(Args...)>
{
std::function<R(Args...)> fn;
std::promise<R> pr; // the promise of the result
public:
template <typename ...Ts>
explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
template <typename ...Ts>
void operator()(Ts &&... ts)
{
pr.set_value(fn(std::forward<Ts>(ts)...)); // fulfill the promise
}
std::future<R> get_future() { return pr.get_future(); }
// disable copy, default move
};
Việc sử dụng mẫu này về cơ bản là giống như của std::packaged_task
. Lưu ý rằng việc di chuyển toàn bộ nhiệm vụ sẽ chuyển lời hứa. Trong các tình huống đặc biệt hơn, người ta cũng có thể chuyển đối tượng lời hứa một cách rõ ràng vào luồng mới và biến nó thành đối số chức năng của hàm luồng, nhưng trình bao bọc nhiệm vụ như ở trên có vẻ như là một giải pháp linh hoạt hơn và ít xâm phạm hơn.
Ngoại lệ
Lời hứa có liên quan mật thiết đến ngoại lệ. Chỉ riêng giao diện của một lời hứa là không đủ để truyền tải hoàn toàn trạng thái của nó, vì vậy các ngoại lệ được đưa ra bất cứ khi nào một thao tác trên một lời hứa không có ý nghĩa. Tất cả các ngoại lệ là loại std::future_error
, xuất phát từ std::logic_error
. Trước hết, một mô tả về một số hạn chế:
Một lời hứa được xây dựng mặc định là không hoạt động. Lời hứa không hoạt động có thể chết mà không có hậu quả.
Một lời hứa sẽ hoạt động khi có được một tương lai thông qua get_future()
. Tuy nhiên, chỉ có một tương lai có thể có được!
Một lời hứa phải được thỏa mãn thông qua set_value()
hoặc có một ngoại lệ được đặt ra set_exception()
trước khi thời gian tồn tại của nó kết thúc nếu tương lai của nó được tiêu thụ. Một lời hứa hài lòng có thể chết mà không có hậu quả, và get()
sẽ có sẵn trong tương lai. Một lời hứa với một ngoại lệ sẽ đưa ra ngoại lệ được lưu trữ theo yêu cầu của get()
tương lai. Nếu lời hứa chết không có giá trị cũng không ngoại lệ, kêu gọi get()
tương lai sẽ đưa ra ngoại lệ "thất hứa".
Dưới đây là một loạt thử nghiệm nhỏ để chứng minh những hành vi đặc biệt khác nhau này. Đầu tiên, khai thác:
#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>
int test();
int main()
{
try
{
return test();
}
catch (std::future_error const & e)
{
std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl;
}
catch (std::exception const & e)
{
std::cout << "Standard exception: " << e.what() << std::endl;
}
catch (...)
{
std::cout << "Unknown exception." << std::endl;
}
}
Bây giờ vào các bài kiểm tra.
Trường hợp 1: Lời hứa không hoạt động
int test()
{
std::promise<int> pr;
return 0;
}
// fine, no problems
Trường hợp 2: Lời hứa tích cực, không sử dụng
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
return 0;
}
// fine, no problems; fut.get() would block indefinitely
Trường hợp 3: Quá nhiều tương lai
int test()
{
std::promise<int> pr;
auto fut1 = pr.get_future();
auto fut2 = pr.get_future(); // Error: "Future already retrieved"
return 0;
}
Trường hợp 4: Lời hứa thỏa mãn
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_value(10);
}
return fut.get();
}
// Fine, returns "10".
Trường hợp 5: Quá nhiều sự hài lòng
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_value(10);
pr2.set_value(10); // Error: "Promise already satisfied"
}
return fut.get();
}
Trường hợp ngoại lệ cùng được ném nếu có nhiều hơn một trong hai của set_value
hay set_exception
.
Trường hợp 6: Ngoại lệ
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo")));
}
return fut.get();
}
// throws the runtime_error exception
Trường hợp 7: Hứa hẹn
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
} // Error: "broken promise"
return fut.get();
}