Hiểu std :: atom :: so sánh_exchange_weak () trong C ++ 11


86
bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak()là một trong những nguyên thủy so sánh-trao đổi được cung cấp trong C ++ 11. Nó yếu theo nghĩa là nó trả về false ngay cả khi giá trị của đối tượng bằng expected. Điều này là do lỗi giả trên một số nền tảng nơi một chuỗi hướng dẫn (thay vì một như trên x86) được sử dụng để triển khai nó. Trên các nền tảng như vậy, chuyển đổi ngữ cảnh, tải lại cùng một địa chỉ (hoặc dòng bộ nhớ cache) bởi một luồng khác, v.v. có thể làm lỗi nguyên thủy. Nó spuriouskhông phải là giá trị của đối tượng (không bằng expected) không hoạt động. Thay vào đó, đó là vấn đề về thời gian.

Nhưng điều làm tôi khó hiểu là những gì được nói trong Tiêu chuẩn C ++ 11 (ISO / IEC 14882),

29.6.5 .. Hậu quả của lỗi giả là gần như tất cả các cách sử dụng so sánh và trao đổi yếu sẽ ở trong một vòng lặp.

Tại sao nó phải ở trong một vòng lặp trong hầu hết các mục đích sử dụng ? Điều đó có nghĩa là chúng ta sẽ lặp lại khi nó không thành công vì lỗi giả? Nếu đúng như vậy, 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 tôi chỉ có thể sử dụng compare_exchange_strong()mà tôi nghĩ sẽ loại bỏ các lỗi giả mạo cho chúng tôi. Các trường hợp sử dụng phổ biến là compare_exchange_weak()gì?

Một câu hỏi khác có liên quan. Trong cuốn sách "C ++ Concurrency In Action", Anthony nói,

//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:

bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);

//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.

Tại sao lại !expectedcó điều kiện vòng lặp? Nó có để ngăn chặn tất cả các chủ đề có thể chết đói và không có tiến bộ trong một thời gian không?

Chỉnh sửa: (một câu hỏi cuối cùng)

Trên các nền tảng không tồn tại lệnh CAS phần cứng duy nhất, cả phiên bản yếu và mạnh đều được triển khai bằng LL / SC (như ARM, PowerPC, v.v.). Vậy có sự khác biệt nào giữa hai vòng lặp sau đây không? Tại sao, nếu có? (Đối với tôi, họ nên có hiệu suất tương tự.)

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..)) 
{ .. }

Tôi đưa ra câu hỏi cuối cùng này, tất cả các bạn đều đề cập rằng có thể có sự khác biệt về hiệu suất bên trong một vòng lặp. Nó cũng được đề cập trong Tiêu chuẩn C ++ 11 (ISO / IEC 14882):

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.

Nhưng như đã phân tích ở trên, hai phiên bản trong một vòng lặp sẽ cho hiệu suất giống nhau / tương tự nhau. Điều tôi nhớ là gì?


4
Ở câu hỏi đầu tiên, trong nhiều trường hợp, bạn cần lặp lại bất kỳ cách nào (cho dù bạn sử dụng phiên bản mạnh hay yếu), và phiên bản yếu có thể có hiệu suất tốt hơn phiên bản mạnh.
TC

2
Cả CAS yếu và mạnh đều được thực hiện "bằng cách sử dụng LL / SC", giống như cách mà cả sắp xếp bong bóng và nhanh chóng đều được thực hiện "bằng cách sử dụng hoán đổi"; nghĩa là, theo nghĩa đó là hoạt động ban đầu được sử dụng để hoàn thành nhiệm vụ. Những gì họ quấn quanh LL / SC là rất khác nhau. CAS yếu chỉ là LL / SC. CAS mạnh là LL / SC với một loạt các thứ khác.
Sneftel


@TuXiaomi với câu trả lời trong liên kết đó, tôi không thể hiểu tại sao "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" như đã nêu trong Tiêu chuẩn.
Deqing

@Deqing Đối với những người khác, Compare_exchange_weak có thể bị lỗi một cách khó hiểu do sự gián đoạn hoặc hành động của các bộ xử lý hoặc luồng khác. Trên các nền tảng đó, Compare_exchange_strong thực sự là một vòng lặp trên Compare_exchange_weak - nếu nó không thành công thì nó sẽ lặp lại. Nó có ích gì không? Có lẽ tôi sai
Tu Xiaomi

