Nhầm lẫn khi boost :: asio :: io_service run method blocks / unblocks


88

Là một người mới bắt đầu hoàn toàn với Boost.Asio, tôi bối rối với io_service::run(). Tôi sẽ đánh giá cao nếu ai đó có thể giải thích cho tôi khi phương pháp này chặn / bỏ chặn. Các tài liệu cho biết:

Các run()chức năng sẽ chặn cho đến khi tất cả công việc kết thúc và không có thêm trình xử lý nào được gửi đi hoặc cho đến khi io_servicengừng hoạt động.

Nhiều luồng có thể gọi run()hàm để thiết lập một nhóm các luồng mà từ đó các io_servicetrình xử lý có thể thực thi. Tất cả các luồng đang chờ trong nhóm là tương đương và io_servicecó thể chọn bất kỳ luồng nào trong số chúng để gọi một trình xử lý.

Một lối thoát bình thường khỏi run()hàm ngụ ý rằng io_serviceđối tượng bị dừng ( stopped()hàm trả về true). Cuộc gọi tiếp theo để run(), run_one(), poll()hoặc poll_one()sẽ trở lại ngay lập tức trừ khi có một cuộc gọi trước reset().

Câu lệnh sau có nghĩa là gì?

[...] không còn người xử lý nào được cử đi [...]


Trong khi cố gắng hiểu hành vi của io_service::run(), tôi đã xem qua ví dụ này (ví dụ 3a). Trong đó, tôi quan sát io_service->run()các khối đó và chờ lệnh công việc.

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->post( boost::bind(CalculateFib, 3));
io_service->post( boost::bind(CalculateFib, 4));
io_service->post( boost::bind(CalculateFib, 5));

work.reset();
worker_threads.join_all();

Tuy nhiên, trong đoạn mã sau mà tôi đang làm việc, máy khách kết nối bằng TCP / IP và phương thức chạy khối cho đến khi nhận được dữ liệu không đồng bộ.

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> socket(new tcp::socket(*io_service));

// Connect to 127.0.0.1:9100.
tcp::resolver resolver(*io_service);
tcp::resolver::query query("127.0.0.1", 
                           boost::lexical_cast< std::string >(9100));
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
socket->connect(endpoint_iterator->endpoint());

// Just blocks here until a message is received.
socket->async_receive(boost::asio::buffer(buf_client, 3000), 0,
                      ClientReceiveEvent);
io_service->run();

// Write response.
boost::system::error_code ignored_error;
std::cout << "Sending message \n";
boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error);

Bất kỳ giải thích nào về run()điều đó mô tả hành vi của nó trong hai ví dụ dưới đây sẽ được đánh giá cao.

Câu trả lời:


234

nền tảng

Hãy bắt đầu với một ví dụ đơn giản và kiểm tra các phần Boost.Asio có liên quan:

void handle_async_receive(...) { ... }
void print() { ... }

...  

boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);

...

io_service.post(&print);                             // 1
socket.connect(endpoint);                            // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print);                             // 4
io_service.run();                                    // 5

Người xử lý là gì?

Một trình xử lý không gì khác hơn là một cuộc gọi lại. Trong mã ví dụ, có 3 trình xử lý:

  • Người printxử lý (1).
  • Người handle_async_receivexử lý (3).
  • Người printxử lý (4).

Mặc dù cùng một print()hàm được sử dụng hai lần, mỗi lần sử dụng được coi là tạo ra trình xử lý có thể nhận dạng duy nhất của riêng nó. Các trình xử lý có thể có nhiều hình dạng và kích cỡ, từ các chức năng cơ bản như các chức năng ở trên đến các cấu trúc phức tạp hơn như các bộ điều khiển được tạo từ boost::bind()và lambdas. Bất kể độ phức tạp như thế nào, trình xử lý vẫn không có gì khác hơn là một cuộc gọi lại.

Công việc là gì?

