Làm cách nào để đạt được rào cản StoreLoad trong C ++ 11?


13

Tôi muốn viết mã di động (Intel, ARM, PowerPC ...) để giải quyết một biến thể của một vấn đề cổ điển:

Initially: X=Y=0

Thread A:
  X=1
  if(!Y){ do something }
Thread B:
  Y=1
  if(!X){ do something }

trong đó mục tiêu là để tránh một tình huống trong đó cả hai luồng đang làmsomething . (Sẽ ổn nếu không có thứ gì chạy; đây không phải là cơ chế chạy chính xác một lần.) Vui lòng sửa cho tôi nếu bạn thấy một số sai sót trong lý luận của tôi dưới đây.

Tôi biết rằng tôi có thể đạt được mục tiêu với memory_order_seq_cstcác nguyên tử storeloads như sau:

std::atomic<int> x{0},y{0};
void thread_a(){
  x.store(1);
  if(!y.load()) foo();
}
void thread_b(){
  y.store(1);
  if(!x.load()) bar();
}

đạt được mục tiêu, bởi vì phải có một số thứ tự duy nhất trên các
{x.store(1), y.store(1), y.load(), x.load()}sự kiện, phải đồng ý với "các cạnh" của thứ tự chương trình:

  • x.store(1) "trong TO là trước" y.load()
  • y.store(1) "trong TO là trước" x.load()

và nếu foo()được gọi, thì chúng ta có thêm cạnh:

  • y.load() "đọc giá trị trước" y.store(1)

và nếu bar()được gọi, thì chúng ta có thêm cạnh:

  • x.load() "đọc giá trị trước" x.store(1)

và tất cả các cạnh được kết hợp với nhau sẽ tạo thành một chu kỳ:

x.store(1)"trong TO là trước" y.load()"đọc giá trị trước" y.store(1)"trong TO là trước" x.load()"đọc giá trị trước"x.store(true)

vi phạm thực tế là các đơn đặt hàng không có chu kỳ.

Tôi cố tình sử dụng các thuật ngữ phi tiêu chuẩn "trong TO là trước" và "đọc giá trị trước" trái ngược với các thuật ngữ tiêu chuẩn như happens-before, bởi vì tôi muốn thu hút phản hồi về tính chính xác của giả định rằng các cạnh này thực sự ngụ ý happens-beforemối quan hệ, có thể được kết hợp với nhau trong một đồ thị và chu trình trong đồ thị kết hợp như vậy bị cấm. Tôi không chắc chắn về điều đó. Những gì tôi biết là mã này tạo ra các rào cản chính xác trên Intel gcc & clang và trên ARM gcc


Bây giờ, vấn đề thực sự của tôi phức tạp hơn một chút, vì tôi không kiểm soát được "X" - nó ẩn đằng sau một số macro, mẫu, v.v. và có thể yếu hơn seq_cst

Tôi thậm chí không biết "X" là một biến đơn hay một số khái niệm khác (ví dụ: semaphore hoặc mutex trọng lượng nhẹ). Tất cả những gì tôi biết là tôi có hai macro set()check()như vậy check()trả về true"sau" một luồng khác đã được gọi set(). (Người ta cũng biết rằng setcheckan toàn theo luồng và không thể tạo ra cuộc đua dữ liệu UB.)

Vì vậy, về mặt khái niệm set()có phần giống như "X = 1" và check()giống như "X", nhưng tôi không có quyền truy cập trực tiếp vào các nguyên tử liên quan, nếu có.

void thread_a(){
  set();
  if(!y.load()) foo();
}
void thread_b(){
  y.store(1);
  if(!check()) bar();
}

Tôi lo lắng, điều đó set()có thể được thực hiện trong nội bộ x.store(1,std::memory_order_release)và / hoặc check()có thể x.load(std::memory_order_acquire). Hoặc theo giả thuyết std::mutexrằng một chủ đề đang mở khóa và một chủ đề khác đang diễn try_lockra; trong tiêu chuẩn ISO std::mutexchỉ được đảm bảo có yêu cầu mua và phát hành, không phải seq_cst.

