Đa luồng trong C++ sử dụng Pthreads

Đa luồng trong C++ sử dụng Pthreads

Đa luồng (multithreading) là một kỹ thuật nâng cao trong lập trình, cho phép một chương trình máy tính thực hiện nhiều tác vụ cùng lúc. Nó là một hình thức đặc biệt của đa nhiệm (multitasking), trong đó máy tính có thể chạy đồng thời hai hoặc nhiều chương trình. Chúng ta thường phân biệt hai loại đa nhiệm chính: dựa trên tiến trình (process-based) và dựa trên luồng (thread-based).
  • Đa nhiệm dựa trên tiến trình là việc thực thi song song của nhiều chương trình độc lập.
  • Đa nhiệm dựa trên luồng là việc thực thi đồng thời các phân đoạn khác nhau bên trong cùng một chương trình.
Một chương trình đa luồng chứa hai hoặc nhiều phần có thể chạy đồng thời. Mỗi phần như vậy được gọi là một luồng (thread), và mỗi luồng định nghĩa một đường dẫn thực thi độc lập. Để minh họa các khái niệm này, chúng ta sẽ tập trung vào cách viết các chương trình đa luồng C++ sử dụng thư viện POSIX Threads, hay còn gọi là Pthreads. Pthreads cung cấp một API chuẩn, có sẵn trên nhiều hệ thống tương thích POSIX như GNU/Linux, FreeBSD, NetBSD, macOS và Solaris.

Khởi tạo Luồng

Để tạo một luồng POSIX mới, chúng ta sử dụng hàm `pthread_create`:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
Hàm `pthread_create` chịu trách nhiệm tạo một luồng mới và đưa nó vào trạng thái có thể thực thi. Dưới đây là mô tả chi tiết các tham số:
Tham số Mô tả
thread Con trỏ tới một biến kiểu pthread_t, sẽ lưu trữ định danh của luồng mới được tạo.
attr Một con trỏ tới đối tượng thuộc tính luồng (pthread_attr_t). Bạn có thể sử dụng đối tượng này để cấu hình các thuộc tính của luồng như trạng thái tách rời (detached state), kích thước stack, v.v. Nếu không muốn thiết lập thuộc tính cụ thể, hãy truyền NULL để sử dụng các giá trị mặc định.
start_routine Địa chỉ của hàm mà luồng mới sẽ bắt đầu thực thi. Hàm này phải có chữ ký void* (*)(void*).
arg Một con trỏ kiểu void* đến đối số mà bạn muốn truyền vào hàm start_routine của luồng. Nếu không cần truyền tham số, bạn có thể sử dụng NULL.
Hàm trả về 0 nếu luồng được tạo thành công, và một giá trị khác 0 nếu có lỗi xảy ra.

Kết thúc Luồng

Để một luồng tự kết thúc hoặc được kết thúc một cách rõ ràng, chúng ta sử dụng hàm `pthread_exit`:
#include <pthread.h>
void pthread_exit(void *status);
Hàm `pthread_exit` được gọi để kết thúc luồng hiện tại một cách tường minh. Thường thì hàm này được gọi khi luồng đã hoàn thành công việc và không cần tiếp tục tồn tại. Nếu hàm `main()` của chương trình kết thúc trước các luồng con mà nó tạo ra và sử dụng `pthread_exit()`, các luồng con sẽ tiếp tục thực thi. Ngược lại, nếu `main()` không gọi `pthread_exit()` và kết thúc, các luồng con có thể bị dừng đột ngột.

Ví dụ 1: Tạo nhiều luồng cơ bản

Ví dụ đơn giản dưới đây minh họa cách tạo 5 luồng khác nhau. Mỗi luồng sẽ in ra một thông báo chào mừng.
#include <iostream>
#include <pthread.h> // Thư viện Pthreads

// Số lượng luồng cần tạo
#define SO_LUONG_LUONG 5

// Hàm thực thi cho mỗi luồng
void* thuc_hien_cong_viec(void* du_lieu_luong) {
    std::cout << "Chào thế giới từ một luồng!" << std::endl;
    return nullptr; // Luồng kết thúc và trả về NULL
}