Câu trả lời:


72

Tại sao thực hiện trao đổi trong một vòng lặp?

Thông thường, bạn muốn công việc của mình được hoàn thành trước khi bạn tiếp tục, do đó, bạn đưa compare_exchange_weakvào một vòng lặp để nó cố gắng trao đổi cho đến khi thành công (tức là quay trở lại true).

Lưu ý rằng cũng compare_exchange_strongthường được sử dụng trong một vòng lặp. Nó không thất bại do lỗi giả, nhưng nó không thành công do ghi đồng thời.

Tại sao phải sử dụng weakthay vì strong?

Khá dễ dàng: Lỗi giả không xảy ra thường xuyên, vì vậy nó không phải là một tác động lớn. Mặt khác, việc chấp nhận lỗi như vậy cho phép triển khai weakphiên bản hiệu quả hơn nhiều (so với strong) trên một số nền tảng: strongphải luôn kiểm tra lỗi giả và che giấu nó. Cái này đắt quá.

Do đó, weakđược sử dụng vì nó nhanh hơn rất nhiều so với strongtrên một số nền tảng

Khi nào bạn nên sử dụng weakvà khi strongnào?

Các trạng thái tham chiếu gợi ý khi nào sử dụng weakvà khi nào sử dụng strong:

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. 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.

Vì vậy, câu trả lời dường như khá đơn giản để nhớ: Nếu bạn phải giới thiệu một vòng lặp chỉ vì lỗi giả, đừng làm điều đó; sử dụng strong. Nếu bạn có một vòng lặp, hãy sử dụng weak.

Tại sao !expectedtrong ví dụ

Nó phụ thuộc vào tình huống và ngữ nghĩa mong muốn của nó, nhưng thông thường nó không cần thiết cho sự chính xác. Bỏ qua nó sẽ mang lại một ngữ nghĩa rất giống nhau. Chỉ trong trường hợp một chuỗi khác có thể đặt lại giá trị thành false, ngữ nghĩa có thể trở nên hơi khác (nhưng tôi không thể tìm thấy một ví dụ có ý nghĩa nào mà bạn muốn điều đó). Xem bình luận của Tony D. để được giải thích chi tiết.

Nó chỉ đơn giản là theo dõi nhanh khi một luồng khác viết true: Sau đó, chúng tôi hủy bỏ thay vì cố gắng viết truelại.

Về câu hỏi cuối cùng của bạn

Nhưng như đã phân tích ở trên, hai phiên bản trong một vòng lặp sẽ cho hiệu suất giống nhau / tương tự nhau. Điều tôi nhớ là gì?

Từ Wikipedia :

Việc triển khai thực sự của LL / SC không phải lúc nào cũng thành công nếu không có cập nhật đồng thời cho vị trí bộ nhớ được đề cập. Bất kỳ sự kiện ngoại lệ nào giữa hai hoạt động, chẳng hạn như chuyển đổi ngữ cảnh, liên kết tải khác hoặc thậm chí (trên nhiều nền tảng) hoạt động tải hoặc lưu trữ khác, sẽ khiến cửa hàng có điều kiện nhanh chóng bị lỗi. Việc triển khai cũ hơn sẽ không thành công nếu có bất kỳ bản cập nhật nào được phát qua bus bộ nhớ.

Vì vậy, ví dụ, LL / SC sẽ không thành công khi chuyển đổi ngữ cảnh. Bây giờ, phiên bản mạnh mẽ sẽ mang lại "vòng lặp nhỏ của riêng nó" để phát hiện lỗi giả mạo đó và che giấu nó bằng cách thử lại. Lưu ý rằng vòng lặp riêng này cũng phức tạp hơn vòng lặp CAS thông thường, vì nó phải phân biệt giữa lỗi giả (và che dấu nó) và lỗi do truy cập đồng thời (dẫn đến trả về bằng giá trị false). Phiên bản yếu không có vòng lặp riêng như vậy.

Vì bạn cung cấp một vòng lặp rõ ràng trong cả hai ví dụ, nên đơn giản là không cần thiết phải có vòng lặp nhỏ cho phiên bản mạnh. Do đó, trong ví dụ với strongphiên bản, việc kiểm tra lỗi được thực hiện hai lần; một lần compare_exchange_strong(phức tạp hơn vì nó phải phân biệt lỗi giả mạo và cộng dồn đồng thời) và một lần theo vòng lặp của bạn. Việc kiểm tra tốn kém này là không cần thiết và lý do tại sao weaksẽ nhanh hơn ở đây.

