2011-12-21 33 views
8

Tôi đang viết một máy chủ đa luồng tương thích POSIX trong c/C++ phải có khả năng chấp nhận, đọc từ và ghi vào một số lượng lớn kết nối không đồng bộ. Máy chủ có một số luồng công nhân thực hiện nhiệm vụ và đôi khi (và không thể đoán trước) dữ liệu hàng đợi được ghi vào ổ cắm. Dữ liệu cũng đôi khi (và không thể đoán trước) được ghi vào ổ cắm bởi các máy khách, vì vậy máy chủ cũng phải đọc không đồng bộ. Một cách rõ ràng để làm điều này là cung cấp cho mỗi kết nối một luồng mà đọc và ghi từ/đến socket của nó; điều này là xấu, mặc dù, vì mỗi kết nối có thể tồn tại trong một thời gian dài và máy chủ do đó có thể phải giữ hàng trăm hoặc hàng ngàn chủ đề chỉ để theo dõi các kết nối.Đang chờ điều kiện (pthread_cond_wait) và thay đổi ổ cắm (chọn) đồng thời

Một cách tiếp cận tốt hơn là phải có một chuỗi duy nhất xử lý tất cả các liên lạc bằng cách sử dụng các hàm select()/pselect(). Tức là, một chuỗi đơn lẻ chờ trên bất kỳ socket nào có thể đọc được, sau đó sinh ra một công việc để xử lý đầu vào sẽ được xử lý bởi một nhóm các chủ đề khác bất cứ khi nào đầu vào có sẵn. Bất cứ khi nào các luồng công nhân khác tạo ra đầu ra cho một kết nối, nó sẽ được xếp hàng đợi và luồng giao tiếp đợi cho socket đó có thể ghi được trước khi ghi nó.

Vấn đề với điều này là luồng giao tiếp có thể đang chờ trong hàm select() hoặc pselect() khi đầu ra được xếp hàng đợi bởi các luồng công nhân của máy chủ. Có thể, nếu không có đầu vào đến trong vài giây hoặc vài phút, một đoạn đầu ra được xếp hàng đợi sẽ chỉ đợi cho chuỗi giao tiếp được thực hiện select() ing. Điều này không nên xảy ra, tuy nhiên - dữ liệu nên được viết càng sớm càng tốt.

Hiện tại tôi thấy một vài giải pháp cho vấn đề này an toàn chỉ. Một là để có các chủ đề giao tiếp bận rộn-chờ đợi trên đầu vào và cập nhật danh sách các ổ cắm nó chờ đợi để viết mỗi thứ mười của một giây hoặc lâu hơn. Đây không phải là tối ưu vì nó liên quan đến việc chờ đợi, nhưng nó sẽ hoạt động. Một tùy chọn khác là sử dụng pselect() và gửi tín hiệu USR1 (hoặc một cái gì đó tương đương) bất cứ khi nào đầu ra mới được xếp hàng đợi, cho phép chuỗi giao tiếp cập nhật danh sách các ổ cắm mà nó đang chờ trạng thái ghi ngay lập tức. Tôi thích thứ hai ở đây, nhưng vẫn không thích sử dụng một tín hiệu cho một cái gì đó mà phải là một điều kiện (pthread_cond_t). Tuy nhiên, một tùy chọn khác sẽ bao gồm, trong danh sách các bộ mô tả tập tin mà select() đang đợi, một tệp giả mà chúng ta viết một byte vào bất cứ khi nào một socket cần được thêm vào tập tin fd_set có thể ghi cho select(); điều này sẽ đánh thức máy chủ truyền thông vì tệp giả định cụ thể đó sẽ có thể đọc được, do đó cho phép luồng truyền thông cập nhật ngay lập tức fd_set có thể ghi.

Tôi cảm thấy một cách trực quan, cách tiếp cận thứ hai (với tín hiệu) là cách 'chính xác nhất' để lập trình máy chủ, nhưng tôi tò mò nếu có ai biết điều nào ở trên là hiệu quả nhất, nói chung, cho dù một trong những điều trên sẽ gây ra điều kiện chủng tộc mà tôi không biết, hoặc nếu có ai biết về một giải pháp chung chung hơn cho vấn đề này. Những gì tôi thực sự muốn là một hàm pthread_cond_wait_and_select() cho phép thread comm đợi cả hai thay đổi trong socket hoặc tín hiệu từ một điều kiện.

Xin cảm ơn trước.

Trả lời

6

Đây là vấn đề khá phổ biến.

Một giải pháp thường được sử dụng là có đường ống như một cơ chế truyền thông từ các chuỗi công nhân trở lại luồng I/O. Sau khi hoàn thành nhiệm vụ của mình một luồng công nhân ghi con trỏ vào kết quả vào đường ống. Chủ đề I/O đợi trên đầu đọc của đường ống cùng với các ổ cắm khác và các bộ mô tả tập tin và một khi đường ống đã sẵn sàng để đọc nó tỉnh dậy, lấy con trỏ đến kết quả và tiếp tục đẩy kết quả vào kết nối máy khách trong không chế độ blocking.

Lưu ý rằng khi đọc và viết ống ít hơn hoặc bằng PIPE_BUF là nguyên tử, con trỏ sẽ được viết và đọc trong một lần chụp. Một thậm chí có thể có nhiều chủ đề công nhân viết con trỏ vào cùng một đường ống vì bảo đảm nguyên tử.

3

Cách tiếp cận thứ hai của bạn là cách dọn sạch hơn. Hoàn toàn bình thường khi có những thứ như select hoặc epoll bao gồm các sự kiện tùy chỉnh trong danh sách của bạn. Đây là những gì chúng tôi làm trong dự án hiện tại của tôi để xử lý các sự kiện như vậy. Chúng tôi cũng sử dụng bộ hẹn giờ (trên Linux timerfd_create) cho các sự kiện định kỳ.

Trên Linux, eventfd cho phép bạn tạo các sự kiện tùy ý như vậy cho mục đích này - vì vậy tôi cho rằng đó là thực tế được chấp nhận. Đối với các chức năng chỉ POSIX, tốt, hmm, có lẽ một trong các lệnh ống hoặc socketpair Tôi cũng đã thấy.

Bận rộn không phải là lựa chọn tốt.Trước tiên, bạn sẽ quét bộ nhớ sẽ được sử dụng bởi các chủ đề khác, do đó gây ra sự tranh cãi về bộ nhớ CPU. Thứ hai, bạn sẽ luôn phải quay lại cuộc gọi select sẽ tạo ra một số lượng lớn các cuộc gọi hệ thống và các công tắc ngữ cảnh sẽ làm tổn hại đến hiệu năng hệ thống tổng thể.

3

Thật không may, cách tốt nhất để làm điều này là khác nhau cho mỗi nền tảng. Cách kinh điển, di động để thực hiện việc này là có khối chuỗi I/O trong poll. Nếu bạn cần lấy chuỗi I/O để rời khỏi poll, bạn gửi một byte đơn lẻ trên một số pipe mà chuỗi đó đang bỏ phiếu. Điều đó sẽ khiến cho chuỗi thoát khỏi số poll ngay lập tức.

Trên Linux, epoll là cách tốt nhất. Trên các hệ điều hành có nguồn gốc BSD (bao gồm OSX, tôi nghĩ), kqueue. Trên Solaris, nó từng là /dev/poll và có một cái gì đó khác bây giờ có tên tôi quên.

Bạn có thể chỉ muốn xem xét sử dụng thư viện như libevent hoặc Boost.Asio. Chúng cung cấp cho bạn mô hình I/O tốt nhất trên mỗi nền tảng mà họ hỗ trợ.

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