int main() {
    // Mảng để lưu trữ ID của các luồng
    pthread_t cac_id_luong[SO_LUONG_LUONG];

    for (int i = 0; i < SO_LUONG_LUONG; ++i) {
        // Tạo một luồng mới
        // Tham số: con trỏ ID luồng, thuộc tính (NULL cho mặc định), hàm bắt đầu, đối số hàm (NULL)
        int ma_loi = pthread_create(&cac_id_luong[i], nullptr, thuc_hien_cong_viec, nullptr);
        if (ma_loi != 0) {
            std::cerr << "Lỗi khi tạo luồng: mã lỗi = " << ma_loi << std::endl;
        }
    }

    // Chờ tất cả các luồng hoàn thành trước khi tiến trình chính kết thúc
    // Nếu không có dòng này, main có thể kết thúc sớm, đóng các luồng con
    pthread_exit(nullptr);
    return 0; // Sẽ không bao giờ đạt được nếu pthread_exit được gọi
}
Để biên dịch chương trình này, bạn cần liên kết với thư viện Pthreads:
g++ ten_file_cua_ban.cpp -lpthread -o chuong_trinh_da_luong
Sau khi biên dịch, bạn có thể chạy chương trình:
./chuong_trinh_da_luong
Kết quả đầu ra sẽ tương tự như sau:
Chào thế giới từ một luồng!
Chào thế giới từ một luồng!
Chào thế giới từ một luồng!
Chào thế giới từ một luồng!
Chào thế giới từ một luồng!

Ví dụ 2: Truyền tham số cho luồng

Ví dụ này minh họa cách truyền một giá trị số nguyên đơn lẻ làm tham số cho một luồng. Mỗi luồng sẽ in ra ID mà nó nhận được.
#include <iostream>
#include <cstdlib>   // Dùng cho exit
#include <pthread.h>

#define TONG_SO_LUONG 5

// Hàm mà luồng sẽ thực thi, nhận một con trỏ void*
void *hien_thi_thong_diep(void *du_lieu_luong) {
    // Ép kiểu con trỏ void* thành con trỏ int*, sau đó giải tham chiếu để lấy giá trị
    int id_luong_hien_tai = *((int*)du_lieu_luong);
    std::cout << "Xin chào từ luồng có ID: " << id_luong_hien_tai << std::endl;
    pthread_exit(nullptr); // Kết thúc luồng hiện tại
}

int main() {
    pthread_t mang_luong[TONG_SO_LUONG]; // Mảng chứa các ID luồng
    int cac_chi_so_luong[TONG_SO_LUONG]; // Mảng để lưu trữ chỉ số cho mỗi luồng
    int ma_ket_qua; // Mã lỗi từ pthread_create

    for (int i = 0; i < TONG_SO_LUONG; ++i) {
        std::cout << "main() : Đang tạo luồng với chỉ số " << i << std::endl;
        cac_chi_so_luong[i] = i; // Lưu giá trị của i vào một vị trí riêng biệt cho mỗi luồng

        // Tạo luồng. Đối số được truyền là địa chỉ của phần tử trong mảng cac_chi_so_luong
        ma_ket_qua = pthread_create(&mang_luong[i], nullptr, hien_thi_thong_diep, (void *)&cac_chi_so_luong[i]);

        if (ma_ket_qua != 0) {
            std::cerr << "Lỗi: Không thể tạo luồng, mã lỗi = " << ma_ket_qua << std::endl;
            exit(EXIT_FAILURE); // Thoát chương trình nếu có lỗi nghiêm trọng
        }
    }

    pthread_exit(nullptr); // Chờ tất cả các luồng con hoàn thành
    return 0; // Sẽ không bao giờ đạt được
}
Biên dịch và chạy chương trình này tương tự như ví dụ trước. Bạn sẽ thấy mỗi luồng in ra ID riêng của nó:
g++ ten_file_cua_ban.cpp -lpthread -o chuong_trinh_tham_so
./chuong_trinh_tham_so
Kết quả đầu ra (thứ tự có thể thay đổi do tính chất đa luồng):
main() : Đang tạo luồng với chỉ số 0
main() : Đang tạo luồng với chỉ số 1
Xin chào từ luồng có ID: 0
main() : Đang tạo luồng với chỉ số 2
Xin chào từ luồng có ID: 1
main() : Đang tạo luồng với chỉ số 3
Xin chào từ luồng có ID: 2
main() : Đang tạo luồng với chỉ số 4
Xin chào từ luồng có ID: 3
Xin chào từ luồng có ID: 4

Truyền nhiều tham số cho luồng

Khi cần truyền nhiều tham số cho một luồng, cách phổ biến là đóng gói chúng vào một cấu trúc (struct) và truyền con trỏ tới cấu trúc đó.
#include <iostream>
#include <cstdlib>
#include <pthread.h>

#define SO_LUONG_LUONG_MOI 5

// Định nghĩa cấu trúc để chứa dữ liệu truyền cho luồng
struct DuLieuLuong {
    int id_phan_luong;
    const char *chuoi_tin_nhan;
};

