Tôi đang cố gắng tự trả lời câu hỏi này, sau khi xem qua nhiều tài nguyên trực tuyến khác nhau (ví dụ: cái này và cái này ), Tiêu chuẩn C ++ 11, cũng như các câu trả lời được đưa ra ở đây.
Các câu hỏi liên quan được hợp nhất (ví dụ: " tại sao! Mong đợi? " Được hợp nhất với "tại sao đặt so sánh_exchange_weak () trong một vòng lặp? ") Và câu trả lời được đưa ra tương ứng.
Tại sao so sánh_exchange_weak () phải ở trong một vòng lặp trong hầu hết các mục đích sử dụng?
Mẫu điển hình A
Bạn cần đạt được bản cập nhật nguyên tử dựa trên giá trị trong biến nguyên tử. Lỗi chỉ ra rằng biến không được cập nhật với giá trị mong muốn của chúng tôi và chúng tôi muốn thử lại. Lưu ý rằng chúng tôi không thực sự quan tâm đến việc nó không thành công do ghi đồng thời hay lỗi giả. Nhưng chúng tôi quan tâm rằng chính chúng tôi là người tạo ra sự thay đổi này.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Một ví dụ trong thế giới thực là cho một số luồng để thêm đồng thời một phần tử vào một danh sách được liên kết đơn lẻ. Mỗi luồng đầu tiên tải con trỏ đầu, phân bổ một nút mới và nối phần đầu vào nút mới này. Cuối cùng, nó cố gắng hoán đổi nút mới với phần đầu.
Một ví dụ khác là sử dụng mutex std::atomic<bool>
. Tại hầu hết các một thread có thể vào đoạn găng tại một thời điểm, tùy thuộc vào thread đầu tiên thiết lập current
để true
và thoát khỏi vòng lặp.
Mẫu điển hình B
Đây thực sự là khuôn mẫu được đề cập trong cuốn sách của Anthony. Ngược lại với mẫu A, bạn muốn biến nguyên tử được cập nhật một lần, nhưng bạn không quan tâm ai sẽ làm điều đó. Miễn là nó chưa được cập nhật, bạn thử lại. Điều này thường được sử dụng với các biến boolean. Ví dụ: bạn cần triển khai một trình kích hoạt để máy trạng thái tiếp tục. Chủ đề nào kéo cò là bất chấp.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Lưu ý rằng chúng ta thường không thể sử dụng mẫu này để triển khai mutex. Nếu không, nhiều chủ đề có thể nằm trong phần quan trọng cùng một lúc.
Điều đó nói rằng, hiếm khi sử dụng compare_exchange_weak()
bên ngoài một vòng lặp. Ngược lại, có những trường hợp phiên bản mạnh đang được sử dụng. Ví dụ,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
không phù hợp ở đây vì khi nó quay trở lại do lỗi giả, có khả năng là chưa có ai chiếm giữ phần quan trọng.
Chủ đề đói?
Một điểm đáng nói là điều gì sẽ xảy ra nếu các lỗi giả tiếp tục xảy ra do đó làm chết luồng? Về mặt lý thuyết, nó có thể xảy ra trên các nền tảng khi compare_exchange_XXX()
được triển khai dưới dạng một chuỗi hướng dẫn (ví dụ: LL / SC). Việc truy cập thường xuyên vào cùng một dòng bộ nhớ cache giữa LL và SC sẽ tạo ra các lỗi giả liên tục. Một ví dụ thực tế hơn là do lập lịch câm trong đó tất cả các luồng đồng thời được xen kẽ theo cách sau.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Nó có thể xảy ra không?
Nó sẽ không xảy ra mãi mãi, may mắn thay, nhờ những gì C ++ 11 yêu cầu:
Việc triển khai phải đảm bảo rằng các hoạt động so sánh và trao đổi yếu không liên tục trả về sai trừ khi đối tượng nguyên tử có giá trị khác với mong đợi hoặc có các sửa đổi đồng thời đối với đối tượng nguyên tử.
Tại sao chúng ta lại bận tâm sử dụng Compare_exchange_weak () và tự viết vòng lặp? Chúng ta chỉ có thể sử dụng Compare_exchange_strong ().
Nó phụ thuộc.
Trường hợp 1: Khi cần sử dụng cả hai bên trong một vòng lặp. C ++ 11 nói:
Khi quá trình so sánh và trao đổi diễn ra trong vòng lặp, phiên bản yếu sẽ mang lại hiệu suất tốt hơn trên một số nền tảng.
Trên x86 (ít nhất là hiện tại. Có thể một ngày nào đó nó sẽ sử dụng một sơ đồ tương tự như LL / SC để đạt hiệu suất khi nhiều lõi hơn được giới thiệu), phiên bản yếu và mạnh về cơ bản giống nhau vì cả hai đều tập trung vào một lệnh duy nhất cmpxchg
. Trên một số nền tảng khác compare_exchange_XXX()
không được triển khai nguyên tử (ở đây có nghĩa là không có phần cứng nguyên thủy duy nhất tồn tại), phiên bản yếu bên trong vòng lặp có thể thắng cuộc chiến vì phiên bản mạnh sẽ phải xử lý các lỗi giả và thử lại cho phù hợp.
Nhưng,
hiếm khi, chúng tôi có thể thích compare_exchange_strong()
hơn compare_exchange_weak()
ngay cả trong một vòng lặp. Ví dụ: khi có nhiều việc phải làm giữa biến nguyên tử được tải và một giá trị mới được tính toán được trao đổi (xem function()
ở trên). Nếu bản thân biến nguyên tử không thay đổi thường xuyên, chúng ta không cần lặp lại phép tính tốn kém cho mọi lỗi giả. Thay vào đó, chúng tôi có thể hy vọng rằng compare_exchange_strong()
"hấp thụ" những thất bại như vậy và chúng tôi chỉ lặp lại tính toán khi nó không thành công do sự thay đổi giá trị thực.
Trường hợp 2: Khi chỉ compare_exchange_weak()
cần sử dụng bên trong một vòng lặp. C ++ 11 cũng cho biết:
Khi một so sánh và trao đổi yếu sẽ yêu cầu một vòng lặp và một so sánh mạnh thì không, nhưng một so sánh mạnh sẽ được ưu tiên hơn.
Điều này thường xảy ra khi bạn lặp lại chỉ để loại bỏ các lỗi giả từ phiên bản yếu. Bạn thử lại cho đến khi trao đổi thành công hoặc không thành công do ghi đồng thời.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Tốt nhất, đó là sáng tạo lại các bánh xe và hoạt động giống như compare_exchange_strong()
. Tệ hơn? Cách tiếp cận này không tận dụng được hết các máy cung cấp phép so sánh và trao đổi không giả mạo trong phần cứng .
Cuối cùng, nếu bạn lặp lại những thứ khác (ví dụ: xem "Mẫu điển hình A" ở trên), thì rất có thể compare_exchange_strong()
nó cũng sẽ được đưa vào một vòng lặp, điều này đưa chúng ta trở lại trường hợp trước.