Cũng lưu ý rằng đối số của bạn (LL / SC) chỉ là một khả năng để thực hiện điều này. Có nhiều nền tảng hơn có các bộ hướng dẫn thậm chí khác nhau. Ngoài ra (và quan trọng hơn), lưu ý rằng std::atomicphải hỗ trợ tất cả các hoạt động cho tất cả các kiểu dữ liệu có thể có , vì vậy ngay cả khi bạn khai báo một cấu trúc mười triệu byte, bạn có thể sử dụng compare_exchangetrên này. Ngay cả khi trên một CPU có CAS, bạn không thể CAS mười triệu byte, vì vậy trình biên dịch sẽ tạo ra các lệnh khác (có thể là khóa thu được, theo sau là so sánh và hoán đổi không nguyên tử, sau đó là phát hành khóa). Bây giờ, hãy nghĩ xem có bao nhiêu điều có thể xảy ra khi hoán đổi mười triệu byte. Vì vậy, trong khi lỗi giả có thể rất hiếm đối với trao đổi 8 byte, nó có thể phổ biến hơn trong trường hợp này.

Tóm lại, C ++ cung cấp cho bạn hai ngữ nghĩa, một "nỗ lực tốt nhất" một ( weak) và một "tôi sẽ làm điều đó chắc chắn, cho dù có bao nhiêu điều tồi tệ có thể xảy ra ở giữa" một ( strong). Làm thế nào những điều này được thực hiện trên các loại dữ liệu và nền tảng khác nhau là một chủ đề hoàn toàn khác. Đừng ràng buộc mô hình tinh thần của bạn với việc triển khai trên nền tảng cụ thể của bạn; thư viện chuẩn được thiết kế để làm việc với nhiều kiến ​​trúc hơn những gì bạn có thể biết. Kết luận chung duy nhất mà chúng ta có thể rút ra là đảm bảo thành công thường khó hơn (và do đó có thể đòi hỏi phải làm việc thêm) hơn là chỉ cố gắng và chừa chỗ cho thất bại có thể xảy ra.


"Chỉ sử dụng mạnh nếu không có nghĩa là bạn có thể chịu đựng sự thất bại giả." - có thực sự là một thuật toán phân biệt giữa lỗi do ghi đồng thời và lỗi giả không? Tất cả những thứ mà tôi có thể nghĩ ra đều cho phép chúng tôi đôi khi bỏ lỡ các bản cập nhật hoặc không, trong trường hợp đó chúng tôi cần một vòng lặp.
Voo

3
@Voo: Câu trả lời đã cập nhật. Bây giờ các gợi ý từ tài liệu tham khảo được bao gồm. Có thể có một thuật toán tạo ra sự khác biệt. Ví dụ, hãy xem xét ngữ nghĩa "người ta phải cập nhật nó": Việc cập nhật thứ gì đó phải được thực hiện chính xác một lần, vì vậy một khi chúng ta thất bại do viết đồng thời, chúng ta biết người khác đã làm điều đó và chúng ta có thể hủy bỏ. Nếu chúng tôi không thành công do lỗi giả mạo, hơn là chưa có ai cập nhật nó, vì vậy chúng tôi phải thử lại.
gexicide

8
" Tại sao lại như vậy! Được mong đợi trong ví dụ? Nó không cần thiết cho sự chính xác. Bỏ qua nó sẽ mang lại cùng một ngữ nghĩa." - không phải như vậy ... nếu nói rằng trao đổi đầu tiên không thành công vì nó đã tìm thấy brồi true, thì - với expectedbây giờ true- không có && !expectednó lặp lại và thử một trao đổi khác (ngớ ngẩn) truetruecó thể "thành công" đáng kể khi phá vỡ whilevòng lặp, nhưng có thể trưng bày hành vi khác có ý nghĩa nếu btrong thời gian đó đã thay đổi trở lại false, trong trường hợp đó, vòng lặp sẽ tiếp tục và cuối cùng có thể thiết lập b true lại trước khi bị phá vỡ.
Tony Delroy

@TonyD: Đúng vậy, tôi nên làm rõ điều đó.
gexicide