// Hàm mà luồng sẽ thực thi, nhận con trỏ tới cấu trúc DuLieuLuong
void *xu_ly_du_lieu_luong(void *arg_luong) {
    // Ép kiểu con trỏ void* sang con trỏ kiểu DuLieuLuong*
    DuLieuLuong *du_lieu_cua_toi = static_cast<DuLieuLuong*>(arg_luong);

    std::cout << "ID của phân luồng: " << du_lieu_cua_toi->id_phan_luong;
    std::cout << ", Tin nhắn: " << du_lieu_cua_toi->chuoi_tin_nhan << std::endl;

    pthread_exit(nullptr);
}

int main() {
    pthread_t mang_id_luong[SO_LUONG_LUONG_MOI];
    DuLieuLuong mang_du_lieu_luong[SO_LUONG_LUONG_MOI]; // Mảng các cấu trúc dữ liệu
    int ma_kq; // Mã kết quả từ pthread_create

    for (int i = 0; i < SO_LUONG_LUONG_MOI; ++i) {
        std::cout << "main() : Đang khởi tạo luồng, chỉ số " << i << std::endl;
        
        // Gán giá trị cho các thành viên của cấu trúc
        mang_du_lieu_luong[i].id_phan_luong = i;
        mang_du_lieu_luong[i].chuoi_tin_nhan = "Đây là thông điệp tùy chỉnh";

        // Tạo luồng, truyền địa chỉ của cấu trúc làm đối số
        ma_kq = pthread_create(&mang_id_luong[i], nullptr, xu_ly_du_lieu_luong, (void *)&mang_du_lieu_luong[i]);

        if (ma_kq != 0) {
            std::cerr << "Lỗi: Không thể tạo luồng, mã lỗi = " << ma_kq << std::endl;
            exit(EXIT_FAILURE);
        }
    }

    pthread_exit(nullptr); // Chờ các luồng con hoàn tất
    return 0; // Sẽ không đạt được
}
Biên dịch và chạy chương trình này sẽ cho kết quả tương tự, với mỗi luồng hiển thị ID và thông báo riêng:
g++ -Wno-write-strings ten_file_cua_ban.cpp -lpthread -o chuong_trinh_nhieu_tham_so
./chuong_trinh_nhieu_tham_so
Kết quả đầu ra (thứ tự có thể thay đổi):
main() : Đang khởi tạo luồng, chỉ số 0
main() : Đang khởi tạo luồng, chỉ số 1
ID của phân luồng: 0, Tin nhắn: Đây là thông điệp tùy chỉnh
main() : Đang khởi tạo luồng, chỉ số 2
ID của phân luồng: 1, Tin nhắn: Đây là thông điệp tùy chỉnh
main() : Đang khởi tạo luồng, chỉ số 3
ID của phân luồng: 2, Tin nhắn: Đây là thông điệp tùy chỉnh
main() : Đang khởi tạo luồng, chỉ số 4
ID của phân luồng: 3, Tin nhắn: Đây là thông điệp tùy chỉnh
ID của phân luồng: 4, Tin nhắn: Đây là thông điệp tùy chỉnh

Kết nối và Tách luồng

Trong Pthreads, chúng ta có thể kiểm soát vòng đời của luồng bằng hai hàm chính: `pthread_join` và `pthread_detach`.
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
* Hàm `pthread_join()` khiến luồng gọi (ví dụ: luồng `main` hoặc một luồng khác) phải chờ đợi cho đến khi luồng được chỉ định bởi `thread` kết thúc thực thi. Khi một luồng được tạo, một trong các thuộc tính của nó sẽ xác định liệu nó có thể được "kết nối" (joinable) hay "tách rời" (detached). Chỉ những luồng được định nghĩa là "joinable" mới có thể được chờ bằng `pthread_join()`. * Hàm `pthread_detach()` được dùng để tách một luồng. Một luồng "detached" không thể được `join()` bởi bất kỳ luồng nào khác, và tài nguyên của nó sẽ được tự động giải phóng khi nó kết thúc.

Ví dụ 3: Sử dụng `pthread_join` để chờ luồng

Ví dụ này minh họa cách sử dụng `pthread_join()` để đợi tất cả các luồng con hoàn thành công việc của chúng.
#include <iostream>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h> // Để sử dụng hàm sleep()
#include <cstdint>  // Để sử dụng intptr_t cho truyền dữ liệu qua void*

#define TONG_LUONG_JOINABLE 5

