2013-01-24 36 views
9

Tôi có đoạn mã sau C++ 2011:C++ memory_order với hàng rào và aquire/phát hành

std::atomic<bool> x, y; 
std::atomic<int> z; 

void f() { 
    x.store(true, std::memory_order_relaxed); 
    std::atomic_thread_fence(std::memory_order_release); 
    y.store(true, std::memory_order_relaxed); 
} 

void g() { 
    while (!y.load(std::memory_order_relaxed)) {} 
    std::atomic_thread_fence(std::memory_order_acquire); 
    if (x.load(std::memory_order_relaxed)) ++z; 
} 

int main() { 
    x = false; 
    y = false; 
    z = 0; 
    std::thread t1(f); 
    std::thread t2(g); 
    t1.join(); 
    t2.join(); 
    assert(z.load() !=0); 
    return 0; 
} 

Tại lớp kiến ​​trúc máy tính của tôi, chúng tôi đã được cho biết rằng khẳng định trong mã này luôn trở thành hiện thực. Nhưng sau khi xem xét nó thouroughly bây giờ, tôi không thể thực sự hiểu tại sao nó như vậy.

Đối với những gì tôi biết:

  • Một hàng rào với 'memory_order_release' sẽ không cho phép các cửa hàng trước để được thực thi sau khi nó
  • Một hàng rào với 'memory_order_acquire' sẽ không cho phép điều đó bất kỳ tải đến sau khi nó được thực thi trước nó.

Nếu hiểu biết của tôi là chính xác, tại sao chuỗi hành động sau đây không xảy ra?

  1. Bên t1, y.store(true, std::memory_order_relaxed); được gọi
  2. t2 chạy hoàn toàn, và sẽ thấy một 'false' khi tải 'x', do đó không tăng z trong một đơn vị
  3. t1 kết thúc thực hiện
  4. Trong chủ đề chính, khẳng định không thành công vì z.load() trả về 0

Tôi nghĩ điều này tuân thủ các quy tắc 'có được' - 'phát hành', nhưng, ví dụ câu trả lời hay nhất trong câu hỏi này: Understanding c++11 memory fences. simi lar cho trường hợp của tôi, nó gợi ý rằng một cái gì đó giống như bước 1 trong chuỗi hành động của tôi không thể xảy ra trước khi 'memory_order_release', nhưng không nhận được vào chi tiết vì lý do đằng sau nó.

tôi là terribly bối rối về điều này, và sẽ rất vui mừng nếu có ai có thể làm sáng tỏ về nó :)

Trả lời

4

Chính xác những gì xảy ra trong mỗi trường hợp này phụ thuộc vào những gì bạn đang xử lý thực tế sử dụng. Ví dụ, x86 có thể không khẳng định về điều này, vì nó là một kiến ​​trúc cache-coherent (bạn có thể có điều kiện chủng tộc, nhưng khi một giá trị được ghi vào bộ nhớ cache/bộ nhớ từ bộ vi xử lý, tất cả các bộ xử lý khác sẽ đọc giá trị đó - tất nhiên, không dừng một bộ xử lý khác ghi một giá trị khác ngay sau đó, v.v.).

Vì vậy, giả định này đang chạy trên một bộ xử lý ARM hoặc tương tự mà không được đảm bảo là bộ nhớ cache chặt chẽ bởi chính nó:

Bởi vì ghi vào x được thực hiện trước khi memory_order_release, vòng lặp t2 sẽ không thoát khỏi while(y...) cho đến x cũng đúng. Điều này có nghĩa là khi x đang được đọc sau, nó được đảm bảo là một, do đó, z được cập nhật. Truy vấn nhỏ duy nhất của tôi là nếu bạn không cần một số release cho z ... Nếu main đang chạy trên một bộ xử lý khác với t1t2, thì z có thể có giá trị cũ trong main. Tất nhiên, điều đó không được thực hiện nếu bạn có một hệ điều hành đa nhiệm (hoặc chỉ ngắt làm đủ công cụ, vv) - vì nếu bộ vi xử lý chạy t1 bị xóa bộ đệm, thì t2 có thể đọc được giá trị mới của x.

Và như tôi đã nói, điều này sẽ không ảnh hưởng đến bộ vi xử lý x86 (AMD hoặc Intel).

Vì vậy, để giải thích hướng dẫn rào cản nói chung (cũng áp dụng đối với Intel và AMD process0rs):

Đầu tiên, chúng ta cần phải hiểu rằng mặc dù hướng dẫn có thể bắt đầu và kết thúc trong trật tự, bộ vi xử lý không có một vị tướng " sự hiểu biết "của trật tự. Hãy nói rằng chúng tôi có điều này "pseudo-máy-code":

... 
mov $5, x 
cmp a, b 
jnz L1 
mov $4, x 

L1: ...

bộ vi xử lý đầu cơ có thể thực hiện mov $4, x trước khi nó hoàn thành "jnz L1" - vì vậy, để giải quyết này thực tế, bộ xử lý sẽ phải khôi phục mov $4, x trong trường hợp jnz L1 được thực hiện.