Nếu đây là trường hợp, thì check()nếu cơ thể có thể được "sắp xếp lại" trước đó y.store(true)( Xem câu trả lời của Alex nơi họ chứng minh rằng điều này xảy ra trên PowerPC ).
Điều này sẽ thực sự tồi tệ, vì bây giờ chuỗi sự kiện này là có thể:

  • thread_b()đầu tiên tải giá trị cũ của x( 0)
  • thread_a() thực hiện mọi thứ kể cả foo()
  • thread_b() thực hiện mọi thứ kể cả bar()

Vì vậy, cả hai foo()bar()được gọi, mà tôi phải tránh. Lựa chọn của tôi để ngăn chặn điều đó là gì?


Lựa chọn A

Cố gắng buộc hàng rào Store-Load. Điều này, trong thực tế, có thể đạt được bằng cách std::atomic_thread_fence(std::memory_order_seq_cst);- như Alex đã giải thích trong một câu trả lời khác, tất cả các trình biên dịch được thử nghiệm đều phát ra một hàng rào đầy đủ:

  • x86_64: SẮC
  • PowerPC: hwsync
  • Itanuim: mf
  • ARMv7 / ARMv8: dmb là
  • MIPS64: đồng bộ hóa

Vấn đề với cách tiếp cận này là, tôi không thể tìm thấy bất kỳ sự đảm bảo nào trong các quy tắc C ++, mà std::atomic_thread_fence(std::memory_order_seq_cst)phải dịch sang hàng rào bộ nhớ đầy đủ. Trên thực tế, khái niệm atomic_thread_fences trong C ++ dường như ở một mức độ trừu tượng khác với khái niệm lắp ráp các rào cản bộ nhớ và liên quan nhiều hơn đến những thứ như "hoạt động nguyên tử đồng bộ hóa với cái gì". Có bằng chứng lý thuyết nào cho thấy việc thực hiện dưới đây đạt được mục tiêu không?

void thread_a(){
  set();
  std::atomic_thread_fence(std::memory_order_seq_cst)
  if(!y.load()) foo();
}
void thread_b(){
  y.store(true);
  std::atomic_thread_fence(std::memory_order_seq_cst)
  if(!check()) bar();
}

Lựa chọn B

Sử dụng kiểm soát chúng tôi có trên Y để đạt được đồng bộ hóa, bằng cách sử dụng các thao tác đọc-sửa-ghi memory_order_acq_rel trên Y:

void thread_a(){
  set();
  if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
  y.exchange(1,std::memory_order_acq_rel);
  if(!check()) bar();
}

Ý tưởng ở đây là việc truy cập vào một nguyên tử ( y) phải được tạo thành một thứ tự duy nhất mà tất cả các nhà quan sát đồng ý, vì vậy hoặc fetch_addlà trước exchangehoặc ngược lại.

Nếu fetch_addlà trước exchangeđó thì phần "phát hành" fetch_addđồng bộ hóa với phần "thu nhận" exchangevà do đó tất cả các tác dụng phụ set()phải được hiển thị để thực thi mã check(), do đó bar()sẽ không được gọi.

Nếu không, exchangelà trước fetch_add, sau đó fetch_addsẽ thấy 1và không gọi foo(). Vì vậy, không thể gọi cả hai foo()bar(). Liệu lý luận này có đúng không?


Tùy chọn C

Sử dụng nguyên tử giả, để giới thiệu "các cạnh" ngăn ngừa thảm họa. Xem xét phương pháp sau:

void thread_a(){
  std::atomic<int> dummy1{};
  set();
  dummy1.store(13);
  if(!y.load()) foo();
}
void thread_b(){
  std::atomic<int> dummy2{};
  y.store(1);
  dummy2.load();
  if(!check()) bar();
}

Nếu bạn nghĩ rằng vấn đề ở đây là atomiccục bộ, thì hãy tưởng tượng việc chuyển chúng sang phạm vi toàn cầu, theo lý do sau đây, điều đó dường như không quan trọng với tôi, và tôi đã cố tình viết mã theo cách để phơi bày sự buồn cười đó như thế nào và dummy2 hoàn toàn riêng biệt.

