C++ Networking 00: Căn bản

Manh

March 12, 2016

C++ Networking

1. C++ Networking

Thư viện lập trình networking Asio được tác giả Christopher Kohlhoff phát triển, cung cấp các chức năng networking và I/O cho một ứng dụng C++, có mặt trong bộ thư viện Boost (Boost.Asio).

Thư viện này cũng đã được tác giả dùng để phát triển lên bản đề xuất cho hội đồng C++ để được chuẩn hóa và xuất hiện trong các phiên bản C++ mới. Thông tin về Asio và C++ Standard các bạn có thể theo dõi qua link: http://chriskohlhoff.github.io/asio-tr2/

Những phần mềm sử dụng Asio networking đáng chú ý:

– libtorrent: BitTorrent protocol, được sử dụng trong uTorrent

– libbitcoin: thư viện phát triển bitcoin đa nền tảng

– Blue Gene/Q: networking của siêu máy tính IBM này sử dụng Boost.Asio

– PokerTH online: một game Poker mã nguồn mở

http://think-async.com/Asio/WhoIsUsingAsio

Vậy nên bạn có thể tin tưởng rằng, Asio là một thư viện networking đủ tốt để có thể sử dụng, cũng như về sau có thể nó sẽ được xuất hiện trong các chuẩn C++ mới.

2. Cài đặt Asio

Có 2 cách đê sử dụng thư viện networking này.

Một là standalone Asio, bạn chỉ cần download thư viện Asio tại think-async.com và sử dụng. Ưu điểm của cách này là dễ dàng cài đặt (chỉ cần include header là có thể biên dịch), nhược điểm là bạn phải sử dụng C++11.

Cách thứ 2 là bạn download thư viện boost (boost.org) và cài đặt. Ưu điểm là không có C++11, tuy vậy bạn phải học cách sử dụng Boost (có thể nhiều bạn đã làm quen với Boost) và thư viện boost download về khá là nặng.

Trong các bài viết, mình sẽ sử dụng standalone Asio và C++11, để tránh sự rườm rà trong khi code hơn là dùng Boost. Tuy nhiên tất cả các nguyên lý sử dụng thì không có gì khác nhau.

Mình sẽ hạn chế tối đa các đặc điểm mới của C++11 để người đọc dễ làm quen hơn.

Bạn hoàn toàn có thể sử dụng Lambda, C++11 for loop,… để code lại các ví dụ.

Hướng dẫn Boost.Asio: http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/using.html

3. Biên dịch với Standalone Asio

Để biên dịch Asio mà không dùng đến boost, bạn cần define ASIO_STANDALONE trước khi include thư viện asio, và bật chế độ biên dịch C++11 cho IDE mà bạn đang dùng.

Nếu bạn biên dịch qua command line, chỉ cần thêm -std=c++11 vào dòng lệnh.

4. Asio căn bản: io_service object

Trong quá trình sử dụng Asio, object này chính là đối tượng căn bản nhất và đóng vai trò quan trọng trong thực thi công việc của bạn. Đây là một đối tượng hỗ trợ các input/output (I/O nói theo cách đơn giản thì nó là giao tiếp giữa bên xử lý thông tin và bên nhận thông tin – có thể là người dùng hoặc là một bên xử lý thông tin khác).

io_service là một thread-safe object, nên bạn có thể sử dụng (gọi các hàm của nó) từ các thread khác nhau một cách an toàn mà không cần code thêm logic.

Multi-threading với io_service:

Rất đơn giản để lập trình multi-thread với object này, chỉ cần bạn gọi hàm io_service::run() trên các thread khác nhau, object này sẽ đảm bảo những công việc mà bạn gán cho nó được thực thi trên các thread gọi io_service.

Gán công việc cho io_service.

Công việc ở đây là một Function Object.

Có 2 phương thức để gán công việc của bạn, đó là post và dispatch.

Dispatch: yêu cầu io_service thực thi ngay lập tức

Post: *đăng ký* job này với io_service để thực thi nó trên một thread khác, kết quả là hàm post sẽ return ngay lập tức cho bạn và chạy tiếp các code ở dưới mà không cần đợi functor thực thi xong.

OK, phần khái niệm đã tạm xong, chúng ta đến code ví dụ.

Trong bài này mình sử dụng C++11 thread, sẽ khá dễ hiểu nếu như bạn đã sử dụng thread trong các ngôn ngữ khác.

C++11 – basic threading:

#include <thread>
#include <iostream>
 
void func() {
    std::cout << "Called from a thread" << std::endl;
}
 
int main() {
    std::thread t(&func); // spawns new thread and returns immediately, main program continues
 
    // more code here
 
    t.join(); // waits for thread to finish
    return 0;
}

Hàm join của thread sẽ block chương trình cho đến khi nào thread đã hoàn thành và bị hủy. Tất nhiên bạn có thể join một thread ở bất kỳ đâu trong chương trình, chỉ cần đảm bảo rằng code của bạn sẽ không phải đợi thread do đã join quá sớm. Trong trường hợp code của bạn không cần phụ thuộc vào sự thực thi của thread, hoặc bạn cần 1 thread chạy độc lập, thì bạn có thể gọi std::thread::detach() ngay sau khi tạo thread, thread đó sẽ chạy tách biệt và tự thu hồi khi main thread (chạy chương trình chính) bị đóng.