Tương tự như vậy, nếu chúng ta có:

mov $1, x 
wmb   // "write memory barrier" 
mov $1, y 

bộ vi xử lý có quy tắc để nói "không thực hiện bất kỳ hướng dẫn cửa hàng ban hành SAU WMB đến khi tất cả các cửa hàng trước khi nó đã được hoàn thành". Đó là một hướng dẫn "đặc biệt" - nó có mục đích chính xác để đảm bảo thứ tự bộ nhớ. Nếu nó không làm điều đó, bạn có một bộ vi xử lý bị hỏng, và ai đó trong bộ phận thiết kế có "ass của mình trên dòng".

Tương tự, "rào cản bộ nhớ đọc" là một hướng dẫn đảm bảo, bởi các nhà thiết kế bộ vi xử lý, bộ xử lý sẽ không hoàn thành một lần đọc khác cho đến khi chúng tôi hoàn thành các lần đọc đang chờ xử lý trước lệnh rào cản.

Miễn là chúng tôi không làm việc trên bộ xử lý "thử nghiệm" hoặc một số chip skanky không hoạt động chính xác, nó S work làm việc theo cách đó. Đó là một phần của định nghĩa của hướng dẫn đó. Nếu không có sự bảo đảm như vậy, nó sẽ là không thể (hoặc ít nhất là cực kỳ phức tạp và "đắt tiền") để thực hiện (an toàn) spinlocks, semaphores, mutexes, v.v.

Thường có những "rào cản bộ nhớ ngầm" - đó là hướng dẫn gây ra rào cản bộ nhớ ngay cả khi chúng không. Phần mềm gián đoạn ("INT X" hướng dẫn hoặc tương tự) có xu hướng làm điều này.

+0

+1 để biết thông tin thú vị. Nhưng câu hỏi: Bạn có nói rằng trên kiến ​​trúc không liên kết với bộ nhớ cache, ngay cả hàng rào bộ nhớ 'memory_order_release' không đủ để đảm bảo bộ nhớ cache của các bộ vi xử lý khác được cập nhật? – jogojapan

+0

Không, tôi nói rằng 'sử dụng memory_order_release' sẽ đảm bảo bộ nhớ cache-mạch lạc. Nhưng tôi phải xin lỗi, tôi đã đọc sai mã của bạn - tôi không thấy điều này sẽ thất bại như thế nào. Nếu y đúng với t2, thì x sẽ đúng trong t2. Vì vậy, giả sử vòng lặp 't2' cuối cùng kết thúc, thì' z' sẽ được tăng lên. Xin lỗi vì điều đó. –

+0

Tôi sẽ cập nhật câu trả lời. Nhưng nhớ rằng hai "tham gia" đảm bảo rằng cả hai chủ đề được hoàn thành trước khi chính được xác nhận. –

2

Tôi không thích tranh cãi về C++ các câu hỏi đồng thời về mặt "bộ xử lý này thực hiện việc này, bộ xử lý đó thực hiện điều đó". C++ 11 có một mô hình bộ nhớ, và chúng ta nên sử dụng mô hình bộ nhớ này để xác định những gì là hợp lệ và những gì không phải là. Kiến trúc CPU và các mô hình bộ nhớ thường khó hiểu hơn. Plus có nhiều hơn một trong số họ. Với điều này trong tâm trí, hãy xem xét điều này: thread t2 bị chặn trong vòng lặp while cho đến khi t1 thực hiện y.store và thay đổi đã được truyền đến t2. Vì vậy, chúng ta có mối quan hệ xảy ra trước đó giữa y.store trong t1 và y.load trong t2 cho phép nó rời khỏi vòng lặp.

Hơn nữa, chúng tôi có chuỗi nội tuyến đơn giản xảy ra trước khi các mối quan hệ giữa x.store và hàng rào phát hành và rào cản và y.store.

Trong t2, chúng ta có một sự kiện xảy ra trước đó giữa tải trọng thực và hàng rào thu được và x.load.

Vì xảy ra trước khi xảy ra chuyển tiếp, hàng rào phát hành xảy ra trước hàng rào có được và x.store xảy ra trước x.load. Vì các rào cản, x.store đồng bộ hóa với x.tải, có nghĩa là tải phải xem giá trị được lưu trữ.

Cuối cùng, z.add_and_fetch (post-increment) xảy ra trước khi kết thúc chuỗi, xảy ra trước khi chủ đề chính thức dậy từ t2.join, xảy ra trước khi z.load trong chuỗi chính, do đó, sửa đổi thành z phải được hiển thị trong chuỗi chính.

+0

Vâng, do đó, điểm mấu chốt là những gì tôi không hiểu là 'std :: atomic_thread_fence (std :: memory_order_release);' tạo ra một mối quan hệ xảy ra trước đó giữa chính nó pluas các cửa hàng trước đó, và 'y.store (đúng, std :: memory_order_relaxed); '. Thật không may Mats đã giải thích nó vì vậy tôi cho anh ta dấu kiểm cho sự nhanh chóng :) – alfongj

Các vấn đề liên quan