Tại sao trên trái đất này có thể làm việc? Chà, phải có một số thứ tự duy nhất {dummy1.store(13), y.load(), y.store(1), dummy2.load()}phải phù hợp với "cạnh" thứ tự chương trình:

  • dummy1.store(13) "trong TO là trước" y.load()
  • y.store(1) "trong TO là trước" dummy2.load()

(Cửa hàng seq_cst + tải hy vọng tạo thành C ++ tương đương với hàng rào bộ nhớ đầy đủ bao gồm StoreLoad, giống như chúng hoạt động trên các ISA thực sự, kể cả AArch64, trong đó không yêu cầu các rào cản riêng biệt.)

Bây giờ, chúng tôi có hai trường hợp để xem xét: hoặc y.store(1)là trước y.load()hoặc sau trong tổng số thứ tự.

Nếu y.store(1)là trước y.load()đó thì foo()sẽ không được gọi và chúng tôi an toàn.

Nếu y.load()là trước y.store(1)đó, sau đó kết hợp nó với hai cạnh mà chúng ta đã có theo thứ tự chương trình, chúng tôi suy luận rằng:

  • dummy1.store(13) "trong TO là trước" dummy2.load()

Bây giờ, dummy1.store(13)là một hoạt động phát hành, phát hành các hiệu ứng set()dummy2.load()là một hoạt động có được, vì vậy check()sẽ thấy các hiệu ứng set()và do đó bar()sẽ không được gọi và chúng tôi an toàn.

Có đúng ở đây để nghĩ rằng check()sẽ thấy kết quả của set()? Tôi có thể kết hợp các "cạnh" của các loại khác nhau ("thứ tự chương trình" hay còn gọi là Trình tự trước, "tổng thứ tự", "trước khi phát hành", "sau khi có được") như thế không? Tôi có nghi ngờ nghiêm trọng về điều này: Các quy tắc C ++ dường như nói về mối quan hệ "đồng bộ hóa với" giữa cửa hàng và tải trên cùng một vị trí - ở đây không có tình huống như vậy.

Lưu ý rằng chúng tôi chỉ lo lắng về trường hợp dumm1.stoređược biết đến (thông qua lập luận khác) được trước dummy2.loadtrong tổng seq_cst trật tự. Vì vậy, nếu họ đã truy cập cùng một biến, tải sẽ thấy giá trị được lưu trữ và được đồng bộ hóa với nó.

(Rào cản bộ nhớ / sắp xếp lại lý do cho việc triển khai trong đó tải nguyên tử và lưu trữ biên dịch thành ít nhất các rào cản bộ nhớ 1 chiều (và các hoạt động seq_cst không thể sắp xếp lại: ví dụ: cửa hàng seq_cst không thể vượt qua tải seq_cst) lưu trữ sau khi dummy2.loadchắc chắn trở nên hiển thị cho các chủ đề khác sau y.store . Và tương tự cho các chủ đề khác, ... trước đó y.load.)


Bạn có thể chơi với việc triển khai Tùy chọn A, B, C của tôi tại https://godbolt.org/z/u3dTa8


1
Mô hình bộ nhớ C ++ không có bất kỳ khái niệm nào về sắp xếp lại StoreLoad, chỉ đồng bộ hóa với và xảy ra trước đó. (Và UB trên các cuộc đua dữ liệu trên các đối tượng phi nguyên tử, không giống như phần cứng thực sự.) Trên tất cả các triển khai thực tế tôi biết, std::atomic_thread_fence(std::memory_order_seq_cst)sẽ biên dịch thành một rào cản đầy đủ, nhưng vì toàn bộ khái niệm là một chi tiết triển khai mà bạn sẽ không tìm thấy bất kỳ đề cập đến nó trong tiêu chuẩn. (Các mô hình bộ nhớ CPU thường được xác định theo nghĩa các phép ghi lại được cho phép liên quan đến tính nhất quán tuần tự. Ví dụ: x86 là seq-cst + bộ đệm lưu trữ w / chuyển tiếp)
Peter Cordes