Công việc là một số xử lý mà Boost.Asio đã được yêu cầu thực hiện thay mặt cho mã ứng dụng. Đôi khi Boost.Asio có thể bắt đầu một số công việc ngay sau khi nó được thông báo về nó, và những lần khác, nó có thể chờ đợi để thực hiện công việc sau đó. Sau khi hoàn thành công việc, Boost.Asio sẽ thông báo cho ứng dụng bằng cách gọi trình xử lý được cung cấp .

Boost.Asio đảm bảo rằng xử lý sẽ chỉ chạy trong một chủ đề mà hiện nay đang kêu gọi run(), run_one(), poll(), hoặc poll_one(). Đây là các luồng sẽ thực hiện công việc và gọi các trình xử lý . Do đó, trong ví dụ trên, print()không được gọi khi nó được đăng vào io_service(1). Thay vào đó, nó được thêm vào io_servicevà sẽ được gọi vào một thời điểm sau đó. Trong trường hợp này, nó nằm trong io_service.run()(5).

Hoạt động không đồng bộ là gì?

Một hoạt động không đồng bộ tạo ra công việc và Boost.Asio sẽ gọi một trình xử lý để thông báo cho ứng dụng khi công việc đã hoàn thành. Các hoạt động không đồng bộ được tạo ra bằng cách gọi một hàm có tên với tiền tố async_. Các hàm này còn được gọi là các hàm khởi tạo .

Các hoạt động không đồng bộ có thể được phân tách thành ba bước duy nhất:

  • Bắt đầu, hoặc thông báo, liên quan đến io_servicecông việc cần được thực hiện. Các async_receivehoạt động (3) thông báo io_servicerằng nó sẽ cần phải dữ liệu không đồng bộ đọc từ ổ cắm, sau đó async_receivetrả về ngay lập tức.
  • Đang thực hiện công việc thực tế. Trong trường hợp này, khi socketnhận dữ liệu, các byte sẽ được đọc và sao chép vào buffer. Công việc thực tế sẽ được thực hiện trong:
    • Hàm khởi tạo (3), nếu Boost.Asio có thể xác định rằng nó sẽ không chặn.
    • Khi ứng dụng chạy rõ ràng io_service(5).
  • Gọi handle_async_receive ReadHandler . Một lần nữa, các trình xử lý chỉ được gọi trong các chuỗi đang chạy io_service. Do đó, bất kể khi nào công việc được thực hiện (3 hoặc 5), nó được đảm bảo rằng handle_async_receive()sẽ chỉ được gọi trong io_service.run()(5).

Sự tách biệt về thời gian và không gian giữa ba bước này được gọi là nghịch lưu dòng điều khiển. Đó là một trong những sự phức tạp làm cho việc lập trình không đồng bộ trở nên khó khăn. Tuy nhiên, có những kỹ thuật có thể giúp giảm thiểu điều này, chẳng hạn như bằng cách sử dụng coroutines .

Làm gì io_service.run()?

Khi một luồng gọi io_service.run(), công việc và trình xử lý sẽ được gọi từ bên trong luồng này. Trong ví dụ trên, io_service.run()(5) sẽ chặn cho đến khi:

  • Nó đã được gọi và trả về từ cả hai printtrình xử lý, hoạt động nhận hoàn thành dù thành công hay thất bại, và handle_async_receivetrình xử lý của nó đã được gọi và trả về.
  • Các io_servicedừng lại một cách rõ ràng qua io_service::stop().
  • Một ngoại lệ được đưa ra từ bên trong một trình xử lý.

Một luồng psuedo-ish tiềm năng có thể được mô tả như sau:

tạo io_service
tạo ổ cắm
thêm trình xử lý in vào io_service (1)
đợi ổ cắm kết nối (2)
thêm một yêu cầu công việc đọc không đồng bộ vào io_service (3)
thêm trình xử lý in vào io_service (4)
chạy io_service (5)
  có công việc hoặc người xử lý không?
    vâng, có 1 tác phẩm và 2 trình xử lý
      ổ cắm có dữ liệu không? không, không làm gì cả
      chạy trình xử lý in (1)
  có công việc hoặc người xử lý không?
    vâng, có 1 tác phẩm và 1 trình xử lý
      ổ cắm có dữ liệu không? không, không làm gì cả
      chạy trình xử lý in (4)
  có công việc hoặc người xử lý không?
    vâng, có 1 tác phẩm
      ổ cắm có dữ liệu không? không, tiếp tục đợi
  - socket nhận dữ liệu -
      socket có dữ liệu, hãy đọc nó vào bộ đệm
      thêm trình xử lý handle_async_receive vào io_service
  có công việc hoặc người xử lý không?
    vâng, có 1 trình xử lý
      chạy trình xử lý handle_async_receive (3)
  có công việc hoặc người xử lý không?
    không, đặt io_service là dừng và quay lại

Lưu ý khi đọc xong, nó đã thêm một trình xử lý khác vào io_service. Chi tiết tinh tế này là một đặc điểm quan trọng của lập trình không đồng bộ. Nó cho phép các trình xử lý được liên kết với nhau. Ví dụ: nếu handle_async_receivekhông nhận được tất cả dữ liệu mà nó mong đợi, thì việc triển khai nó có thể đăng một hoạt động đọc không đồng bộ khác, dẫn đến io_serviceviệc có nhiều công việc hơn và do đó không quay lại từ đó io_service.run().

Do lưu ý rằng khi io_servicecó ran mất việc, ứng dụng phải reset()sự io_servicetrước khi chạy nó một lần nữa.


Câu hỏi ví dụ và mã ví dụ 3a

Bây giờ, hãy kiểm tra hai đoạn mã được tham chiếu trong câu hỏi.

Mã câu hỏi

socket->async_receivethêm công việc vào io_service. Do đó, io_service->run()sẽ chặn cho đến khi hoạt động đọc hoàn thành với thành công hoặc lỗi, và ClientReceiveEventđã chạy xong hoặc ném một ngoại lệ.

Ví dụ 3a

Với hy vọng làm cho nó dễ hiểu hơn, đây là một Ví dụ 3a được chú thích nhỏ hơn:

void CalculateFib(std::size_t n);

int main()
{
  boost::asio::io_service io_service;
  boost::optional<boost::asio::io_service::work> work =       // '. 1
      boost::in_place(boost::ref(io_service));                // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  work = boost::none;                                         // 4
  worker_threads.join_all();                                  // 5
}

Ở mức cao, chương trình sẽ tạo ra 2 luồng xử lý io_servicevòng lặp sự kiện của (2). Điều này dẫn đến một nhóm chủ đề đơn giản sẽ tính toán các số Fibonacci (3).

Một điểm khác biệt chính giữa Mã câu hỏi và mã này là mã này gọi io_service::run()(2) trước khi công việc thực tế và trình xử lý được thêm vào io_service(3). Để ngăn việc io_service::run()trả về ngay lập tức, một io_service::workđối tượng được tạo (1). Đối tượng này ngăn không cho io_servicehết công việc; do đó, io_service::run()sẽ không trở lại do không có công việc.

Quy trình tổng thể như sau:

  1. Tạo và thêm io_service::workđối tượng được thêm vào io_service.
  2. Nhóm chủ đề được tạo ra mà gọi io_service::run(). Các luồng công nhân này sẽ không trở lại từ io_serviceio_service::workđối tượng.
  3. Thêm 3 trình xử lý tính số Fibonacci vào io_servicevà trả về ngay lập tức. Các luồng công nhân, không phải luồng chính, có thể bắt đầu chạy các trình xử lý này ngay lập tức.
  4. Xóa io_service::workđối tượng.
  5. Chờ các luồng công nhân chạy xong. Điều này sẽ chỉ xảy ra khi cả 3 trình xử lý đã hoàn thành việc thực thi, vì cả 3 trình xử lý đều io_servicekhông có trình xử lý và hoạt động.

