Chúng tôi đã sử dụng asio trong sản xuất trong nhiều năm nay và gần đây chúng tôi đã đạt đến điểm quan trọng khi máy chủ của chúng tôi chỉ nạp đủ để nhận thấy vấn đề bí ẩn.boost :: asio lý do đằng sau num_implementations cho io_service :: strand
Trong kiến trúc của chúng tôi, mỗi thực thể riêng lẻ chạy độc lập sử dụng đối tượng strand
cá nhân. Một số thực thể có thể thực hiện một công việc dài (đọc từ tệp, thực hiện yêu cầu MySQL, v.v.). Rõ ràng, công việc được thực hiện trong xử lý được bọc bằng sợi. Tất cả âm thanh đẹp và đẹp và nên hoạt động hoàn hảo, cho đến khi chúng tôi bắt đầu nhận thấy một điều không thể như hẹn giờ hết hạn sau vài giây, mặc dù chủ đề 'chờ công việc' và ngừng hoạt động vì không có lý do rõ ràng. Nó trông giống như công việc dài được thực hiện bên trong một sợi đã ảnh hưởng đến các sợi không liên quan khác, không phải tất cả chúng, nhưng hầu hết.
Đã vô số giờ để xác định vấn đề. Đường đi đã dẫn đến cách đối tượng strand
được tạo: strand_service::construct
(here).
Vì một số lý do, nhà phát triển đã quyết định có một số lượng giới hạn triển khai strand
. Có nghĩa là một số đối tượng hoàn toàn không liên quan sẽ chia sẻ một triển khai đơn lẻ và do đó sẽ bị tắc nghẽn vì điều này.
Trong phương thức độc lập (không tăng cường) asio thư viện, cách tiếp cận tương tự đang được sử dụng. Nhưng thay vì triển khai được chia sẻ, mỗi triển khai hiện độc lập nhưng có thể chia sẻ đối tượng mutex
với các triển khai khác (here).
Tất cả là gì? Tôi chưa bao giờ nghe về giới hạn về số lượng mutex trong hệ thống. Hoặc bất kỳ chi phí nào liên quan đến việc tạo/hủy của họ. Mặc dù vấn đề cuối cùng có thể dễ dàng được giải quyết bằng cách tái chế các mutex thay vì phá hủy chúng.
Tôi có một trường hợp thử nghiệm đơn giản nhất để hiển thị như thế nào ấn tượng là một sự xuống cấp hiệu suất:
#include <boost/asio.hpp>
#include <atomic>
#include <functional>
#include <iostream>
#include <thread>
std::atomic<bool> running{true};
std::atomic<int> counter{0};
struct Work
{
Work(boost::asio::io_service & io_service)
: _strand(io_service)
{ }
static void start_the_work(boost::asio::io_service & io_service)
{
std::shared_ptr<Work> _this(new Work(io_service));
_this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this)));
}
static void do_the_work(std::shared_ptr<Work> _this)
{
counter.fetch_add(1, std::memory_order_relaxed);
if (running.load(std::memory_order_relaxed)) {
start_the_work(_this->_strand.get_io_service());
}
}
boost::asio::strand _strand;
};
struct BlockingWork
{
BlockingWork(boost::asio::io_service & io_service)
: _strand(io_service)
{ }
static void start_the_work(boost::asio::io_service & io_service)
{
std::shared_ptr<BlockingWork> _this(new BlockingWork(io_service));
_this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this)));
}
static void do_the_work(std::shared_ptr<BlockingWork> _this)
{
sleep(5);
}
boost::asio::strand _strand;
};
int main(int argc, char ** argv)
{
boost::asio::io_service io_service;
std::unique_ptr<boost::asio::io_service::work> work{new boost::asio::io_service::work(io_service)};
for (std::size_t i = 0; i < 8; ++i) {
Work::start_the_work(io_service);
}
std::vector<std::thread> workers;
for (std::size_t i = 0; i < 8; ++i) {
workers.push_back(std::thread([&io_service] {
io_service.run();
}));
}
if (argc > 1) {
std::cout << "Spawning a blocking work" << std::endl;
workers.push_back(std::thread([&io_service] {
io_service.run();
}));
BlockingWork::start_the_work(io_service);
}
sleep(5);
running = false;
work.reset();
for (auto && worker : workers) {
worker.join();
}
std::cout << "Work performed:" << counter.load() << std::endl;
return 0;
}
Build nó sử dụng lệnh này:
g++ -o asio_strand_test_case -pthread -I/usr/include -std=c++11 asio_strand_test_case.cpp -lboost_system
thử nghiệm chạy theo một cách thông thường:
time ./asio_strand_test_case
Work performed:6905372
real 0m5.027s
user 0m24.688s
sys 0m12.796s
Chạy thử nghiệm với tác vụ chặn dài:
time ./asio_strand_test_case 1
Spawning a blocking work
Work performed:770
real 0m5.031s
user 0m0.044s
sys 0m0.004s
Sự khác biệt rất ấn tượng. Điều xảy ra là mỗi tác vụ không chặn mới tạo đối tượng strand
mới cho đến khi nó chia sẻ cùng một triển khai với strand
của tác vụ chặn. Khi điều này xảy ra, đó là một kết thúc chết, cho đến khi kết thúc công việc lâu dài.
Sửa: Giảm công việc song song xuống số đề làm việc (1000
-8
) và đầu ra chạy thử nghiệm cập nhật. Đã làm điều này bởi vì khi cả hai con số được gần vấn đề là nhìn thấy rõ hơn.
* "Dự đoán của tôi để làm một điều như vậy sẽ là không cho phép quá tải hệ điều hành bằng cách tạo ra rất nhiều và rất nhiều mutexes. Đó sẽ là xấu." * Tại sao? Những gì trên không có ngoài bộ nhớ số không đổi nhỏ (mỗi mutex)? –
@yurikilochek Chúng là mutexes. Theo định nghĩa, chúng vô ích trừ khi được sử dụng để đồng bộ hóa. Điều đó khiến cho bộ sưu tập lớn các nguyên bản đồng bộ hóa được chờ đợi đồng thời. ':: WaitForMultipleObjectsEx' có thể không quan tâm, nhưng đó là một chuyển đổi ngữ cảnh, nó không chỉ là một vài byte bộ nhớ. Trên Linux, không có gọi AFAIK như vậy. – sehe
@Arunmu Bất kể số lần triển khai vấn đề sẽ tồn tại, bởi vì nó nằm trong thiết kế. Tăng số lượng có thể giành chiến thắng một thời gian nhưng chỉ ở một mức độ nào đó. Trong ứng dụng thời gian thực, điều này sẽ không bao giờ hoạt động. Hãy thử ví dụ của tôi với 'đối tượng công việc' bằng số lượng chủ đề, tức là' 8' thay vì '1000'. Trong trường hợp đó, việc triển khai '1024' hầu như không giúp được gì (' Công việc thực hiện: 8331'). – GreenScape