@PeterCordes cảm ơn, tôi có thể đã không rõ ràng trong văn bản của tôi. Tôi muốn truyền đạt những gì bạn đã viết trong phần "Lựa chọn A". Tôi biết tiêu đề câu hỏi của tôi sử dụng từ "StoreLoad" và "StoreLoad" là một khái niệm từ một thế giới hoàn toàn khác. Vấn đề của tôi là làm thế nào để ánh xạ khái niệm này vào C ++. Hoặc nếu không thể lập bản đồ trực tiếp, thì làm thế nào để đạt được mục tiêu tôi đã đặt ra: ngăn chặn foo()bar()từ cả hai được gọi.
qbolec

1
Bạn có thể sử dụng compare_exchange_*để thực hiện thao tác RMW trên bool nguyên tử mà không thay đổi giá trị của nó (chỉ cần đặt kỳ vọng và mới cho cùng một giá trị).
mpoeter

1
@Fareanor và qbolec: atomic<bool>exchangecompare_exchange_weak. Cái sau có thể được sử dụng để làm một RMW giả bằng cách (cố gắng) CAS (đúng, đúng) hoặc sai, sai. Nó hoặc thất bại hoặc nguyên tử thay thế giá trị với chính nó. (Trong x86-64 asm, mẹo đó lock cmpxchg16blà cách bạn thực hiện tải 16 byte nguyên tử được bảo đảm; không hiệu quả nhưng ít tệ hơn so với khóa riêng.)
Peter Cordes

1
@PeterCordes vâng tôi biết điều đó có thể xảy ra mà cả hai foo()cũng bar()sẽ không được gọi. Tôi không muốn mang đến nhiều yếu tố "thế giới thực" của mã, để tránh "bạn nghĩ rằng bạn có vấn đề X nhưng bạn có vấn đề về Y". Nhưng, nếu người ta thực sự cần biết tầng nền là gì: set()thực sự some_mutex_exit(), check()try_enter_some_mutex(), y"có một số người phục vụ", foo()là "thoát mà không đánh thức bất cứ ai", bar()là "chờ cho wakup" ... Nhưng, tôi từ chối thảo luận về thiết kế này ở đây - tôi không thể thay đổi nó thực sự.
qbolec

Câu trả lời:


5

Tùy chọn A và B là các giải pháp hợp lệ.

  • Tùy chọn A: thực sự không có vấn đề gì với hàng rào seq-cst dịch ra, tiêu chuẩn C ++ xác định rõ ràng những gì đảm bảo nó cung cấp. Tôi đã trình bày chúng trong bài viết này: Khi nào một hàng rào memory_order_seq_cst hữu ích?
  • Lựa chọn B: có, lý luận của bạn là chính xác. Tất cả các sửa đổi trên một số đối tượng có một tổng thứ tự (thứ tự sửa đổi), vì vậy bạn có thể sử dụng điều đó để đồng bộ hóa các luồng và đảm bảo khả năng hiển thị của tất cả các tác dụng phụ.

Tuy nhiên, lựa chọn C không hợp lệ! Một mối quan hệ đồng bộ hóa với chỉ có thể được thiết lập bằng các hoạt động thu nhận / giải phóng trên cùng một đối tượng . Trong trường hợp của bạn, bạn có hai đối tượng hoàn toàn khác nhau và độc lập dummy1dummy2. Nhưng những điều này không thể được sử dụng để thiết lập mối quan hệ xảy ra trước khi xảy ra. Trong thực tế, vì các biến nguyên tử hoàn toàn cục bộ (nghĩa là chúng chỉ được chạm bởi một luồng), trình biên dịch có thể tự do loại bỏ chúng dựa trên quy tắc as-if .

Cập nhật

Tùy chọn A:
Tôi giả sử set()check()hoạt động trên một số giá trị nguyên tử. Sau đó, chúng ta có tình huống sau (-> biểu thị tuần tự-trước ):

  • set()-> fence1(seq_cst)->y.load()
  • y.store(true)-> fence2(seq_cst)->check()

Vì vậy, chúng ta có thể áp dụng quy tắc sau:

Đối với các hoạt động nguyên tử AB trên một đối tượng nguyên tử M , trong đó A sửa đổi MB lấy giá trị của nó, nếu có memory_order_seq_csthàng rào XY sao cho A được sắp xếp trước X , Y được sắp xếp trước BX trước YS , sau đó B quan sát các tác động của A hoặc sửa đổi sau này của M theo thứ tự sửa đổi.

Tức là, hoặc check()thấy giá trị đó được lưu trữ sethoặc y.load()thấy giá trị được ghi y.store()(các thao tác trên ythậm chí có thể sử dụng memory_order_relaxed).

Tùy chọn C:
Các trạng thái tiêu chuẩn C ++ 17 [32.4.3, p1347]:

Sẽ có một tổng đơn hàng S trên tất cả các memory_order_seq_csthoạt động, phù hợp với đơn đặt hàng "xảy ra trước" và các lệnh sửa đổi cho tất cả các vị trí bị ảnh hưởng [...]

Từ quan trọng ở đây là "nhất quán". Điều đó ngụ ý rằng nếu một hoạt động Một xảy ra-trước khi phẫu thuật B , sau đó A phải đặt trước B trong S . Tuy nhiên, ý nghĩa logic là một chiều đường phố, vì vậy chúng ta không thể suy luận nghịch đảo: chỉ vì một số hoạt động C đến trước một hoạt động D trong S không có nghĩa là C xảy ra trước khi D .

Cụ thể, hai thao tác seq-cst trên hai đối tượng riêng biệt không thể được sử dụng để thiết lập xảy ra trước khi quan hệ, mặc dù các thao tác được sắp xếp hoàn toàn trong S. Nếu bạn muốn đặt hàng các hoạt động trên các đối tượng riêng biệt, bạn phải tham khảo seq-cst -fences (xem Tùy chọn A).


Không rõ ràng rằng Tùy chọn C không hợp lệ. Các hoạt động seq-cst ngay cả trên các đối tượng riêng tư vẫn có thể yêu cầu các hoạt động khác ở một mức độ nào đó. Đồng ý rằng không có đồng bộ hóa với, nhưng chúng tôi không quan tâm đến việc foo hay bar nào chạy (hoặc dường như không), chỉ là cả hai đều không chạy. Mối quan hệ tuần tự trước và tổng thứ tự của các hoạt động seq-cst (phải tồn tại) tôi nghĩ cho chúng ta điều đó.
Peter Cordes

Cảm ơn bạn @mpoeter. Bạn có thể giải thích rõ hơn về Lựa chọn A. Loại đạn nào trong ba câu trả lời của bạn áp dụng ở đây không? IIUC nếu y.load()không thấy hiệu lực của y.store(1), thì chúng ta có thể chứng minh từ các quy tắc rằng trong S, atomic_thread_fencecủa thread_a là trước atomic_thread_fencethread_b. Những gì tôi không thấy là làm thế nào để có được từ đây để kết luận rằng các set()tác dụng phụ có thể nhìn thấy được check().
qbolec

1
@qbolec: Tôi đã cập nhật câu trả lời của mình với nhiều chi tiết hơn về tùy chọn A.
mpoeter

1
Có, một hoạt động seq-cst cục bộ vẫn sẽ là một phần của tổng đơn hàng S trên tất cả các hoạt động seq-cst. Nhưng S là "chỉ" phù hợp với xảy ra-trước khi đặt hàng và sửa đổi đơn đặt hàng , tức là, nếu A xảy ra-trước khi B , sau đó A phải đặt trước B trong S . Nhưng ngược lại không được bảo đảm, tức là, chỉ vì A đến trước B trong S , chúng tôi không thể suy ra , rằng A xảy ra-trước khi B .
mpoeter

1
Chà, giả sử rằng setcheckcó thể được thực thi song song một cách an toàn, có lẽ tôi sẽ đi với Tùy chọn A, đặc biệt nếu đây là hiệu suất quan trọng, vì nó tránh được sự tranh chấp về biến được chia sẻ y.
mpoeter

1

Trong ví dụ đầu tiên, y.load()đọc 0 không có nghĩa là y.load()xảy ra trước đó y.store(1).

