16

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ới async() bạn thậm chí không biết có bao nhiêu thread s sẽ được sử dụng bởi vì đó là lên đến async() 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ụng async() để 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.

+0

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ộ. –

+1

@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

Trả lời

5

"Sự cố" với tiêu chuẩn: async là theo mặc định bạn không biết có bắt đầu chuỗi hay không. Nếu hàm của bạn cần chạy trên một luồng riêng biệt thì đây là vấn đề vì hàm của bạn có thể không chạy cho đến khi hàm get() hoặc wait() được gọi. Bạn có thể vượt qua std :: launch :: async để đảm bảo chức năng được khởi chạy trên chuỗi riêng của nó, giống như std :: thread không thể tách BG e.

+0

Vâng, điều đó có ý nghĩa. Tôi sẽ lấy câu trả lời đó là "về cơ bản, không có ràng buộc" bởi vì với tôi, "vấn đề" đó thực sự chỉ là ngữ nghĩa của 'std :: async' nên đề cập đến nó như một giới hạn bằng cách nào đó như nói" không sử dụng 'fread' để ghi vào tập tin”. Tất nhiên, khi thực hiện một cặp sản xuất-người tiêu dùng, nó sẽ là một ý tưởng thực sự tồi để sử dụng 'std :: async' với chính sách khởi chạy mặc định. – 5gon12eder

0

Bạn đã chỉ ra sự cố. Các chủ đề có thể được tái chế ... Vì vậy, việc sử dụng bộ nhớ thread_local rất nguy hiểm.

Vòng lặp của bạn chỉ - nhưng, như bạn đã đề cập, có thể không hiệu quả.

Bạn có thể yêu cầu ngôn ngữ tạo ra một chuỗi khác bằng std :: launch :: async. Nhưng chủ đề vẫn có thể được tái chế.

+2

Cảm ơn bạn. Điều tôi tò mò là - nếu tôi hiểu đúng tiêu chuẩn - việc triển khai phải hoạt động * như thể nó đã tạo ra một luồng mới (hoặc thực thi tác vụ trực tiếp trên luồng của người gọi), ngay cả khi nó thực sự tái chế. Vậy đâu là điểm "nguy hiểm" với 'std :: async' nhưng an toàn với' std :: thread'? Nhưng nếu bạn có thể sao lưu khiếu nại của bạn rằng cấu trúc trong ví dụ của tôi là tuân thủ tiêu chuẩn, tôi sẽ chấp nhận câu trả lời của bạn. – 5gon12eder

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