1. Luồng thực thi là gì?
Trong kernel Linux không có khái niệm thread, mà có khái niệm tiến trình nhẹ (LWP - Lightweight Process). Khái niệm thread mà chúng ta thường nói đến thực chất nằm ở thư viện C.
1.1 Cách thức mô tả luồng
Luồng thực chất cũng là một cấu trúc task_struct. Luồng công việc sao chép task_struct của luồng chính và chia sẻ mm_struct. ID luồng được mô tả bằng pid trong task_struct, trong khi tgid trong task_struct là ID nhóm luồng, cho biết luồng thuộc nhóm nào. Đối với luồng chính, pid và tgid giống nhau.
1.2 Cách xem ID luồng
Sử dụng lệnh: ps -eLf để xem các luồng của một tiến trình.
1.3 Đa luồng tránh rối loạn stack gọi như thế nào?
Luồng công việc và luồng chính chia sẻ mm_struct, nếu cả hai cùng push vào stack sẽ gây ra lỗi. Thực tế, luồng công việc push vào vùng chia sẻ chứa nhiều tài nguyên riêng của luồng. Mỗi luồng mặc định chiếm 8MB trong vùng chia sẻ, có thể thay đổi bằng ulimit -s.
1.3.1 Tài nguyên riêng của luồng
- ID luồng
- Tập thanh ghi
- errno
- Signal mask
- Ưu tiên lập lịch
1.3.2 Tài nguyên và môi trường chia sẻ
- Bảng mô tả file
- Cách xử lý signal
- Thư mục làm việc hiện tại
- User ID và group ID
1.4 Tại sao cần đa luồng?
Giống như ngân hàng có nhiều quầy phục vụ để tăng tốc độ xử lý.
1.4.1 Ưu điểm của luồng
- Chia sẻ không gian địa chỉ bộ nhớ
- Thời gian tạo luồng ít hơn tạo tiến trình
- Thời gian kết thúc luồng ít hơn kết thúc tiến trình
- Chi phí chuyển đổi ngữ cảnh giữa các luồng nhỏ hơn
- Chia sẻ dữ liệu giữa luồng đơn giản hơn
- Tận dụng tối đa khả năng song song của bộ xử lý đa lõi
1.4.2 Nhược điểm của luồng
- Độ bền giảm: một luồng có lỗi sẽ khiến toàn bộ tiến trình kết thúc
- Mô hình luồng phức tạp, dễ gặp lỗi và khó kiểm thử
2. Tạo luồng
2.1 Interface tạo luồng
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
2.2 Lựa chọn tham số arg
Không nên truyền biến tạm thời. Sử dụng biến được cấp phát trên heap.
2.3 ID luồng và không gian địa chỉ tiến trình
ID luồng là một địa chỉ trong không gian địa chỉ tiến trình.
3. Kết thúc luồng
Các cách kết thúc luồng mà không kết thúc tiến trình:
- Return từ hàm entry
- Gọi pthread_exit(NULL)
- Bị luồng khác hủy bằng pthread_cancel()
4. Chờ luồng
4.1 Interface chờ luồng
int pthread_join(pthread_t thread, void **retval);
4.2 Tại sao cần chờ luồng đã kết thúc?
Nếu không chờ, tài nguyên của luồng sẽ không được giải phóng, gây rò rỉ tài nguyên.
5. Tách luồng
int pthread_detach(pthread_t thread);
Luồng ở trạng thái đã tách sẽ tự động giải phóng tài nguyên khi kết thúc.
6. An toàn luồng
6.1 什么是线程不安全?
Nhiều luồng truy cập cùng một tài nguyên ngầm, gây ra trạng thái không nhất quán.
6.2 Giải pháp cho an toàn luồng
Sử dụng mutex (đối tượng loại trừ tương hỗ) để bảo vệ vùng tới hạn.
6.3 Interface mutex
6.3.1 Khởi tạo mutex
// Tĩnh
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// Động
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
6.3.2 Khóa mutex
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
6.3.3 Mở khóa mutex
int pthread_mutex_unlock(pthread_mutex_t *mutex);
7. Đồng bộ hóa luồng
7.1 Tại sao cần đồng bộ hóa luồng?
Để đảm bảo truy cập tài nguyên ngầm hợp lý.
7.2 Biến điều kiện
Biến điều kiện cho phép luồng đợi cho đến khi một điều kiện nào đó được thỏa mãn.
7.2.1 Khởi tạo biến điều kiện
// Tĩnh
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// Động
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
7.2.2 Đợi biến điều kiện
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
7.2.3 Thông báo biến điều kiện
int pthread_cond_signal(pthread_cond_t *cond); // Thông báo một luồng
int pthread_cond_broadcast(pthread_cond_t *cond); // Thông báo tất cả luồng
8. Hủy luồng
int pthread_cancel(pthread_t thread);
Một luồng có thể yêu cầu hủy luồng khác. Luồng bị hủy nên có các hàm dọn dẹp để giải phóng tài nguyên.
9. Mô hình Nhà sản xuất - Người tiêu dùng
9.1 Bản chất của mô hình
Là một hàng đợi an toàn luồng và hai loại luồng: nhà sản xuất và người tiêu dùng.
9.2 Lợi ích
- Giảm kết hợp (decoupling)
- Hỗ trợ xử lý đồng thời cao
- Balancing tải
9.3 Ví dụ triển khai
#include <pthread.h>
#include <queue>
#include <iostream>
class ProducerConsumer {
private:
std::queue<int> buffer;
int capacity;
pthread_mutex_t mutex;
pthread_cond_t notEmpty;
pthread_cond_t notFull;
public:
ProducerConsumer(int cap) : capacity(cap) {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬Empty, NULL);
pthread_cond_init(¬Full, NULL);
}
void produce(int item) {
pthread_mutex_lock(&mutex);
while (buffer.size() == capacity) {
pthread_cond_wait(¬Full, &mutex);
}
buffer.push(item);
std::cout << "Đã sản xuất: " << item << std::endl;
pthread_cond_signal(¬Empty);
pthread_mutex_unlock(&mutex);
}
int consume() {
pthread_mutex_lock(&mutex);
while (buffer.empty()) {
pthread_cond_wait(¬Empty, &mutex);
}
int item = buffer.front();
buffer.pop();
std::cout << "Đã tiêu dùng: " << item << std::endl;
pthread_cond_signal(¬Full);
pthread_mutex_unlock(&mutex);
return item;
}
};
10. Lưu ý khi lập trình đa luồng
- Xác định logic cốt lõi trước
- Xác định các tài nguyên ngầm và vùng tới hạn
- Thực hiện đồng bộ hóa khi cần thiết
- Tránh gọi fork() trong chương trình đa luồng
- Xử lý kỹ việc hủy luồng để tránh rò rỉ tài nguyên