Xin lỗi các bạn, tôi đã thêm một câu hỏi cuối cùng nữa;)
Eric Z

17

Tại sao nó phải ở trong một vòng lặp trong hầu hết các mục đích sử dụng ?

Bởi vì nếu bạn không lặp lại và nó không thành công thì chương trình của bạn đã không thực hiện được bất kỳ điều gì hữu ích - bạn đã không cập nhật đối tượng nguyên tử và bạn không biết giá trị hiện tại của nó là bao nhiêu (Sửa lỗi: xem bình luận bên dưới của Cameron). Nếu cuộc gọi không có tác dụng gì thì bạn nên làm gì?

Điều đó có nghĩa là chúng ta sẽ lặp lại khi nó không thành công vì lỗi giả?

Đúng.

Nếu đúng như vậy, 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 tôi chỉ có thể sử dụng Compare_exchange_strong () mà tôi nghĩ sẽ loại bỏ các lỗi giả mạo cho chúng tôi. Các trường hợp sử dụng phổ biến của Compare_exchange_weak () là gì?

Trên một số kiến ​​trúc compare_exchange_weakthì hiệu quả hơn, và các lỗi giả khá hiếm gặp, vì vậy có thể viết các thuật toán hiệu quả hơn bằng cách sử dụng dạng yếu và vòng lặp.

Nói chung, có lẽ tốt hơn nên sử dụng phiên bản mạnh thay thế nếu thuật toán của bạn không cần lặp lại, vì bạn không cần phải lo lắng về các lỗi giả mạo. Nếu nó vẫn cần lặp lại ngay cả đối với phiên bản mạnh (và nhiều thuật toán vẫn cần lặp lại), thì việc sử dụng biểu mẫu yếu có thể hiệu quả hơn trên một số nền tảng.

Tại sao lại !expectedcó điều kiện vòng lặp?

Giá trị có thể đã được đặt thành truebởi một chuỗi khác, vì vậy bạn không muốn tiếp tục lặp lại khi cố gắng đặt nó.

Biên tập:

Nhưng như đã phân tích ở trên, hai phiên bản trong một vòng lặp sẽ cho hiệu suất giống nhau / tương tự nhau. Điều tôi nhớ là gì?

Rõ ràng là trên các nền tảng có khả năng xảy ra lỗi giả thì việc triển khai compare_exchange_strongphải phức tạp hơn, để kiểm tra lỗi giả và thử lại.

Dạng yếu chỉ trả về khi lỗi giả, nó không thử lại.


2
+1 Thực tế chính xác trên tất cả các số đếm (mà Q rất cần).
Tony Delroy

Về you don't know what its current value isđiểm đầu tiên, khi một lỗi giả xảy ra, giá trị hiện tại có nên bằng giá trị mong đợi tại thời điểm đó không? Nếu không, đó sẽ là một thất bại thực sự.
Eric Z

IMO, cả phiên bản yếu và mạnh đều được triển khai bằng LL / SC trên các nền tảng không tồn tại nguyên bản phần cứng CAS nào. Vậy đối với tôi tại sao lại có sự khác biệt về hiệu suất giữa while(!compare_exchange_weak(..))while(!compare_exchange_strong(..))?
Eric Z

Xin lỗi các bạn, tôi đã thêm một câu hỏi cuối cùng.
Eric Z

1
@ Jonathan: Chỉ cần một nitpick, nhưng bạn làm biết giá trị hiện tại nếu nó không thành công spuriously (tất nhiên, cho dù đó là vẫn còn giá trị hiện tại do thời gian bạn đọc biến là vấn đề hoàn toàn khác, nhưng đó là bất kể yếu / strong). Ví dụ, tôi đã sử dụng điều này để cố gắng đặt một biến giả sử giá trị của nó là null, và nếu không thành công (hay không), hãy tiếp tục thử nhưng chỉ tùy thuộc vào giá trị thực là gì.
Cameron

17

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àycá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để truevà 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;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
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;
// !expected: if it fails spuriously, we should try again.
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.


13

Được rồi, tôi cần một hàm thực hiện dịch chuyển sang trái nguyên tử. Bộ xử lý của tôi không có hoạt động gốc cho việc này và thư viện chuẩn không có chức năng cho nó, vì vậy có vẻ như tôi đang viết của riêng mình. Đây là:

void atomicLeftShift(std::atomic<int>* var, int shiftBy)
{
    do {
        int oldVal = std::atomic_load(var);
        int newVal = oldVal << shiftBy;
    } while(!std::compare_exchange_weak(oldVal, newVal));
}

Bây giờ, có hai lý do khiến vòng lặp có thể được thực thi nhiều lần.

  1. Ai đó đã thay đổi biến trong khi tôi đang thực hiện ca sang trái của mình. Kết quả tính toán của tôi không nên được áp dụng cho biến nguyên tử, bởi vì nó sẽ xóa hiệu quả mà người khác viết.
  2. CPU của tôi bị ợ và CAS yếu nhanh chóng bị lỗi.

Tôi thành thật không quan tâm cái nào. Chuyển sang trái đủ nhanh để tôi cũng có thể thực hiện lại, ngay cả khi thất bại là giả.

Tuy nhiên, điều kém nhanh hơn là mã bổ sung mà CAS mạnh cần quấn quanh CAS yếu để trở nên mạnh mẽ. Mã đó không có tác dụng gì nhiều khi CAS yếu thành công ... nhưng khi thất bại, CAS mạnh cần thực hiện một số công việc thám tử để xác định xem đó là Trường hợp 1 hay Trường hợp 2. Công việc thám tử đó có dạng một vòng lặp thứ hai, hiệu quả bên trong vòng lặp của riêng tôi. Hai vòng lặp lồng nhau. Hãy tưởng tượng giáo viên dạy thuật toán của bạn đang trừng mắt nhìn bạn ngay bây giờ.

Và như tôi đã đề cập trước đây, tôi không quan tâm đến kết quả của công việc thám tử đó! Dù bằng cách nào, tôi sẽ làm lại CAS. Vì vậy, việc sử dụng CAS mạnh không mang lại lợi ích gì cho tôi, và khiến tôi mất đi một lượng hiệu quả nhỏ nhưng có thể đo lường được.

Nói cách khác, CAS yếu được sử dụng để thực hiện các hoạt động cập nhật nguyên tử. CAS mạnh được sử dụng khi bạn quan tâm đến kết quả của CAS.


0

Tôi nghĩ rằng hầu hết các câu trả lời ở trên đề cập đến "lỗi giả" như một số loại vấn đề, sự cân bằng giữa hiệu suất và độ đúng.

Nó có thể được coi là phiên bản yếu nhanh hơn hầu hết các lần, nhưng trong trường hợp lỗi giả, nó sẽ trở nên chậm hơn. Và phiên bản mạnh mẽ là phiên bản không có khả năng bị lỗi giả mạo, nhưng hầu như luôn chậm hơn.

Đối với tôi, sự khác biệt chính là cách hai phiên bản này xử lý vấn đề ABA:

phiên bản yếu sẽ chỉ thành công nếu không có ai chạm vào dòng bộ nhớ cache giữa tải và lưu trữ, vì vậy nó sẽ 100% phát hiện ra vấn đề ABA.

phiên bản mạnh sẽ chỉ thất bại nếu so sánh không thành công, vì vậy nó sẽ không phát hiện vấn đề ABA nếu không có các biện pháp bổ sung.

Vì vậy, về lý thuyết, nếu bạn sử dụng phiên bản yếu trên kiến ​​trúc thứ tự yếu, bạn không cần cơ chế phát hiện ABA và việc thực hiện sẽ đơn giản hơn nhiều, cho hiệu suất tốt hơn.

Tuy nhiên, trên x86 (kiến trúc theo thứ tự mạnh), phiên bản yếu và phiên bản mạnh giống nhau và cả hai đều gặp phải vấn đề ABA.

Vì vậy, nếu bạn viết một thuật toán đa nền tảng hoàn toàn, bạn cần phải giải quyết vấn đề ABA, vì vậy không có lợi ích về hiệu suất khi sử dụng phiên bản yếu, nhưng sẽ có hình phạt về hiệu suất đối với việc xử lý các lỗi giả.

Tóm lại - vì lý do di động và hiệu suất, phiên bản mạnh luôn là một lựa chọn tốt hơn hoặc tương đương.

Phiên bản yếu chỉ có thể là một lựa chọn tốt hơn nếu nó cho phép bạn bỏ qua hoàn toàn các biện pháp đối phó ABA hoặc thuật toán của bạn không quan tâm đến ABA.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.