Các vấn đề khi chia sẻ dữ liệu giữa các luồng
Khi làm việc với nhiều luồng, việc chia sẻ dữ liệu thường dẫn đến các vấn đề liên quan đến sự thay đổi của dữ liệu. Một khái niệm quan trọng được sử dụng để phân tích tình huống này là "điều kiện bất biến" (invariant). Điều kiện bất biến là một khẳng định luôn đúng đối với một tập dữ liệu cụ thể.
Ví dụ: Khi thực hiện xóa một nút từ danh sách liên kết hai chiều, nếu chỉ cập nhật một phần con trỏ trước khi hoàn tất toàn bộ quá trình, điều kiện bất biến sẽ bị phá vỡ cho đến khi mọi thao tác đều hoàn thành.
Bảo vệ dữ liệu chia sẻ bằng mutex
Một cách phổ biến để bảo vệ dữ liệu khỏi các vấn đề đồng thời là sử dụng mutex. Mutex đảm bảo rằng chỉ có một luồng có thể truy cập vào dữ liệu tại cùng một thời điểm.
Ví dụ sau đây minh họa cách sử dụng mutex để bảo vệ danh sách:
#include <list>
#include <mutex>
std::list<int> shared_list;
std::mutex list_mutex;
void add_element(int value) {
std::lock_guard<std::mutex> guard(list_mutex);
shared_list.push_back(value);
}
bool contains_element(int value) {
std::lock_guard<std::mutex> guard(list_mutex);
return std::find(shared_list.begin(), shared_list.end(), value) != shared_list.end();
}
Cách tiếp cận khác để bảo vệ dữ liệu
Bên cạnh việc sử dụng mutex, còn có những công cụ khác giúp bảo vệ dữ liệu chia sẻ:
Đối tượng std::once_flag và hàm std::call_once: Được sử dụng khi cần bảo vệ dữ liệu trong quá trình khởi tạo mà không yêu cầu khóa liên tục sau đó. Đọc-ghi mutex (read-write mutex): Cho phép nhiều luồng đọc đồng thời nhưng chỉ cho phép một luồng viết độc quyền.
Ví dụ về việc sử dụng đọc-ghi mutex:
#include <map>
#include <shared_mutex>
class SharedData {
private:
std::map<int, std::string> data_map;
mutable std::shared_mutex data_mutex;
public:
std::string get_value(int key) const {
std::shared_lock<std::shared_mutex> lock(data_mutex);
auto it = data_map.find(key);
return (it != data_map.end()) ? it->second : "";
}
void set_value(int key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(data_mutex);
data_map[key] = value;
}
};
Lưu ý về hiệu suất
Khi thiết kế hệ thống đa luồng, cần cân nhắc kỹ lưỡng mức độ chi tiết của việc khóa (lock granularity). Khóa quá lớn có thể gây ra contention cao, trong khi khóa quá nhỏ có thể dẫn đến race condition.
Ngoài ra, cần tránh thực hiện các thao tác tốn thời gian như đọc/ghi file trong khi đang giữ khóa, vì điều này có thể làm giảm hiệu suất tổng thể của ứng dụng.