// Hàm thực thi cho luồng con
void *thoi_gian_cho(void *arg_luong) {
    // Ép kiểu con trỏ void* thành intptr_t để lấy giá trị ID
    int id_luong_hien_tai = static_cast<int>(reinterpret_cast<intptr_t>(arg_luong));
    
    std::cout << "Luồng ID " << id_luong_hien_tai << ": Đang tạm dừng trong 1 giây..." << std::endl;
    sleep(1); // Tạm dừng luồng trong 1 giây
    
    std::cout << "Luồng ID " << id_luong_hien_tai << ": Đã hoàn thành và thoát." << std::endl;
    pthread_exit(nullptr); // Luồng kết thúc, trả về nullptr (0)
}

int main() {
    pthread_t cac_luong_con[TONG_LUONG_JOINABLE];
    pthread_attr_t thuoc_tinh_luong; // Đối tượng thuộc tính luồng
    void *trang_thai_ket_thuc; // Biến lưu trạng thái kết thúc của luồng con

    // Khởi tạo đối tượng thuộc tính luồng
    pthread_attr_init(&thuoc_tinh_luong);
    // Đặt thuộc tính cho luồng là joinable (có thể chờ được)
    pthread_attr_setdetachstate(&thuoc_tinh_luong, PTHREAD_CREATE_JOINABLE);

    // Mảng để lưu trữ ID truyền cho mỗi luồng
    int mang_id_truyen[TONG_LUONG_JOINABLE]; 

    for (int i = 0; i < TONG_LUONG_JOINABLE; ++i) {
        std::cout << "main() : Đang tạo luồng, chỉ số " << i << std::endl;
        mang_id_truyen[i] = i; // Lưu ID riêng cho mỗi luồng
        
        // Tạo luồng. Đối số được truyền là địa chỉ của phần tử trong mảng mang_id_truyen
        int ma_kq = pthread_create(&cac_luong_con[i], &thuoc_tinh_luong, thoi_gian_cho, (void*)&mang_id_truyen[i]);
        if (ma_kq != 0) {
            std::cerr << "Lỗi: Không thể tạo luồng, mã lỗi = " << ma_kq << std::endl;
            exit(EXIT_FAILURE);
        }
    }

    // Hủy đối tượng thuộc tính sau khi đã tạo xong tất cả các luồng
    pthread_attr_destroy(&thuoc_tinh_luong);

    // Chờ tất cả các luồng con hoàn thành
    for (int i = 0; i < TONG_LUONG_JOINABLE; ++i) {
        int ma_kq = pthread_join(cac_luong_con[i], &trang_thai_ket_thuc);
        if (ma_kq != 0) {
            std::cerr << "Lỗi: Không thể chờ luồng, mã lỗi = " << ma_kq << std::endl;
            exit(EXIT_FAILURE);
        }
        std::cout << "main: Luồng ID " << i << " đã hoàn tất với trạng thái: " << trang_thai_ket_thuc << std::endl;
    }

    std::cout << "main: Chương trình chính kết thúc." << std::endl;
    pthread_exit(nullptr); // Kết thúc luồng chính một cách rõ ràng
    return 0; // Sẽ không đạt được
}
Biên dịch và chạy chương trình:
g++ ten_file_cua_ban.cpp -lpthread -o chuong_trinh_join
./chuong_trinh_join
Kết quả đầu ra sẽ hiển thị các luồng con thực thi và luồng chính chờ đợi từng luồng một:
main() : Đang tạo luồng, chỉ số 0
main() : Đang tạo luồng, chỉ số 1
main() : Đang tạo luồng, chỉ số 2
main() : Đang tạo luồng, chỉ số 3
main() : Đang tạo luồng, chỉ số 4
Luồng ID 0: Đang tạm dừng trong 1 giây...
Luồng ID 1: Đang tạm dừng trong 1 giây...
Luồng ID 2: Đang tạm dừng trong 1 giây...
Luồng ID 3: Đang tạm dừng trong 1 giây...
Luồng ID 4: Đang tạm dừng trong 1 giây...
Luồng ID 4: Đã hoàn thành và thoát.
Luồng ID 3: Đã hoàn thành và thoát.
Luồng ID 2: Đã hoàn thành và thoát.
Luồng ID 1: Đã hoàn thành và thoát.
Luồng ID 0: Đã hoàn thành và thoát.
main: Luồng ID 0 đã hoàn tất với trạng thái: 0x0
main: Luồng ID 1 đã hoàn tất với trạng thái: 0x0
main: Luồng ID 2 đã hoàn tất với trạng thái: 0x0
main: Luồng ID 3 đã hoàn tất với trạng thái: 0x0
main: Luồng ID 4 đã hoàn tất với trạng thái: 0x0
main: Chương trình chính kết thúc.

Thẻ: C++ Đa luồng Pthreads lập trình hệ thống POSIX

Đăng vào ngày 18 tháng 10 lúc 11:34