Mã có thể được viết theo cách khác, giống như Mã gốc, nơi các trình xử lý được thêm vào io_service, và sau đó io_servicevòng lặp sự kiện được xử lý. Điều này loại bỏ nhu cầu sử dụng io_service::workvà dẫn đến mã sau:

int main()
{
  boost::asio::io_service io_service;

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'
  worker_threads.join_all();                                  // 5
}

Đồng bộ so với Không đồng bộ

Mặc dù mã trong câu hỏi đang sử dụng hoạt động không đồng bộ, nhưng nó đang hoạt động đồng bộ một cách hiệu quả, vì nó đang đợi hoạt động không đồng bộ hoàn tất:

socket.async_receive(buffer, handler)
io_service.run();

tương đương với:

boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);

Theo nguyên tắc chung, cố gắng tránh trộn các hoạt động đồng bộ và không đồng bộ. Thông thường, nó có thể biến một hệ thống phức tạp thành một hệ thống phức tạp. Câu trả lời này nêu bật các ưu điểm của lập trình không đồng bộ, một số ưu điểm cũng được đề cập trong tài liệu Boost.Asio .


13
Bài viết tuyệt vời. Tôi chỉ muốn thêm một điều vì tôi cảm thấy nó không được chú ý: Sau khi run () đã trở lại, bạn cần gọi reset () trên io_service của mình trước khi có thể chạy lại () nó. Nếu không, nó có thể trả về ngay lập tức cho dù có hoặc không có hoạt động async_ đang chờ hay không.
DeVadder

Bộ đệm đến từ đâu? Nó là gì?
ruipacheco

Tôi vẫn còn bối rối. Nếu trộn là đồng bộ và không đồng bộ không được khuyến khích, thì chế độ không đồng bộ thuần túy là gì? bạn có thể cho một ví dụ hiển thị mã mà không có io_service.run ();?
Splash

@Splash Người ta có thể sử dụng io_service.poll()để xử lý vòng lặp sự kiện mà không cần chặn các hoạt động nổi bật. Khuyến nghị chính để tránh trộn lẫn các hoạt động đồng bộ và không đồng bộ là để tránh thêm sự phức tạp không cần thiết và để ngăn khả năng phản hồi kém khi trình xử lý mất nhiều thời gian để hoàn thành. Có một số trường hợp an toàn, chẳng hạn như khi người ta biết hoạt động đồng bộ sẽ không chặn.
Tanner Sansbury

Ý bạn là gì khi "hiện tại" trong "Boost.Asio đảm bảo rằng trình xử lý sẽ chỉ chạy trong một chuỗi hiện đang gọirun() ...." ? Nếu có N luồng (đã được gọi run()), thì cái nào là luồng "hiện tại"? Có thể có nhiều? Hay ý của bạn là luồng đã hoàn thành việc thực thi async_*()(nói async_read), cũng được đảm bảo gọi các trình xử lý của nó?
Nawaz

18

Để đơn giản hóa cách runlàm, hãy nghĩ về nó như một nhân viên phải xử lý một đống giấy; nó lấy một tờ, thực hiện những gì mà tờ nói, ném tờ đi và lấy tờ tiếp theo; khi anh ta hết tờ, nó rời khỏi văn phòng. Trên mỗi tờ có thể có bất kỳ loại chỉ dẫn nào, thậm chí thêm một tờ mới vào cọc. Về ASIO: bạn có thể cung cấp cho một io_servicecông việc theo hai cách, về cơ bản: bằng cách sử dụng postvào nó như trong mẫu mà bạn liên kết, hoặc bằng cách sử dụng các đối tượng khác mà trong nội bộ gọi postvào io_service, giống như socketvà nó async_*phương pháp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.