Tuy nhiên, điều đó có nghĩa là nó sớm hơn trong tổng đơn hàng nhờ vào quy tắc rằng tải seq_cst trả về giá trị của cửa hàng seq_cst cuối cùng trong tổng đơn hàng hoặc giá trị của một số cửa hàng không seq_cst không xảy ra trước đó nó (trong trường hợp này không tồn tại). Vì vậy, nếu y.store(1)sớm hơn so với y.load()tổng thứ tự, y.load()sẽ trả về 1.

Bằng chứng vẫn đúng vì tổng đơn hàng không có chu kỳ.

Làm thế nào về giải pháp này?

std::atomic<int> x2{0},y{0};

void thread_a(){
  set();
  x2.store(1);
  if(!y.load()) foo();
}

void thread_b(){
  y.store(1);
  if(!x2.load()) bar();
}

Vấn đề của OP là tôi không có quyền kiểm soát đối với "X" - nó nằm sau các macro bao bọc hoặc thứ gì đó và có thể không phải là cửa hàng / tải seq-cst. Tôi cập nhật câu hỏi để làm nổi bật điều đó tốt hơn.
Peter Cordes

@PeterCordes Ý tưởng là tạo ra một "x" khác mà anh ta có quyền kiểm soát. Tôi sẽ đổi tên thành "x2" trong câu trả lời của mình để làm cho nó rõ ràng hơn. Tôi chắc chắn rằng tôi đang thiếu một số yêu cầu, nhưng nếu yêu cầu duy nhất là đảm bảo rằng foo () và bar () không được gọi cả hai, thì điều này đáp ứng điều đó.
Tomek Czajka

Cũng vậy if(false) foo();nhưng tôi nghĩ OP cũng không muốn điều đó: P Điểm thú vị nhưng tôi nghĩ OP không muốn các cuộc gọi có điều kiện dựa trên các điều kiện mà họ chỉ định!
Peter Cordes

1
Xin chào @TomekCzajka, cảm ơn vì đã dành thời gian đề xuất giải pháp mới. Nó sẽ không hoạt động trong trường hợp cụ thể của tôi, vì nó bỏ qua các tác dụng phụ quan trọng của check()(xem bình luận của tôi cho câu hỏi của tôi cho ý nghĩa thực tế của set,check,foo,bar). Tôi nghĩ rằng nó có thể làm việc với if(!x2.load()){ if(check())x2.store(0); else bar(); }thay vào đó.
qbolec

1

@mpoeter giải thích tại sao Tùy chọn A và B an toàn.

Trong thực tế về triển khai thực tế, tôi nghĩ Tùy chọn A chỉ cần std::atomic_thread_fence(std::memory_order_seq_cst)trong Chủ đề A, không phải B.

Các cửa hàng seq-cst trong thực tế bao gồm một rào cản bộ nhớ đầy đủ, hoặc trên AArch64 ít nhất không thể sắp xếp lại với các lần tải hoặc seq_cst sau này ( stlrphát hành tuần tự phải thoát khỏi bộ đệm lưu trữ trước khi ldarcó thể đọc từ bộ đệm).

Ánh xạ C ++ -> asm có lựa chọn đặt chi phí thoát bộ đệm của cửa hàng lên các cửa hàng nguyên tử hoặc tải nguyên tử. Sự lựa chọn lành mạnh cho việc triển khai thực tế là làm cho tải nguyên tử trở nên rẻ, vì vậy các cửa hàng seq_cst bao gồm một rào cản đầy đủ (bao gồm cả StoreLoad). Trong khi tải seq_cst giống như tải tải trên hầu hết.

(Nhưng không phải POWER; thậm chí tải cần đồng bộ hóa nặng = rào cản đầy đủ để dừng chuyển tiếp lưu trữ từ các luồng khác trên cùng lõi có thể dẫn đến IRIW sắp xếp lại, vì seq_cst yêu cầu tất cả các luồng phải đồng ý theo thứ tự tất cả các seq_cst op. Liệu hai nguyên tử ghi vào các vị trí khác nhau trong các luồng khác nhau sẽ luôn được nhìn thấy theo cùng một thứ tự bởi các luồng khác? )

