Cơ chế Đồng bộ Hóa Luồng và Giao tiếp Giữa Các Tiến trình

Các cơ chế đồng bộ hóa luồng đảm bảo rằng nhiều luồng truy cập tài nguyên chung một cách an toàn, tránh xung đột dữ liệu và duy trì tính nhất quán trạng thái. Do đặc tính phi đồng bộ của hệ thống đa luồng, việc điều phối trật tự thực thi đòi hỏi các công cụ kiểm soát truy cập có chủ đích — trong đó, bán dẫn (semaphore) là một trong những nguyên thủy nền tảng.

Thao tác với Semaphore POSIX

Khởi tạo đối tượng semaphore:

sem_t resource_gate;

Khởi tạo giá trị ban đầu (cho luồng cùng tiến trình sử dụng):

sem_init(&resource_gate, 0, 1); // Giá trị khởi tạo = 1 → khóa nhị phân

Chờ (giảm giá trị):

sem_wait(&resource_gate); // Chặn nếu giá trị ≤ 0

Phát hành (tăng giá trị):

sem_post(&resource_gate); // Đánh thức luồng đang chờ (nếu có)

Dọn dẹp khi không còn sử dụng:

sem_destroy(&resource_gate);

Giao diện API Semaphore

  • Khởi tạo:
    int sem_init(sem_t *restrict sem, int pshared, unsigned int value);
      – sem: con trỏ tới cấu trúc semaphore
      – pshared: 0 → dùng chung giữa các luồng; khác 0 → dùng chung giữa các tiến trình
      – value: giá trị khởi tạo (≥ 0)
  • Chờ tài nguyên:
    int sem_wait(sem_t *sem); — chặn cho đến khi giá trị > 0, sau đó giảm đi 1
  • Giải phóng tài nguyên:
    int sem_post(sem_t *sem); — tăng giá trị lên 1, đánh thức tối đa một luồng đang chờ
  • Hủy:
    int sem_destroy(sem_t *sem); — chỉ áp dụng với semaphore được khởi tạo bằng sem_init()

Hiện tượng Deadlock và Chiến lược Phòng tránh

Deadlock xảy ra khi hai hay nhiều luồng bị kẹt vĩnh viễn do chờ đợi lẫn nhau giải phóng tài nguyên đã chiếm giữ. Bốn điều kiện cần thiết để deadlock hình thành:

  1. Tính loại trừ lẫn nhau (Mutual Exclusion): Tài nguyên không thể được chia sẻ đồng thời.
  2. Giữ và chờ (Hold and Wait): Một luồng giữ tài nguyên và đồng thời yêu cầu thêm tài nguyên khác.
  3. Không chiếm đoạt (No Preemption): Tài nguyên không thể bị thu hồi từ luồng đang giữ nó.
  4. Chờ vòng (Circular Wait): Tồn tại chuỗi luồng {P₁, P₂, ..., Pₙ} sao cho Pᵢ chờ tài nguyên mà Pᵢ₊₁ đang giữ (với Pₙ chờ P₁).

Các phương pháp giảm thiểu khả năng deadlock:

  • Áp dụng thứ tự khóa cố định trên mọi luồng (ví dụ: luôn khóa A trước B).
  • Sử dụng sem_trywait() thay vì sem_wait() để tránh chặn vô hạn.
  • Thiết kế sao cho mỗi luồng yêu cầu toàn bộ tài nguyên cần thiết trước khi bắt đầu xử lý.
  • Triển khai cơ chế phát hiện và phục hồi (timeout, rollback) trong các hệ thống phức tạp.

Giao tiếp Giữa Các Tiến trình (IPC)

Do không gian địa chỉ riêng biệt, các tiến trình không thể truy cập trực tiếp vùng nhớ của nhau → cần cơ chế IPC chuyên biệt. Phân loại theo phạm vi kết nối:

IPC trên cùng một máy chủ

  • Cơ chế truyền thống:
    Pipe vô danh (anonymous pipe): chỉ dùng giữa tiến trình cha – con.
    FIFO (named pipe): file đặc biệt trong hệ thống tệp, cho phép giao tiếp giữa bất kỳ tiến trình nào biết tên.
    Tín hiệu (signals): thông báo sự kiện đơn giản (vd: SIGUSR1), không mang dữ liệu.
  • Cơ chế IPC hiện đại (System V / POSIX):
    Bộ nhớ chia sẻ (shared memory): vùng nhớ được ánh xạ chung, tốc độ cao nhưng cần đồng bộ hóa bên ngoài.
    Hàng đợi tin nhắn (message queue): gửi/nhận gói dữ liệu có kích thước cố định hoặc biến đổi.
    Tập hợp semaphore: nhóm semaphore dùng đồng bộ hóa phức tạp hơn (vd: kiểm soát nhiều tài nguyên).

IPC giữa các máy chủ khác nhau

  • Socket mạng: Giao thức TCP/UDP qua mạng, hỗ trợ cả liên lạc cục bộ (Unix domain socket) và liên mạng (IPv4/IPv6).

Thao tác với Pipe Vô danh

Tạo pipe (trả về hai mô tả tệp):

int fd_pair[2];
if (pipe(fd_pair) == -1) { /* xử lý lỗi */ }

fd_pair[0]: đầu đọc (read end)
fd_pair[1]: đầu ghi (write end)

Ghi và đọc dữ liệu:

write(fd_pair[1], buffer, size); // Ghi vào pipe
read(fd_pair[0], buffer, size);   // Đọc từ pipe

Đóng các đầu khi hoàn tất:

close(fd_pair[0]); // Đóng đầu đọc
close(fd_pair[1]); // Đóng đầu ghi

Thao tác với FIFO (Named Pipe)

Tạo file FIFO:

if (mkfifo("/tmp/my_fifo", 0666) == -1 && errno != EEXIST) {
    perror("mkfifo failed");
}

Mở và sử dụng như file thường:

int fifo_fd = open("/tmp/my_fifo", O_RDWR); // Không chặn nếu không có tiến trình đối diện
write(fifo_fd, data, len);
read(fifo_fd, buf, sizeof(buf));
close(fifo_fd);

Xóa FIFO sau khi không còn cần thiết:

unlink("/tmp/my_fifo");

Một số Nguyên tắc Thiết kế Phần mềm Liên quan

  • Nguyên tắc Cohesion-Coupling: Thiết kế module có chức năng tập trung (high cohesion), đồng thời giảm mức độ phụ thuộc giữa các thành phần (low coupling).
  • Nguyên tắc Open/Closed: Hệ thống nên dễ mở rộng (thêm tính năng mới) mà không cần sửa đổi mã nguồn hiện hữu.
  • Khởi tạo khóa mutex toàn cục: Có thể dùng macro PTHREAD_MUTEX_INITIALIZER thay vì gọi pthread_mutex_init().
  • Hàm memset():
    void *memset(void *s, int c, size_t n);
    Dùng để gán giá trị byte c cho n byte liên tiếp bắt đầu từ địa chỉ s. Trả về s nếu thành công.
  • Phân loại kênh truyền:
    Simplex: truyền một chiều (vd: phát sóng radio).
    Half-duplex: truyền hai chiều nhưng không đồng thời (vd: walkie-talkie).
    Full-duplex: truyền/nhận đồng thời hai chiều (vd: cuộc gọi điện thoại).

Thẻ: semaphore ipc pipe fifo deadlock

Đăng vào ngày 16 tháng 05 lúc 15:07