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);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
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
print
xử lý (1).
- Người
handle_async_receive
xử lý (3).
- Người
print
xử 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_service
và 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_service
công việc cần được thực hiện. Các async_receive
hoạt động (3) thông báo io_service
rằng nó sẽ cần phải dữ liệu không đồng bộ đọc từ ổ cắm, sau đó async_receive
trả 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
socket
nhậ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
print
trì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_receive
trình xử lý của nó đã được gọi và trả về.
- Các
io_service
dừ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_receive
khô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_service
việ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_service
có ran mất việc, ứng dụng phải reset()
sự io_service
trướ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_receive
thê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ớ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 =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
Ở mức cao, chương trình sẽ tạo ra 2 luồng xử lý io_service
vò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_service
hế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:
- Tạo và thêm
io_service::work
đối tượng được thêm vào io_service
.
- 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_service
vì io_service::work
đối tượng.
- Thêm 3 trình xử lý tính số Fibonacci vào
io_service
và 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.
- Xóa
io_service::work
đối tượng.
- 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_service
khô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_service
vòng lặp sự kiện được xử lý. Điều này loại bỏ nhu cầu sử dụng io_service::work
và 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));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Đồ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 .