Trong chương “hướng dẫn” 5.3.5.3 của cuốn sách Ngôn ngữ lập trình C++ (ấn bản thứ 4), Bjarne Stroustrup viết về hàm std::async
.Giới hạn của std :: async là Stroustrup là gì?
Có một giới hạn rõ ràng: Đừng bao giờ nghĩ rằng sử dụng
async()
cho các nhiệm vụ mà khóa dùng chung tài nguyên cần - vớiasync()
bạn thậm chí không biết có bao nhiêuthread
s sẽ được sử dụng bởi vì đó là lên đếnasync()
quyết định dựa trên những gì nó biết về tài nguyên hệ thống có sẵn tại thời điểm cuộc gọi.
Có thể tìm thấy lời khích lệ tương tự trong the C++11-FAQ on his website.
“Đơn giản” là khía cạnh quan trọng nhất của thiết kế
async()
/future
; tương lai cũng có thể được sử dụng với các chủ đề nói chung, nhưng thậm chí không nghĩ rằng sử dụngasync()
để khởi chạy các tác vụ làm I/O, thao tác mutexes hoặc theo các cách khác tương tác với các tác vụ khác.
Điều thú vị là, anh không giải thích về giới hạn này khi anh quay lại các tính năng đồng thời của C++ 11 cụ thể hơn trong § 42.4.6 của cuốn sách. Thậm chí thú vị hơn, trong chương này, anh ta thực sự tiếp tục (so sánh với tuyên bố trên trang web của mình):
Sử dụng đơn giản và thực tế để thu thập dữ liệu từ người dùng.
The documentation of async
on cppreference.com
không đề cập đến bất kỳ hạn chế nào như vậy.
Sau khi đọc một số đề xuất và thảo luận dẫn đến tiêu chuẩn C++ 11 ở dạng cuối cùng (không may là tôi không có quyền truy cập), tôi hiểu rằng async
được kết hợp rất muộn vào tiêu chuẩn C++ 11 và có rất nhiều cuộc thảo luận về tính năng mạnh mẽ này. Đặc biệt, tôi đã tìm thấy bài viết Async Tasks in C++11: Not Quite There Yet by Bartosz Milewski một bản tóm tắt rất tốt về các vấn đề cần được xem xét khi triển khai async
.
Tuy nhiên, tất cả các vấn đề thảo luận có liên quan đến thread_local
biến mà không phải là destructed nếu a thread được tái chế, bế tắc giả mạo hoặc vi phạm truy cập dữ liệu nếu a thread chuyển nhiệm vụ giữa hành động của mình và cả hai nhiệm vụ tổ chức một mutex
hay recursive_mutex
tương ứng và do đó ra. Đây là những mối quan tâm nghiêm trọng đối với người triển khai tính năng nhưng nếu tôi hiểu chính xác, đặc điểm kỹ thuật hiện tại của async
yêu cầu tất cả các chi tiết này bị ẩn khỏi người dùng bằng cách thực hiện tác vụ hoặc trên chuỗi của người gọi hoặc như thể một chủ đề mới đã được tạo cho bài tập.
Vì vậy, câu hỏi của tôi là: gì tôi không được phép làm với async
rằng tôi được phép làm bằng thread
s bằng tay và lý do hạn chế này là gì?
Ví dụ: có sự cố gì với chương trình sau không?
#include <future>
#include <iostream>
#include <mutex>
#include <vector>
static int tally {};
static std::mutex tally_mutex {};
static void
do_work(const int amount)
{
for (int i = 0; i < amount; ++i)
{
// Might do something actually useful...
const std::unique_lock<std::mutex> lock {tally_mutex};
tally += 1;
}
}
int
main()
{
constexpr int concurrency {10};
constexpr int amount {1000000};
std::vector<std::future<void>> futures {};
for (int t = 0; t < concurrency; ++t)
futures.push_back(std::async(do_work, amount/concurrency));
for (auto& future : futures)
future.get();
std::cout << tally << std::endl;
}
Rõ ràng, nếu thời gian chạy quyết định lên lịch tất cả các tác vụ trên chuỗi chính, chúng tôi sẽ không cần thiết phải lấy lại nhiều lần vì không có lý do chính đáng. Nhưng mặc dù điều này có thể là không hiệu quả, nhưng không phải là không chính xác.
Như trong ví dụ của Stroustrup - không làm khóa, bạn có thể dễ dàng bị bế tắc vì không đảm bảo rằng async sẽ được thực hiện trong một chuỗi khác. Trong ví dụ của bạn, chỉ cần khóa mutex trong chức năng chính trước khi gọi là không đồng bộ. –
@DmitriSosnik Bạn có thể giải thích chính xác điều này có thể gây ra bế tắc không? Nhận được mutex một lần trong thread chính trước khi tung ra các nhiệm vụ chắc chắn sẽ không bế tắc mà còn làm giảm việc sử dụng một quảng cáo mutex absurdum và cho 'tally' một giá trị rác. Dù sao, câu hỏi của tôi là những hạn chế của 'std :: async', không phải là cách tốt nhất để thiết lập một biến là 1000000. – 5gon12eder