(Tất nhiên để đảm bảo an toàn chính thức , chúng tôi cần một hàng rào trong cả hai để quảng bá bộ thu / phát hành () -> kiểm tra () thành một bộ đồng bộ hóa seq_cst. Tôi cũng sẽ làm việc cho một bộ thoải mái, nhưng tôi nghĩ kiểm tra thoải mái có thể sắp xếp lại với thanh từ POV của các chủ đề khác.)


Tôi nghĩ vấn đề thực sự với Lựa chọn C là nó phụ thuộc vào một số nhà quan sát giả định có thể đồng bộ hóa và với ycác hoạt động giả. Và do đó, chúng tôi hy vọng trình biên dịch sẽ duy trì thứ tự đó khi tạo asm cho một ISA dựa trên rào cản.

Điều này sẽ đúng trong thực tế trên các ISA thực sự; cả hai luồng bao gồm một rào cản đầy đủ hoặc tương đương và trình biên dịch không (chưa) tối ưu hóa nguyên tử. Nhưng tất nhiên "biên dịch thành một ISA dựa trên rào cản" không phải là một phần của tiêu chuẩn ISO C ++. Bộ đệm chia sẻ kết hợp là trình quan sát giả định tồn tại cho lý luận asm nhưng không phải cho lý luận ISO C ++.

Để Tùy chọn C hoạt động, chúng tôi cần một thứ tự như dummy1.store(13);/ y.load()/ set();(như được thấy bởi Chủ đề B) để vi phạm một số quy tắc ISO C ++ .

Chuỗi chạy các câu lệnh này phải hoạt động như thể được set() thực thi trước (vì Trình tự trước). Điều đó tốt, thứ tự bộ nhớ thời gian chạy và / hoặc biên dịch thời gian sắp xếp lại các hoạt động vẫn có thể làm điều đó.

Hai seq_cst op d1=13yphù hợp với Sequined Before (thứ tự chương trình). set()không tham gia vào trật tự toàn cầu bắt buộc cho seq_cst op vì nó không phải là seq_cst.

Chủ đề B không đồng bộ hóa - với dummy1.store để không xảy ra yêu cầu trước khi áp dụng setliên quand1=13 , mặc dù nhiệm vụ đó là một hoạt động phát hành.

Tôi không thấy bất kỳ vi phạm quy tắc có thể khác; Tôi không thể tìm thấy bất cứ điều gì ở đây bắt buộc phải phù hợp với setTrình tự trước đó d1=13.

Lý do "dummy1.store phát hành set ()" là lỗ hổng. Thứ tự đó chỉ áp dụng cho một người quan sát thực sự đồng bộ hóa - với nó hoặc trong asm. Như @mpoeter đã trả lời, sự tồn tại của tổng đơn hàng seq_cst không tạo ra hoặc ngụ ý xảy ra trước các mối quan hệ và đó là điều duy nhất chính thức đảm bảo đặt hàng bên ngoài seq_cst.

Bất kỳ loại CPU "bình thường" nào có bộ đệm chia sẻ kết hợp trong đó việc sắp xếp lại này thực sự có thể xảy ra trong thời gian chạy dường như không hợp lý. (Nhưng nếu một trình biên dịch có thể loại bỏ dummy1dummy2sau đó rõ ràng chúng ta sẽ gặp vấn đề và tôi nghĩ rằng điều đó được cho phép theo tiêu chuẩn.)

Nhưng do mô hình bộ nhớ C ++ không được xác định theo thuật ngữ của bộ đệm lưu trữ, bộ đệm kết hợp được chia sẻ hoặc các thử nghiệm litmus về sắp xếp lại được phép, nên những thứ được yêu cầu bởi sự tỉnh táo không phải là quy tắc chính thức của C ++. Điều này có lẽ là cố ý để cho phép tối ưu hóa ngay cả các biến seq_cst hóa ra là luồng riêng tư. (Tất nhiên các trình biên dịch hiện tại không làm điều đó, hoặc bất kỳ tối ưu hóa nào khác của các đối tượng nguyên tử.)

