Hãy bắt đầu với một đoạn mã:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
Tôi đã đặt một số bí danh kiểu khá gợi ý mà chúng ta sẽ không thực sự tận dụng trong C ++ 11, nhưng trở nên hữu ích hơn nhiều trong C ++ 14. Hãy kiên nhẫn, chúng tôi sẽ đến đó.
Câu hỏi của bạn tổng hợp thành:
Làm cách nào để viết hàm tạo chuyển và toán tử gán di chuyển cho lớp này?
Chúng ta sẽ bắt đầu với hàm tạo di chuyển.
Move Constructor
Lưu ý rằng thành viên mutex
đã được thực hiện mutable
. Nói một cách chính xác thì điều này không cần thiết đối với các thành viên di chuyển, nhưng tôi cho rằng bạn cũng muốn các thành viên sao chép. Nếu không phải như vậy, không cần thiết phải tạo mutex mutable
.
Khi thi công A
không cần khóa this->mut_
. Nhưng bạn cần phải khóa mut_
đối tượng mà bạn đang xây dựng (di chuyển hoặc sao chép). Điều này có thể được thực hiện như vậy:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Lưu ý rằng chúng tôi phải xây dựng mặc định các thành viên của this
đầu tiên, và sau đó chỉ định giá trị cho chúng sau khi a.mut_
bị khóa.
Chuyển nhiệm vụ
Toán tử gán di chuyển về cơ bản phức tạp hơn nhiều vì bạn không biết liệu một số luồng khác có đang truy cập lhs hoặc rhs của biểu thức gán hay không. Và nói chung, bạn cần đề phòng trường hợp sau:
x = std::move(y);
y = std::move(x);
Đây là toán tử gán di chuyển bảo vệ chính xác tình huống trên:
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
Lưu ý rằng người ta phải sử dụng std::lock(m1, m2)
để khóa hai mutex, thay vì chỉ khóa chúng lần lượt. Nếu bạn khóa chúng lần lượt, sau đó khi hai luồng gán hai đối tượng theo thứ tự ngược lại như hình trên, bạn có thể nhận được một deadlock. Vấn đề std::lock
là tránh bế tắc đó.
Copy Constructor
Bạn đã không hỏi về các thành viên sao chép, nhưng chúng ta cũng có thể nói về họ ngay bây giờ (nếu không phải bạn, ai đó sẽ cần họ).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
Hàm tạo bản sao trông giống như hàm tạo di chuyển ngoại trừ ReadLock
bí danh được sử dụng thay cho WriteLock
. Hiện tại, cả hai bí danh này std::unique_lock<std::mutex>
và vì vậy nó không thực sự tạo ra bất kỳ sự khác biệt nào.
Nhưng trong C ++ 14, bạn sẽ có tùy chọn nói thế này:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Đây có thể là một sự tối ưu hóa, nhưng không chắc chắn. Bạn sẽ phải đo để xác định xem nó có đúng như vậy không. Nhưng với sự thay đổi này, người ta có thể sao chép cấu trúc từ cùng một rhs trong nhiều luồng đồng thời. Giải pháp C ++ 11 buộc bạn phải thực hiện tuần tự các luồng như vậy, ngay cả khi rhs không được sửa đổi.
Sao chép bài tập
Để hoàn chỉnh, đây là toán tử gán bản sao, sẽ khá tự giải thích sau khi đọc về mọi thứ khác:
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
Và vân vân.
Bất kỳ thành viên nào khác hoặc các chức năng miễn phí có A
trạng thái của quyền truy cập cũng sẽ cần được bảo vệ nếu bạn muốn nhiều luồng có thể gọi chúng cùng một lúc. Ví dụ, đây là swap
:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
Lưu ý rằng nếu bạn chỉ phụ thuộc vào std::swap
việc thực hiện công việc, việc khóa sẽ ở mức độ chi tiết sai, khóa và mở khóa giữa ba bước di chuyển std::swap
sẽ thực hiện nội bộ.
Thật vậy, việc suy nghĩ về nó swap
có thể cung cấp cho bạn thông tin chi tiết về API mà bạn có thể cần cung cấp cho API "an toàn cho luồng" A
, nói chung sẽ khác với API "không an toàn cho luồng" do vấn đề "khóa chi tiết".
Cũng cần lưu ý sự cần thiết phải bảo vệ chống lại "tự hoán đổi". "tự hoán đổi" phải là điều không nên. Nếu không có tự kiểm tra, một trong những sẽ khóa đệ quy cùng một mutex. Điều này cũng có thể được giải quyết mà không cần tự kiểm tra bằng cách sử dụng std::recursive_mutex
cho MutexType
.
Cập nhật
Trong các bình luận bên dưới, Yakk khá không hài lòng về việc phải tạo mặc định những thứ trong bản sao và di chuyển các hàm tạo (và anh ấy có lý). Nếu bạn cảm thấy đủ mạnh mẽ về vấn đề này, đến mức bạn sẵn sàng dành bộ nhớ cho nó, bạn có thể tránh nó như vậy:
Thêm bất kỳ loại khóa nào bạn cần làm thành viên dữ liệu. Các thành viên này phải đến trước dữ liệu đang được bảo vệ:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
Và sau đó trong các hàm tạo (ví dụ: hàm tạo bản sao) thực hiện điều này:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Rất tiếc, Yakk đã xóa bình luận của anh ấy trước khi tôi có cơ hội hoàn thành bản cập nhật này. Nhưng anh ấy xứng đáng được ghi nhận vì đã thúc đẩy vấn đề này và tìm ra giải pháp cho câu trả lời này.
Cập nhật 2
Và dyp đã đưa ra gợi ý hay này:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}