std::bind

Hàm std::bind của thư viện STL có nhiệm vụ wrap một hàm/con trỏ hàm thành một Function Object.

#include <thread>
#include <iostream>
 
void func(int num) {
    std::cout << "Called by thread " << num << std::endl;
}
 
int main() {
    std::thread t(std::bind(&func, 1));
 
    t.join();
    return 0;
}

Cú pháp sử dụng: std::bind(địa_chỉ_của_hàm, danh_sách_tham_số)

Hoặc bạn có thể viết một function object và truyền vào hàm khởi tạo của thread.

Using io_service object

Bạn sẽ thấy đoạn code sau khó hiểu khi mới bắt đầu, nhưng hãy để ý rằng các function object được post cho io_service và thực thi trên các thread khác nhau, chúng ta không đưa một công việc cụ thể nào cho thread ngoài việc gọi hàm run của io_service cả.

Mình sử dụng thêm thư viện chrono của C++11 để cho các thread dừng 1 giây sau khi print.

#define ASIO_STANDALONE // define trước khi include asio header hoặc thêm switch -D cho g++
#include <asio.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
 
using namespace asio;
 
void print(const std::string& s) {
    while (true) {
        std::cout << s << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }   
}
 
void run(io_service* service) {
    service->run();
}
 
int main() {
    io_service service;
 
    // post your work to an io_service object
    service.post(std::bind(&print, "I'm thread A"));
    service.post(std::bind(&print, "I'm thread B"));
 
    std::thread t1(std::bind(&run, &service));
    std::thread t2(std::bind(&run, &service));
 
    t1.join();
    t2.join();
    return 0;
}

Khi chạy đoạn code này, bạn sẽ thấy 2 thread sẽ liên tục print ra màn hình.

Nhưng bạn sẽ thấy kết quả có vấn đề không ổn ở đây:

Output stream của chúng ta đang dùng nó không thread-safe cho lắm. Trong lúc thread A đang sử dụng std::cout để print, thread B có thể chen vào, điều đó khiến thread A chưa kịp print ký tự xuống dòng thì thread B print ngay sau đó, và kết quả là chúng ta có 2 câu ở trên 1 dòng và 1 ký tự xuống dòng bị tụt ra phía sau tạo nên một dòng trống.

Làm sao để khắc phục vấn đề này?

Well, thường thì chúng ta sẽ nghĩ ngay đến mutex. Nhưng bạn hãy xem tiếp mục sau đây.

5. Strand object

(Multi-threading without mutexes)

Sử dụng mutex vẫn sẽ có khả năng rằng chương trình của bạn xảy ra dead-lock, các threads tự khóa lẫn nhau dẫn tới không có cách nào khác ngoài kill chương trình. Strand object có thể đảm bảo rằng dead-lock không bao giờ xảy ra.

Nguyên lý: Các functor được wrap bởi cùng một strand object sẽ không bao giờ được thực thi cùng một lúc (bất kể chạy trên thread nào).

Strand object có thể tăng tốc độ thực thi: một thread sẽ tạm dừng nếu gặp một mutex đang bị khóa, nhưng với Strand Object, thread đó sẽ thực hiện những job khác của io_service mà không phải đợi thread kia mở khóa cho nó.

Ở đoạn code dưới đây, mình tạo ra 3 job, 2 job print và 1 job phụ.

Trong lúc thread A thực hiện job print, thread B bị khóa (không thể thực thi job 2) nên nó sẽ thực thi job 3. Sau khi job 1 và 3 chạy xong, một thread (có thể là A, cũng có thể là B) thực thi job 2, nếu giá trị của biến a đã thay đổi, job này sẽ return mà không print (để kiểm tra rằng trước đó job phụ đã được thực thi đồng thời trong lúc print bởi một thread nào đó)

#define ASIO_STANDALONE
#include <asio.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
 
using namespace asio;
 
int a = 0;
 
void print(const std::string& s) {
    if (a != 0)
        return;
 
    for (int i = 0; i < 3; ++i) {
        std::cout << s << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }   
}
 
void other_work() {
    for (int i = 0; i < 11; ++i) {
        ++a;
    }
}
 
void run(io_service* service) {
    service->run();
}
 
int main() {
    io_service service;
    io_service::strand st(service);
 
    // three jobs
    // wraps the first two jobs using the same strand object
    service.post(st.wrap(std::bind(&print, "I'm thread A")));
    service.post(st.wrap(std::bind(&print, "I'm thread B")));
    service.post(std::bind(&other_work));
 
    std::thread t1(std::bind(&run, &service));
    std::thread t2(std::bind(&run, &service));
 
    t1.join();
    t2.join();
 
    std::cout << "Result: " << a << std::endl;
    return 0;
}

Comments

Related Posts

C++ Networking 06: Network message

Manh

December 13, 2016

C++ Networking

Thiết kế network message Comments

Read More

C++ Networking 05: Stream buffer

Manh

December 12, 2016

C++ Networking

Sử dụng Asio stream buffer Comments

Read More