Một triển khai trong đó một luồng thực sự có thể nhìn thấy set()cuối cùng trong khi một luồng khác có thể thấy set()âm thanh đầu tiên là không thể tin được. Thậm chí POWER không thể làm được điều đó; cả tải seq_cst và lưu trữ bao gồm các rào cản đầy đủ cho POWER. . )

C ++ không đảm bảo bất cứ điều gì cho người không seq_cst trừ khi có thực sự một người quan sát, và sau đó chỉ dành cho người quan sát đó. Không có ai ở trong lãnh thổ mèo của Schroedinger. Hoặc, nếu hai cây ngã trong rừng, một cây có bị ngã trước cây kia không? (Nếu đó là một khu rừng lớn, thuyết tương đối rộng nói rằng nó phụ thuộc vào người quan sát và rằng không có khái niệm phổ quát về tính đồng thời.)


@mpoeter đề xuất một trình biên dịch thậm chí có thể loại bỏ tải giả và lưu trữ các hoạt động, ngay cả trên các đối tượng seq_cst.

Tôi nghĩ rằng điều đó có thể đúng khi họ có thể chứng minh rằng không có gì có thể đồng bộ hóa với một hoạt động. ví dụ: trình biên dịch có thể thấy rằng dummy2không thoát khỏi hàm có thể có thể loại bỏ tải seq_cst đó.

Điều này có ít nhất một hậu quả trong thế giới thực: nếu biên dịch cho AArch64, điều đó sẽ cho phép một cửa hàng seq_cst trước đó sắp xếp lại trong thực tế với các hoạt động thoải mái sau này, điều này sẽ không thể xảy ra với cửa hàng seq_cst + tải hết bộ đệm của cửa hàng trước bất kỳ tải sau này có thể thực thi.

Tất nhiên các trình biên dịch hiện tại hoàn toàn không tối ưu hóa nguyên tử, mặc dù ISO C ++ không cấm nó; đó là một vấn đề chưa được giải quyết cho ủy ban tiêu chuẩn.

Điều này được cho phép tôi nghĩ bởi vì mô hình bộ nhớ C ++ không có người quan sát ngầm hoặc một yêu cầu mà tất cả các luồng đồng ý khi đặt hàng. Nó cung cấp một số đảm bảo dựa trên bộ nhớ kết hợp, nhưng nó không yêu cầu khả năng hiển thị cho tất cả các luồng phải đồng thời.


Tóm tắt tốt đẹp! Tôi đồng ý rằng trong thực tế có lẽ sẽ đủ nếu chỉ có luồng A có hàng rào seq-cst. Tuy nhiên, dựa trên tiêu chuẩn C ++, chúng tôi sẽ không có sự đảm bảo cần thiết rằng chúng tôi thấy giá trị mới nhất từ ​​đó set(), vì vậy tôi vẫn sẽ sử dụng hàng rào trong luồng B. Tôi cho rằng một cửa hàng thư giãn với hàng rào seq-cst sẽ tạo ra gần như cùng mã với cửa hàng seq-cst.
mpoeter

@mpoeter: yup, tôi chỉ nói về thực tế, không chính thức. Đã thêm một ghi chú ở cuối phần đó. Và vâng, trên thực tế trên hầu hết các ISA tôi nghĩ rằng một cửa hàng seq_cst thường chỉ là cửa hàng đơn giản (thư giãn) + một rào cản. Hay không; trên POWER, cửa hàng seq-cst thực hiện (trọng lượng nặng) sync trước cửa hàng, không có gì sau đó. godbolt.org/z/mAr72P Nhưng tải seq-cst cần một số rào cản ở cả hai bên.
Peter Cordes

1

trong tiêu chuẩn ISO std :: mutex chỉ được đảm bảo để có được và phát hành thứ tự, không phải seq_cst.

Nhưng không có gì được đảm bảo để có "seq_cst order", vì seq_cstđây không phải là tài sản của bất kỳ hoạt động nào.

seq_cstlà một sự đảm bảo đối với tất cả các hoạt động của việc thực hiện nhất định std::atomichoặc một lớp nguyên tử thay thế. Như vậy, câu hỏi của bạn là không có căn cứ.

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.