Sơ lược SMP dành cho Android

Các phiên bản nền tảng Android 3.0 trở lên được tối ưu hoá để hỗ trợ kiến trúc đa bộ xử lý. Tài liệu này giới thiệu các vấn đề có thể phát sinh khi viết mã đa luồng cho hệ thống đa xử lý đối xứng trong C, C++ và Java ngôn ngữ lập trình (sau đây được gọi đơn giản là "Java" nhằm mục đích ngắn gọn). Đây chỉ là một cuốn sách cơ bản dành cho các nhà phát triển ứng dụng Android chứ không phải là một tài liệu hoàn chỉnh thảo luận về chủ đề này.

Giới thiệu

SMP là viết tắt của "Symmetric Multi-Processor". Phần này mô tả một thiết kế theo có hai hoặc nhiều lõi CPU giống hệt nhau có chung quyền truy cập vào bộ nhớ chính. Cho đến vài năm trước, tất cả các thiết bị Android đều được UP (Bộ xử lý đơn giản).

Hầu hết – nếu không phải là tất cả – các thiết bị Android luôn có nhiều CPU, nhưng trước đây chỉ một trong số chúng được dùng để chạy các ứng dụng trong khi những người khác quản lý các bit của thiết bị phần cứng (ví dụ: radio). Các CPU có thể có kiến trúc khác nhau và các chương trình đang chạy trên chúng không thể sử dụng bộ nhớ chính để giao tiếp với nhau khác.

Hầu hết thiết bị Android bán ra hiện nay đều được xây dựng dựa trên thiết kế SMP, khiến mọi thứ trở nên phức tạp hơn một chút đối với các nhà phát triển phần mềm. Điều kiện tranh đấu trong một chương trình đa luồng không được gây ra các sự cố rõ ràng trên bộ đơn xử lý, nhưng có thể thường xuyên gặp lỗi khi hai hoặc nhiều luồng của bạn đang chạy đồng thời trên nhiều lõi. Hơn nữa, mã có thể ít hoặc nhiều khả năng gặp lỗi khi chạy trên các nền tảng khác nhau kiến trúc bộ xử lý hay thậm chí trên các cách triển khai khác nhau cấu trúc. Mã đã được kiểm tra kỹ lưỡng trên x86 có thể bị lỗi trên ARM. Mã có thể bắt đầu gặp lỗi khi được biên dịch lại bằng một trình biên dịch hiện đại hơn.

Phần còn lại của tài liệu này sẽ giải thích lý do và cho bạn biết việc bạn cần làm để đảm bảo rằng mã của bạn hoạt động chính xác.

Mô hình tính nhất quán của bộ nhớ: Tại sao các SMP lại có chút khác biệt

Đây là nội dung tổng quan đơn giản, tốc độ cao về một chủ đề phức tạp. Một số khu vực sẽ chưa đầy đủ nhưng không được gây hiểu lầm hoặc không chính xác. Khi bạn sẽ xem trong phần tiếp theo. Các thông tin chi tiết ở đây thường không quan trọng.

Xem phần Đọc thêm ở cuối tài liệu để biết chỉ ra các biện pháp xử lý triệt để hơn về chủ đề này.

Các mô hình tính nhất quán của bộ nhớ, hay thường chỉ là "mô hình bộ nhớ", mô tả đảm bảo ngôn ngữ lập trình hoặc kiến trúc phần cứng trợ giúp về quyền truy cập bộ nhớ. Ví dụ: nếu bạn viết giá trị đến địa chỉ A rồi sau đó viết giá trị đến địa chỉ B, thì có thể đảm bảo rằng mọi lõi CPU đều thấy những lượt ghi đó diễn ra trong đơn đặt hàng.

Mô hình mà hầu hết các lập trình viên đều quen thuộc là tuần tự nhất quán, được mô tả như sau (Adve & Gharachorloo):

  • Tất cả thao tác đối với bộ nhớ sẽ có vẻ thực thi lần lượt từng thao tác
  • Tất cả thao tác trong một luồng có vẻ như thực thi theo thứ tự được mô tả theo chương trình của đơn vị xử lý đó.

Hãy tạm thời giả định rằng chúng ta có một trình biên dịch hoặc thông dịch viên rất đơn giản không gây bất ngờ: Điều này dịch trong mã nguồn để tải và lưu trữ hướng dẫn một cách chính xác lệnh tương ứng, một lệnh cho mỗi lượt truy cập. Chúng tôi cũng sẽ giả định cho tính đơn giản là mỗi luồng thực thi trên bộ xử lý của chính nó.

Nếu bạn nhìn vào một đoạn mã và thấy rằng nó thực hiện một số thao tác đọc và ghi từ trên một cấu trúc CPU nhất quán về tuần tự, bạn biết rằng mã sẽ thực hiện các lượt đọc và ghi đó theo thứ tự dự kiến. Có thể CPU thực sự đang sắp xếp lại các lệnh và trì hoãn việc đọc và ghi, nhưng có không phải là cách để mã chạy trên thiết bị cho biết rằng CPU đang thực hiện bất kỳ hoạt động nào ngoài việc thực hiện hướng dẫn theo cách đơn giản. (Chúng tôi sẽ bỏ qua I/O trình điều khiển thiết bị được ánh xạ bộ nhớ.)

Để minh hoạ những điểm này, bạn nên xem xét các đoạn mã nhỏ, thường được gọi là xét nghiệm quỳ tím.

Dưới đây là ví dụ đơn giản, với mã chạy trên 2 luồng:

Chuỗi 1 Chuỗi 2
A = 3
B = 5
reg0 = B
reg1 = A

Trong ví dụ này và tất cả các ví dụ về quảng cáo địa phương trong tương lai, vị trí bộ nhớ được biểu thị bằng chữ cái viết hoa (A, B, C) và thanh ghi CPU bắt đầu bằng "reg". Tất cả kỷ niệm đều là ban đầu là 0. Các hướng dẫn sẽ được thực thi từ trên xuống dưới. Đây, chuỗi 1 lưu trữ giá trị 3 tại vị trí A và sau đó lưu trữ giá trị 5 tại vị trí B. Chuỗi 2 tải giá trị từ vị trí B vào reg0 rồi tải giá trị từ vị trí A thành reg1. (Lưu ý rằng chúng ta viết theo một thứ tự và sẽ đọc bằng khác).

Luồng 1 và luồng 2 được giả định là sẽ thực thi trên nhiều lõi CPU. Bạn nên luôn đưa ra giả định này khi cân nhắc mã đa luồng.

Tính nhất quán tuần tự đảm bảo rằng sau khi cả hai luồng đã kết thúc thì các thanh ghi sẽ ở một trong các trạng thái sau:

Đăng ký Các trạng thái
reg0=5, reg1=3 có thể (luồng 1 chạy trước tiên)
reg0=0, reg1=0 có thể (luồng 2 chạy trước tiên)
reg0=0, reg1=3 có thể thực hiện (thực thi đồng thời)
reg0=5, reg1=0 không bao giờ

Để gặp phải tình huống mà chúng ta thấy B=5 trước khi thấy cửa hàng đến A, hoặc số lần đọc hoặc ghi sẽ phải xảy ra không đúng thứ tự. Trên một cách tuần tự nhất quán. Đây là điều không thể xảy ra.

Các bộ xử lý đơn nhất (bao gồm cả x86 và ARM) thường thống nhất về tuần tự. Các luồng có vẻ như thực thi theo kiểu xen kẽ, khi nhân hệ điều hành chuyển đổi giữa chúng. Hầu hết các hệ thống SMP, bao gồm cả x86 và ARM, không nhất quán về mặt tuần tự. Ví dụ: thông thường, phần cứng vào bộ đệm lưu trên bộ nhớ để chúng sẽ không vào bộ nhớ ngay lập tức mà hiện lên với các lõi khác.

Các chi tiết có sự khác biệt đáng kể. Ví dụ: x86, mặc dù không tuần tự nhất quán, vẫn đảm bảo rằng reg0 = 5 và reg1 = 0 vẫn là không thể. Các cửa hàng được lưu vào bộ đệm nhưng thứ tự của chúng vẫn được duy trì. Mặt khác, ARM thì không. Thứ tự của các cửa hàng lưu vào vùng đệm không duy trì và các cửa hàng có thể không đạt đến tất cả các lõi khác cùng một lúc. Những khác biệt này rất quan trọng đối với các lập trình viên lắp ráp. Tuy nhiên, như chúng ta sẽ thấy dưới đây, các lập trình viên C, C++ hoặc Java có thể và nên lập trình theo cách che giấu những khác biệt về cấu trúc như vậy.

Cho đến nay, chúng tôi đã giả định một cách phi thực tế rằng đó chỉ là phần cứng sắp xếp lại hướng dẫn. Trên thực tế, trình biên dịch cũng sắp xếp lại các lệnh thành cải thiện hiệu suất. Trong ví dụ của chúng ta, trình biên dịch có thể quyết định rằng một số mã trong Luồng 2 cần giá trị của reg1 trước khi cần reg0, và do đó tải Hãy đăng ký lại trước. Hoặc một số mã trước đó có thể đã tải A và trình biên dịch có thể quyết định sử dụng lại giá trị đó thay vì tải lại A. Trong cả hai trường hợp, thì tải thành reg0 và reg1 có thể được sắp xếp lại.

Sắp xếp lại thứ tự quyền truy cập vào các vị trí bộ nhớ, trong phần cứng hoặc trong trình biên dịch là vì nó không ảnh hưởng đến việc thực thi một luồng, và thì có thể cải thiện đáng kể hiệu suất. Như chúng ta sẽ thấy, hãy cẩn thận một chút, chúng tôi cũng có thể ngăn chặn điều này ảnh hưởng đến kết quả của các chương trình đa luồng.

Vì trình biên dịch cũng có thể sắp xếp lại thứ tự các quyền truy cập bộ nhớ, nên vấn đề này không còn mới mẻ đối với các SMP. Ngay cả trên bộ đơn xử lý, trình biên dịch có thể sắp xếp lại thứ tự tải thành reg0 và reg1 trong ví dụ của chúng tôi và Luồng 1 có thể được lên lịch giữa các hướng dẫn đã đặt lại. Nhưng nếu trình biên dịch của chúng ta không sắp xếp lại được, chúng ta có thể chưa bao giờ quan sát vấn đề này. Trên hầu hết các SMP ARM, ngay cả khi không có trình biên dịch thì việc sắp xếp lại thứ tự đó có thể được nhận thấy, có thể sau khi số lần thực thi thành công. Trừ phi bạn đang lập trình hợp thành nói chung, SMP thường chỉ giúp bạn có nhiều khả năng gặp phải vấn đề luôn ở đó.

Lập trình không phân biệt dữ liệu

May mắn thay, thường có một cách dễ dàng để tránh nghĩ về bất kỳ những chi tiết này. Nếu bạn tuân theo một số quy tắc đơn giản, cách này thường an toàn để quên tất cả phần trước đó, ngoại trừ "tính nhất quán tuần tự" phần. Thật không may, các chức năng khác có thể hiển thị nếu bạn vô tình vi phạm các quy tắc đó.

Các ngôn ngữ lập trình hiện đại khuyến khích giải pháp "không có cuộc đua dữ liệu" phong cách lập trình. Miễn là bạn cam kết không thực hiện "cuộc chạy đua dữ liệu", và tránh một số cấu trúc chỉ cho trình biên dịch biết rằng trình biên dịch và phần cứng hứa hẹn cung cấp các kết quả nhất quán về tuần tự. Điều này không tức là chúng tránh việc sắp xếp lại quyền truy cập bộ nhớ. Điều đó có nghĩa là nếu bạn Khi bạn tuân theo những quy tắc, bạn sẽ không thể biết rằng các quyền truy cập vào bộ nhớ đang được đã đặt hàng lại. Điều này rất giống với việc nói với các bạn rằng xúc xích là món ngon và món ăn ngon miệng, miễn là bạn hứa không ghé thăm nhà máy xúc xích. Cuộc đua dữ liệu là thứ phơi bày sự thật xấu xa về bộ nhớ đang đặt hàng lại.

"Cuộc đua dữ liệu" là gì?

Cuộc đua dữ liệu xảy ra khi ít nhất 2 luồng truy cập đồng thời cùng một dữ liệu thông thường và ít nhất một người trong số họ sửa đổi dữ liệu đó. Theo "ordinary" hơn" chúng tôi muốn nói đến một điều gì đó không cụ thể là đối tượng đồng bộ hoá để giao tiếp theo chuỗi. Disabledx, biến điều kiện, Java chất bay hơi hoặc đối tượng nguyên tử C++ không phải là dữ liệu thông thường và quyền truy cập của chúng được phép tranh cử. Trên thực tế, chúng được dùng để ngăn chặn việc đua dữ liệu trên .

Để xác định xem hai luồng có truy cập đồng thời vào cùng một luồng hay không vị trí bộ nhớ, chúng ta có thể bỏ qua cuộc thảo luận sắp xếp lại bộ nhớ ở trên và giả định tính nhất quán tuần tự. Chương trình sau không có cuộc đua dữ liệu nếu AB là các biến boolean thông thường sai ban đầu:

Chuỗi 1 Chuỗi 2
if (A) B = true if (B) A = true

Vì các toán tử không được sắp xếp lại, nên cả hai điều kiện đều sẽ được đánh giá là false và không có biến nào được cập nhật. Do đó, không thể tạo ra một cuộc đua dữ liệu. Có không cần suy nghĩ về điều có thể xảy ra nếu tải từ A và lưu trữ vào B ở Chuỗi cuộc trò chuyện 1 đã được sắp xếp lại theo cách nào đó. Trình biên dịch không được phép sắp xếp lại Thread 1 bằng cách viết lại thành "B = true; if (!A) B = false". Đó chính là chẳng hạn như làm xúc xích ở giữa thị trấn vào ban ngày.

Cuộc đua dữ liệu được xác định chính thức trên các kiểu tích hợp cơ bản như số nguyên và tham chiếu hoặc con trỏ. Đang gán cho một int đồng thời việc đọc nó trong một chuỗi khác rõ ràng là một cuộc đua dữ liệu. Nhưng cả C++ thư viện chuẩn và các thư viện Bộ sưu tập Java được viết để bạn cũng có thể giải thích về dữ liệu khác nhau ở cấp thư viện. Họ hứa hẹn sẽ không thực hiện chạy đua dữ liệu trừ phi có quyền truy cập đồng thời vào cùng một vùng chứa, thì ít nhất một trong để cập nhật thông tin đó. Cập nhật set<T> trong một chuỗi trong khi đọc đồng thời trong một ứng dụng khác cho phép thư viện giới thiệu một cuộc đua dữ liệu và do đó có thể được hiểu theo cách không chính thức là "cuộc đua dữ liệu cấp thư viện". Ngược lại, cập nhật một set<T> trong một chuỗi trong khi đọc một phân khúc khác với nhau, sẽ không dẫn đến cuộc đua dữ liệu, bởi vì thư viện hứa hẹn sẽ không thực hiện chạy đua dữ liệu (cấp thấp) trong trường hợp đó.

Thông thường, có quyền truy cập đồng thời vào các trường khác nhau trong một cấu trúc dữ liệu không thể thực hiện chạy dữ liệu. Tuy nhiên, có một ngoại lệ quan trọng đối với quy tắc này: Các chuỗi trường bit liền kề trong C hoặc C++ được coi là một "vị trí bộ nhớ". Truy cập vào bất kỳ trường bit nào trong một trình tự như vậy được coi là truy cập vào tất cả các báo cáo đó nhằm mục đích xác định sự tồn tại của một cuộc đua dữ liệu. Điều này phản ánh việc phần cứng thông thường không có khả năng để cập nhật từng bit riêng lẻ mà không cần đọc và ghi lại các bit liền kề. Các lập trình viên Java không có mối lo ngại tương tự nào.

Tránh chạy đua dữ liệu

Các ngôn ngữ lập trình hiện đại cung cấp nhiều tính năng đồng bộ hoá để tránh chạy đua dữ liệu. Sau đây là những công cụ cơ bản nhất:

Khoá hoặc Disabledx
Tắt tiếng (C++11 std::mutex hoặc pthread_mutex_t) hoặc Bạn có thể sử dụng khối synchronized trong Java để đảm bảo rằng một số không chạy đồng thời với các phần mã khác truy cập vào mã cùng một dữ liệu. Chúng tôi sẽ đề cập chung đến các cơ sở này và các cơ sở tương tự khác là "khoá". Luôn có được một phương thức khoá cụ thể trước khi dùng chung cấu trúc dữ liệu và giải phóng cấu trúc này sau đó, ngăn chặn tình trạng chạy đua dữ liệu khi truy cập cấu trúc dữ liệu. API này cũng đảm bảo rằng các bản cập nhật và quyền truy cập đều mang tính nguyên tử, tức là không bản cập nhật khác cho cấu trúc dữ liệu có thể chạy ở giữa. Chương trình này hoàn toàn xứng đáng cho đến nay là công cụ phổ biến nhất giúp ngăn chặn việc tranh đua dữ liệu. Sử dụng Java synchronized khối hoặc C++ lock_guard hoặc unique_lock đảm bảo bạn thả khoá đúng cách trong trường hợp ngoại lệ.
Biến dễ bay hơi/nguyên tử
Java cung cấp các trường volatile hỗ trợ truy cập đồng thời mà không cần chạy đua dữ liệu. Kể từ năm 2011, C và C++ đã hỗ trợ Các biến và trường atomic có ngữ nghĩa tương tự. Đây là thường khó sử dụng hơn khoá, vì chúng chỉ đảm bảo rằng truy cập riêng lẻ vào một biến duy nhất mang tính nguyên tử. (Trong C++, thư này thường mở rộng đến các thao tác đọc-sửa đổi-ghi đơn giản, như gia số. Java cần có lệnh gọi phương thức đặc biệt cho việc đó.) Không giống như khoá, biến volatile hoặc atomic không thể được dùng trực tiếp để ngăn các luồng khác can thiệp vào trình tự mã dài hơn.

Điều quan trọng cần lưu ý là volatile có ý nghĩa trong C++ và Java. Trong C++, volatile không ngăn chặn dữ liệu mặc dù mã cũ thường sử dụng nó như một giải pháp cho việc thiếu Đối tượng atomic. Bạn không nên sử dụng nội dung này nữa; inch C++, hãy sử dụng atomic<T> cho các biến có thể đồng thời được nhiều chuỗi truy cập. volatile trong C++ dành cho thanh ghi thiết bị và những thứ tương tự.

Biến C/C++ atomic hoặc biến Java volatile có thể được dùng để ngăn tình trạng đua dữ liệu trên các biến khác. Nếu flag là khai báo có loại atomic<bool> hoặc atomic_bool(C/C++) hoặc volatile boolean (Java), và ban đầu là false thì đoạn mã sau đây sẽ là data-race-free:

Chuỗi 1 Chuỗi 2
A = ...
  flag = true
while (!flag) {}
... = A

Vì Luồng 2 chờ flag được thiết lập, nên quyền truy cập vào A trong Luồng 2 phải xảy ra sau và không đồng thời với, giao cho A trong Chuỗi 1. Do đó, không có cuộc đua dữ liệu nào trên A. Cuộc đua vào flag không được tính là một cuộc đua dữ liệu, vì các quyền truy cập biến động/nguyên tử không phải là "quyền truy cập bộ nhớ thông thường".

Cần triển khai để ngăn chặn hoặc ẩn việc sắp xếp lại bộ nhớ đủ để khiến mã như phép kiểm thử quỳ trước hoạt động như mong đợi. Điều này thường tạo ra các quyền truy cập vào bộ nhớ nguyên tử/dễ biến động đắt hơn đáng kể so với các lượt truy cập thông thường.

Mặc dù ví dụ trước là không có tính năng đua dữ liệu, sẽ khoá cùng với Object.wait() trong Java hoặc các biến điều kiện trong C/C++ thường cung cấp một giải pháp tốt hơn không liên quan đến việc đợi trong vòng lặp khi làm tiêu hao pin.

Khi tính năng sắp xếp lại bộ nhớ hiển thị

Việc lập trình không có cuộc đua dữ liệu thường giúp chúng ta không phải giải quyết một cách rõ ràng gặp phải sự cố khi sắp xếp lại quyền truy cập bộ nhớ. Tuy nhiên, có một vài trường hợp trong thứ tự sắp xếp lại sẽ hiển thị:
  1. Nếu chương trình của bạn gặp lỗi dẫn đến việc chạy đua dữ liệu ngoài ý muốn, các biến đổi phần cứng và trình biên dịch có thể hiển thị rõ ràng, đồng thời hành vi chương trình của bạn có thể gây bất ngờ. Ví dụ: nếu chúng tôi quên khai báo flag biến động trong ví dụ trước, Luồng 2 có thể thấy chưa khởi tạo A. Hoặc trình biên dịch có thể quyết định rằng cờ không thể có thể thay đổi trong vòng lặp của Thread 2 và biến đổi chương trình thành
    Chuỗi 1 Chuỗi 2
    A = ...
      flag = true
    reg0 = cờ; trong khi (!reg0) {}
    ... = A
    Khi gỡ lỗi, bạn có thể thấy vòng lặp này tiếp tục vĩnh viễn bất kể rằng flag là đúng.
  2. C++ cung cấp phương tiện để thư giãn rõ ràng tính nhất quán tuần tự ngay cả khi không có cuộc đua. Vận hành nguyên tử có thể nhận các đối số memory_order_... tường minh. Tương tự, Gói java.util.concurrent.atomic cung cấp tính năng nhiều cơ sở vật chất tương tự nhau, đáng chú ý là lazySet(). Và Java các lập trình viên đôi khi sử dụng các cuộc đua dữ liệu có chủ đích để đạt được hiệu quả tương tự. Tất cả những yếu tố này đều cải thiện hiệu suất đồng thời độ phức tạp của việc lập trình. Chúng tôi chỉ thảo luận ngắn gọn về chúng bên dưới.
  3. Một số mã C và C++ được viết theo kiểu cũ hơn, không phải hoàn toàn nhất quán với các tiêu chuẩn ngôn ngữ hiện tại, trong đó volatile biến được sử dụng thay vì các biến atomic và thứ tự bộ nhớ không được phép rõ ràng bằng cách chèn hàng rào hoặc rào cản. Bạn phải có lý do rõ ràng về quyền truy cập sắp xếp lại thứ tự cũng như tìm hiểu về các mô hình bộ nhớ phần cứng. Kiểu lập trình dọc theo các dòng này vẫn được sử dụng trong nhân Linux. Không nên được dùng trong các ứng dụng Android mới và cũng không được thảo luận thêm ở đây.

Thực hành

Việc gỡ lỗi các vấn đề về tính nhất quán của bộ nhớ có thể rất khó khăn. Nếu thiếu nguyên nhân khai báo khoá, atomic hoặc volatile một số mã để đọc dữ liệu cũ, bạn có thể không tìm hiểu lý do bằng cách kiểm tra vùng nhớ kết xuất bộ nhớ bằng trình gỡ lỗi. Vào thời điểm bạn có thể phát hành một truy vấn trình gỡ lỗi, lõi CPU có thể đã ghi nhận toàn bộ tập hợp truy cập và nội dung của bộ nhớ và thanh ghi CPU sẽ nằm trong trạng thái "không thể".

Việc không nên làm trong C

Ở đây, chúng tôi đưa ra một số ví dụ về mã không chính xác cùng với các cách đơn giản để khắc phục chúng. Trước khi làm việc đó, chúng ta cần thảo luận về cách sử dụng ngôn ngữ cơ bản của chúng tôi.

C/C++ và "volatile"

Khai báo volatile trong C và C++ là một công cụ dùng cho mục đích rất đặc biệt. Chúng ngăn trình biên dịch sắp xếp lại hoặc xoá volatile truy cập. Điều này có thể hữu ích cho việc truy cập mã truy cập vào thanh ghi thiết bị phần cứng, bộ nhớ được ánh xạ tới nhiều vị trí hoặc có liên quan đến setjmp. Nhưng C và C++ volatile, không giống như Java volatile không được thiết kế để giao tiếp theo luồng.

Trong C và C++, quyền truy cập vào volatile có thể được sắp xếp lại thứ tự bằng cách truy cập vào dữ liệu bất biến và sự đảm bảo về tính nguyên tử. Do đó, bạn không thể sử dụng volatile để chia sẻ dữ liệu giữa các luồng trong mã di động, ngay cả trên bộ đơn xử lý. C volatile thường không ngăn quyền truy cập sắp xếp lại theo phần cứng, do đó nó thậm chí còn kém hữu ích hơn trong môi trường SMP đa luồng. Đây là lý do C11 và C++11 hỗ trợ Đối tượng atomic. Thay vào đó, bạn nên sử dụng các mã đó.

Nhiều mã C và C++ cũ vẫn lạm dụng volatile đối với luồng giao tiếp. Điều này thường hoạt động chính xác đối với dữ liệu phù hợp với trong một thanh ghi máy, miễn là có hàng rào rõ ràng hoặc trong các trường hợp trong đó thứ tự bộ nhớ không quan trọng. Tuy nhiên, phương pháp này không đảm bảo sẽ mang lại hiệu quả chính xác với các trình biên dịch trong tương lai.

Ví dụ

Trong hầu hết các trường hợp, bạn nên dùng một khoá (chẳng hạn như pthread_mutex_t hoặc C++11 std::mutex) thay vì một hoạt động nguyên tử, nhưng chúng tôi sẽ sử dụng phương pháp sau để minh hoạ cách chúng sẽ được sử dụng trong tình huống thực tế.

MyThing* gGlobalThing = NULL;  // Wrong!  See below.
void initGlobalThing()    // runs in Thread 1
{
    MyStruct* thing = malloc(sizeof(*thing));
    memset(thing, 0, sizeof(*thing));
    thing->x = 5;
    thing->y = 10;
    /* initialization complete, publish */
    gGlobalThing = thing;
}
void useGlobalThing()    // runs in Thread 2
{
    if (gGlobalThing != NULL) {
        int i = gGlobalThing->x;    // could be 5, 0, or uninitialized data
        ...
    }
}

Ý tưởng ở đây là chúng ta phân bổ một cấu trúc, khởi tạo các trường của cấu trúc và cuối cùng chúng tôi "xuất bản" tệp đó bằng cách lưu trữ tệp đó trong một biến toàn cục. Vào thời điểm đó, bất kỳ luồng nào khác cũng có thể thấy luồng này, nhưng không sao vì nó được khởi chạy hoàn toàn, đúng không?

Vấn đề là có thể quan sát thấy cửa hàng của gGlobalThing trước khi các trường này được khởi tạo, thường là do trình biên dịch hoặc đơn vị xử lý đã sắp xếp lại cửa hàng thành gGlobalThingthing->x Một chuỗi khác đọc từ thing->x có thể thấy 5, 0 hoặc thậm chí dữ liệu chưa khởi tạo.

Vấn đề cốt lõi ở đây là cuộc đua dữ liệu trên gGlobalThing. Nếu Luồng 1 gọi initGlobalThing() trong khi Luồng 2 gọi cuộc gọi useGlobalThing(), gGlobalThing có thể là đọc trong khi được viết.

Bạn có thể khắc phục vấn đề này bằng cách khai báo gGlobalThing là nguyên tử. Trong C++11:

atomic<MyThing*> gGlobalThing(NULL);

Điều này đảm bảo hoạt động ghi sẽ hiển thị với các luồng khác theo thứ tự thích hợp. Việc này cũng đảm bảo ngăn chặn một số lỗi khác được phép, nhưng khó có thể xảy ra trên thực tế Phần cứng Android. Chẳng hạn, mã này đảm bảo rằng chúng ta không thể thấy Con trỏ gGlobalThing chỉ được ghi một phần.

Những việc không nên làm trong Java

Do chưa thảo luận về một số tính năng ngôn ngữ Java có liên quan, nên chúng tôi sẽ lấy một hãy xem nhanh các sản phẩm đó trước.

Về mặt kỹ thuật, Java không yêu cầu mã phải không bị theo dõi dữ liệu (data-race). Và đó là một số ít mã Java được viết rất cẩn thận và hoạt động chính xác khi đứng trước các cuộc đua dữ liệu. Tuy nhiên, việc viết mã như vậy là vô cùng phức tạp và chúng tôi chỉ thảo luận ngắn gọn dưới đây. Quan trọng tệ hơn nữa, các chuyên gia xác định ý nghĩa của đoạn mã này không còn tin rằng quy cách là chính xác. (Thông số kỹ thuật phù hợp với chế độ data-race-free .)

Giờ đây, chúng ta sẽ tuân thủ mô hình data-race-free mà Java cung cấp về cơ bản là các đảm bảo tương tự như C và C++. Xin nhắc lại rằng ngôn ngữ này cung cấp một số dữ liệu gốc nới lỏng tính nhất quán tuần tự một cách rõ ràng, đặc biệt là Cuộc gọi lazySet()weakCompareAndSet() trong java.util.concurrent.atomic. Cũng giống như C và C++, chúng ta tạm thời sẽ bỏ qua các thành phần này.

"được đồng bộ hoá" của Java và "biến động" từ khóa

Từ khoá “được đồng bộ hoá” cung cấp tính năng khoá tích hợp sẵn của ngôn ngữ Java cơ chế. Mỗi đối tượng đều có một "màn hình" liên kết có thể được dùng để cung cấp quyền truy cập loại trừ lẫn nhau. Nếu 2 luồng cố gắng "đồng bộ hoá" trên cùng một đối tượng, một trong số chúng sẽ đợi cho đến khi đối tượng còn lại hoàn tất.

Như chúng tôi đã đề cập ở trên, volatile T của Java là tương tự của atomic<T> của C++11. Quyền truy cập đồng thời vào Các trường volatile được phép và việc này không dẫn đến việc đua dữ liệu. Bỏ qua lazySet() và cộng sự. và cuộc đua dữ liệu, công việc của máy ảo Java là đảm bảo rằng kết quả vẫn xuất hiện tuần tự nhất quán.

Cụ thể, nếu luồng 1 ghi vào trường volatile và luồng 2 sau đó đọc từ cùng trường đó và thấy đoạn mã mới được viết thì luồng 2 cũng được đảm bảo xem tất cả các lượt ghi trước đó được thực hiện bởi chuỗi 1. Về hiệu ứng bộ nhớ, việc ghi vào biến động tương tự như bản phát hành màn hình và việc đọc từ một biến động giống như thu nạp màn hình.

Có một điểm khác biệt đáng chú ý so với atomic của C++: Nếu chúng ta viết volatile int x; trong Java, thì x++ giống với x = x + 1; nó thực hiện tải nguyên tử, tăng kết quả, sau đó thực hiện giảm tải nguyên tử của bạn. Không giống như C++, số gia tăng tổng thể không phải là số nguyên tử. Thay vào đó, toán tử tăng nguyên tử được cung cấp bởi java.util.concurrent.atomic.

Ví dụ

Sau đây là một cách triển khai đơn giản, không chính xác của bộ đếm đơn điệu: (Java lý thuyết và thực hành: Quản lý sự biến động).

class Counter {
    private int mValue;
    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

Giả sử get()incr() được gọi từ nhiều và chúng ta muốn đảm bảo rằng mọi luồng đều thấy số lượng hiện tại khi get() sẽ được gọi. Vấn đề rõ ràng nhất là mValue++ thực ra là 3 toán tử:

  1. reg = mValue
  2. reg = reg + 1
  3. mValue = reg

Nếu 2 luồng thực thi đồng thời trong incr(), một trong bản cập nhật có thể bị mất. Để tăng số lượng nguyên tử, chúng ta cần khai báo incr() "được đồng bộ hóa".

Tuy nhiên, tính năng này vẫn gặp lỗi, đặc biệt là trên tính năng SMP. Vẫn còn một cuộc đua dữ liệu, trong đó get() có thể truy cập mValue đồng thời với incr(). Trong quy tắc Java, lệnh gọi get() có thể là có vẻ được sắp xếp lại so với mã khác. Ví dụ: nếu chúng ta đọc hai các bộ đếm trong một hàng, kết quả có thể dường như không nhất quán vì các lệnh gọi get() mà chúng ta đã sắp xếp lại, theo phần cứng hoặc trình biên dịch. Chúng ta có thể khắc phục vấn đề này bằng cách khai báo get() là đã đồng bộ hoá. Với thay đổi này, mã rõ ràng là chính xác.

Rất tiếc, chúng tôi đã giới thiệu khả năng tranh chấp khoá, có thể cản trở hiệu suất. Thay vì khai báo get() là đã đồng bộ hoá, chúng ta có thể khai báo mValue có trạng thái "volatile". (Lưu ý incr() vẫn phải sử dụng synchronizemValue++ không phải là một toán tử đơn lẻ.) Điều này cũng giúp tránh được tất cả các cuộc đua dữ liệu, vì vậy tính nhất quán tuần tự vẫn được duy trì. incr() sẽ chậm hơn vì phải giám sát cả tính năng nhập/thoát và mức hao tổn liên quan đến cửa hàng dễ biến động, nhưng get() sẽ nhanh hơn, vì vậy, ngay cả khi không có tranh chấp thì đây là chiến thắng nếu số lượt đọc nhiều hơn số lượng ghi. (Xem thêm AtomicInteger để biết cách xoá khối đã đồng bộ hoá.)

Sau đây là một ví dụ khác có hình thức tương tự như các ví dụ C trước đó:

class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;
    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }
    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

Mã này có cùng vấn đề với mã C, cụ thể là có một cuộc đua dữ liệu vào sGoodies. Do đó, bài tập Có thể quan sát thấy sGoodies = goods trước khi khởi chạy trong goods. Nếu bạn khai báo sGoodies bằng phần tử volatile từ khóa, tính nhất quán tuần tự được khôi phục và mọi thứ sẽ hoạt động như mong đợi.

Lưu ý rằng chỉ có tham chiếu sGoodies là tự biến động. Chiến lược phát hành đĩa đơn có quyền truy cập vào các trường bên trong nó. Sau khi sGoodies volatile và thứ tự bộ nhớ được giữ nguyên đúng cách, các trường Không thể truy cập đồng thời. Câu lệnh z = sGoodies.x sẽ thực hiện tải biến động MyClass.sGoodies tiếp theo là tải không biến động sGoodies.x. Nếu bạn là người bản địa tham chiếu MyGoodies localGoods = sGoodies thì z = localGoods.x tiếp theo sẽ không thực hiện bất kỳ tải tự tính nào.

Một thành ngữ phổ biến hơn trong lập trình Java là "kiểm tra kỹ" đang khoá":

class MyClass {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

Ý tưởng là chúng ta muốn có một thực thể duy nhất của Helper đối tượng liên kết với bản sao của MyClass. Chúng ta chỉ phải tạo nên chúng tôi tạo và trả lại mã đó thông qua một getHelper() riêng . Để tránh tình huống tương tranh trong đó 2 luồng tạo thực thể, chúng ta cần phải đồng bộ hoá việc tạo đối tượng. Tuy nhiên, chúng tôi không muốn trả chi phí khối "được đồng bộ hoá" trên mọi cuộc gọi nên chúng ta chỉ thực hiện phần đó nếu helper hiện đang rỗng.

Hàm này có một cuộc đua dữ liệu trên trường helper. Có thể đặt đồng thời với helper == null trong một chuỗi khác.

Để biết quy trình này diễn ra như thế nào, hãy cân nhắc cùng một mã được viết lại một chút, như thể mã được biên dịch sang ngôn ngữ giống C (Tôi đã thêm một vài trường số nguyên để thể hiện Helper’s hoạt động của hàm khởi tạo):

if (helper == null) {
    synchronized() {
        if (helper == null) {
            newHelper = malloc(sizeof(Helper));
            newHelper->x = 5;
            newHelper->y = 10;
            helper = newHelper;
        }
    }
    return helper;
}

Không có gì có thể ngăn chặn phần cứng hoặc trình biên dịch từ việc sắp xếp lại cửa hàng sang helper cùng với các lượt chuyển đổi sang x/y trường. Một chuỗi khác có thể tìm thấy helper không có giá trị rỗng nhưng các trường chưa được thiết lập và sẵn sàng để sử dụng. Để biết thêm thông tin chi tiết và các chế độ lỗi khác, hãy xem phần “"Kiểm tra kép" Đường liên kết đến phần Khai báo "Khoá bị hỏng" trong phần phụ lục để biết thêm thông tin chi tiết, hoặc 71 (“Sử dụng khởi động từng phần một cách thận trọng”) trong Java hiệu quả của Josh Bloch, Phiên bản thứ 2..

Có hai cách để khắc phục vấn đề này:

  1. Thực hiện thao tác đơn giản và xoá yêu cầu kiểm tra bên ngoài. Điều này giúp đảm bảo rằng chúng tôi sẽ không bao giờ kiểm tra giá trị của helper bên ngoài một khối đã đồng bộ hoá.
  2. Khai báo helper biến động. Với một thay đổi nhỏ này, mã trong Ví dụ J-3 sẽ hoạt động chính xác trên Java 1.5 trở lên. (Bạn nên lấy để tự thuyết phục rằng đây là sự thật).

Dưới đây là một hình minh hoạ khác về hành vi của volatile:

class MyClass {
    int data1, data2;
    volatile int vol1, vol2;
    void setValues() {    // runs in Thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }
    void useValues() {    // runs in Thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
}

Nhìn vào useValues(), nếu Luồng 2 chưa quan sát thấy cập nhật lên vol1, thì hệ thống sẽ không thể biết liệu data1 hay data2 đã được đặt. Sau khi thấy nội dung cập nhật lên vol1, ứng dụng biết rằng bạn có thể truy cập an toàn vào data1 và đọc chính xác mà không cần tạo ra một cuộc đua dữ liệu. Tuy nhiên, không thể đưa ra bất kỳ giả định nào về data2, vì cửa hàng đó được thực hiện sau khi cửa hàng biến động.

Lưu ý không thể dùng volatile để ngăn việc sắp xếp lại thứ tự của các quyền truy cập bộ nhớ khác chạy đua với nhau. Chúng tôi không đảm bảo tạo một lệnh hàng rào bộ nhớ máy. Có thể dùng mã này để ngăn dữ liệu sẽ chạy bằng cách chỉ thực thi mã khi một luồng khác đáp ứng điều kiện nhất định.

Việc nên làm

Trong C/C++, hãy ưu tiên C++11 các lớp đồng bộ hoá, chẳng hạn như std::mutex. Nếu không, hãy sử dụng các toán tử pthread tương ứng. Những cách này bao gồm hàng rào bộ nhớ thích hợp, cung cấp kết quả chính xác (tuần tự nhất quán trừ khi có quy định khác) và hành vi hiệu quả trên tất cả phiên bản nền tảng Android. Hãy sử dụng các thành phần đó chính xác. Ví dụ: hãy nhớ rằng biến điều kiện chờ có thể được gây ra một cách ngẫu nhiên trả về mà không được báo hiệu và do đó sẽ xuất hiện trong một vòng lặp.

Tốt nhất là nên tránh sử dụng trực tiếp các hàm nguyên tử, trừ phi cấu trúc dữ liệu mà bạn đang triển khai cực kỳ đơn giản, như một bộ đếm. Khoá và việc mở khoá một mutex pthread đòi hỏi một thao tác nguyên tử duy nhất, và thường có chi phí thấp hơn một lần thiếu bộ nhớ đệm, nếu không có tranh chấp này, vì vậy, bạn sẽ không tiết kiệm được nhiều bằng cách thay thế cuộc gọi mutex bằng hoạt động nguyên tử. Yêu cầu thiết kế không khoá cho các cấu trúc dữ liệu không quan trọng cẩn thận hơn nhiều để đảm bảo rằng thao tác cấp cao hơn trên cấu trúc dữ liệu có vẻ như nguyên tử (là tổng thể, không chỉ là các mảnh rõ ràng ở dạng nguyên tử của chúng).

Nếu bạn sử dụng toán tử nguyên tử, hãy thư giãn thứ tự bằng memory_order... hoặc lazySet() có thể có hiệu suất nhiều thuận lợi, nhưng đòi hỏi sự hiểu biết sâu sắc hơn những gì chúng tôi đã chia sẻ từ trước đến nay. Một phần lớn mã hiện tại đang sử dụng những quảng cáo này được phát hiện có lỗi sau khi thực tế. Hãy tránh những cách này nếu có thể. Nếu trường hợp sử dụng của bạn không hoàn toàn phù hợp với một trong những trường hợp ở phần tiếp theo, đảm bảo bạn là chuyên gia hoặc đã tham khảo ý kiến của một chuyên gia.

Tránh sử dụng volatile để giao tiếp theo luồng trong C/C++.

Trong Java, tốt nhất là các bài toán đồng thời được giải quyết bằng bằng cách sử dụng một lớp tiện ích thích hợp từ gói java.util.concurrent. Mã được viết tốt và tốt thử nghiệm trên SMP.

Có lẽ điều an toàn nhất bạn có thể làm là làm cho các đối tượng của bạn bất biến. Đồ vật từ các lớp như dữ liệu giữ Chuỗi và Số nguyên của Java mà không thể thay đổi được sau khi đối tượng được tạo, tránh mọi khả năng xảy ra các cuộc đua dữ liệu trên các đối tượng đó. Cuốn sách Có hiệu lực Java, Ấn bản thứ 2 có hướng dẫn cụ thể trong "Mục 15: Giảm thiểu khả năng biến đổi". Ghi chú trong cụ thể tầm quan trọng của việc khai báo các trường Java là "cuối cùng" (Bloch).

Ngay cả khi một đối tượng là không thể thay đổi, hãy nhớ rằng việc truyền đạt đối tượng đó với đối tượng khác mà không có bất kỳ loại đồng bộ hoá nào là một cuộc đua dữ liệu. Điều này đôi khi có thể có thể chấp nhận được trong Java (xem bên dưới), nhưng đòi hỏi phải hết sức cẩn trọng và có thể dẫn đến mã dễ gây gián đoạn. Nếu điều đó không quá quan trọng về hiệu suất, hãy thêm Khai báo volatile. Trong C++, việc truyền đạt một con trỏ hoặc tham chiếu đến một đối tượng bất biến mà không có đồng bộ hoá phù hợp, giống như mọi cuộc đua dữ liệu, vẫn là một lỗi. Trong trường hợp này, khả năng hợp lý là dẫn đến sự cố gián đoạn vì ví dụ: luồng nhận có thể thấy một bảng phương thức chưa khởi tạo con trỏ do sắp xếp lại cửa hàng.

Nếu không có lớp thư viện hiện có hoặc lớp không thể thay đổi phù hợp, câu lệnh Java synchronized hoặc C++ lock_guard / unique_lock nên được dùng để bảo vệ quyền truy cập vào bất kỳ trường nào mà nhiều luồng có thể truy cập. Nếu mutex không phù hợp với tình huống của bạn, bạn nên khai báo các trường dùng chung volatile hoặc atomic, nhưng bạn phải thật cẩn thận hiểu tương tác giữa các luồng. Những nội dung khai báo này sẽ không giúp bạn tránh khỏi các lỗi lập trình đồng thời phổ biến, mà chúng sẽ giúp bạn tránh những lỗi bí ẩn liên quan đến việc tối ưu hoá trình biên dịch và SMP rủi ro.

Bạn nên tránh "xuất bản" tham chiếu đến một đối tượng, tức là làm cho đối tượng đó khả dụng với đối tượng khác luồng, trong hàm khởi tạo của nó. Điều này ít quan trọng hơn trong C++ hoặc nếu bạn tiếp tục "cuộc đua dữ liệu không" của chúng tôi trong Java. Tuy nhiên, đây luôn là lời khuyên hữu ích, nếu mã Java của bạn chạy trong các ngữ cảnh khác mà mô hình bảo mật Java quan trọng và không đáng tin cậy có thể tạo ra một cuộc đua dữ liệu bằng cách truy cập vào dữ liệu "bị rò rỉ" đó tham chiếu đối tượng. Điều cũng rất quan trọng nếu bạn chọn bỏ qua cảnh báo của chúng tôi và sử dụng một số kỹ thuật trong phần tiếp theo. Xem (Kỹ thuật xây dựng an toàn trong Java) để biết chi tiết

Thông tin khác về đơn đặt hàng bộ nhớ yếu

C++11 trở lên cung cấp cơ chế rõ ràng để nới lỏng trình tự đảm bảo tính nhất quán cho các chương trình không có cuộc đua dữ liệu. Ca từ phản cảm memory_order_relaxed, memory_order_acquire (tải và memory_order_release(chỉ lưu trữ) đối số cho nguyên tử mỗi hoạt động đều đảm bảo hiệu quả hơn so với mặc định, thường là ngầm ẩn, memory_order_seq_cst. memory_order_acq_rel cung cấp cả memory_order_acquirememory_order_release đảm bảo ghi sửa đổi đọc nguyên tử các toán tử. memory_order_consume là chưa đủ được chỉ định hoặc triển khai rõ ràng để trở nên hữu ích và tạm thời nên được bỏ qua.

Các phương thức lazySet trong Java.util.concurrent.atomic tương tự như các cửa hàng C++ memory_order_release. Java biến thông thường đôi khi được dùng để thay thế cho memory_order_relaxed quyền truy cập, mặc dù họ thực sự có quyền truy cập yếu hơn. Không giống như C++, không có cơ chế thực sự nào để không theo thứ tự truy cập vào các biến được khai báo là volatile.

Thường thì bạn nên tránh những chế độ này trừ phi có lý do cấp bách về hiệu suất sử dụng chúng. Trên các kiến trúc máy được sắp xếp không theo thứ tự như ARM, việc sử dụng các kiến trúc này sẽ thường được lưu theo thứ tự khoảng vài chục chu kỳ máy cho mỗi hoạt động ở nguyên tử. Trên x86, hiệu suất đạt được sẽ chỉ áp dụng cho các cửa hàng và có thể sẽ ít hơn đáng chú ý. Ngược lại, lợi ích có thể giảm khi số lượng người dùng cốt lõi lớn hơn, khi hệ thống bộ nhớ trở thành một yếu tố hạn chế.

Ngữ nghĩa đầy đủ của các nguyên tử được sắp xếp yếu rất phức tạp. Nhìn chung, các tính năng này yêu cầu hiểu chính xác các quy tắc ngôn ngữ, chúng tôi sẽ không đi vào đây. Ví dụ:

  • Trình biên dịch hoặc phần cứng có thể di chuyển memory_order_relaxed truy cập vào (nhưng không nằm ngoài) một phần quan trọng được giới hạn bởi khoá thu nạp và phát hành. Điều này có nghĩa là hai memory_order_relaxed cửa hàng có thể xuất hiện không đúng thứ tự, ngay cả khi chúng được tách riêng bởi một phần quan trọng.
  • Một biến Java thông thường có thể xuất hiện khi bị lạm dụng làm bộ đếm dùng chung sang một luồng khác để giảm, mặc dù luồng này chỉ được tăng lên theo một đơn vị chuỗi khác. Nhưng điều này không đúng đối với nguyên tử C++ memory_order_relaxed.

Chúng tôi muốn nhắc nhở rằng ở đây, chúng tôi đưa ra một số thành ngữ có vẻ bao hàm nhiều cách sử dụng trường hợp các nguyên tử có thứ tự yếu. Nhiều phần tử trong số này chỉ áp dụng được cho C++.

Đường vào không phải đua xe

Khá phổ biến rằng biến là nguyên tử vì đôi khi đọc đồng thời với thao tác ghi, nhưng không phải mọi quyền truy cập đều gặp vấn đề này. Ví dụ: một biến có thể cần ở tỷ lệ nguyên tử vì nội dung được đọc bên ngoài phần quan trọng, nhưng tất cả các bản cập nhật được bảo vệ bằng khoá. Trong trường hợp đó, lượt đọc được bảo vệ bằng cùng một khoá không thể thực hiện tương tự vì không thể ghi đồng thời. Trong trường hợp như vậy, quyền truy cập không chạy đua (tải trong trường hợp này), có thể được chú thích bằng memory_order_relaxed mà không thay đổi độ chính xác của mã C++. Quá trình triển khai khoá đã thực thi thứ tự bộ nhớ bắt buộc đối với quyền truy cập của các chuỗi khác và memory_order_relaxed chỉ định rằng về cơ bản, không cần phải có ràng buộc bổ sung nào khác về thứ tự thực thi để truy cập vào nguyên tử.

Không có sự tương đồng thực sự nào trong Java.

Kết quả không được dựa vào tính chính xác

Nếu chúng ta chỉ sử dụng tải dựa trên dữ liệu ngẫu nhiên để tạo gợi ý, thì điều này thường chấp nhận được không thực thi bất kỳ thứ tự bộ nhớ nào cho tải. Nếu giá trị là không đáng tin cậy, chúng tôi cũng không thể sử dụng kết quả đó để suy ra bất kỳ điều gì một cách đáng tin cậy các biến khác. Do đó, không sao nếu thứ tự bộ nhớ không được đảm bảo và tải là được cung cấp cùng với một đối số memory_order_relaxed.

Điểm chung thực thể của vấn đề này là việc sử dụng C++ compare_exchange để thay thế x theo tỷ lệ bằng f(x). Tải ban đầu của x để tính toán f(x) không cần phải đáng tin cậy. Nếu chúng tôi nhầm lẫn, compare_exchange sẽ không thành công và chúng tôi sẽ thử lại. Bạn có thể sử dụng tải ban đầu của x một đối số memory_order_relaxed; chỉ có thứ tự bộ nhớ cho compare_exchange thực tế.

Dữ liệu được sửa đổi nguyên tử nhưng chưa đọc

Đôi khi, nhiều luồng cũng sửa đổi song song dữ liệu, nhưng chưa được kiểm tra cho đến khi việc tính toán song song hoàn tất. Tốt ví dụ về bộ đếm được tăng dần theo tỷ lệ (ví dụ: sử dụng fetch_add() trong C++ hoặc atomic_fetch_add_explicit() trong C) theo nhiều luồng song song, nhưng là kết quả của các lệnh gọi này sẽ luôn bị bỏ qua. Giá trị kết quả chỉ được đọc ở cuối, sau khi hoàn tất tất cả các bản cập nhật.

Trong trường hợp này, không có cách nào để biết liệu có phải truy cập vào dữ liệu này hay không được sắp xếp lại, do đó mã C++ có thể sử dụng memory_order_relaxed đối số.

Một ví dụ phổ biến về trường hợp này là bộ đếm sự kiện đơn giản. Vì rất phổ biến, nên bạn nên quan sát trường hợp này:

  • Việc sử dụng memory_order_relaxed sẽ cải thiện hiệu suất, nhưng có thể không giải quyết được vấn đề quan trọng nhất về hiệu suất: Mọi bản cập nhật yêu cầu quyền truy cập độc quyền vào dòng bộ nhớ đệm chứa bộ đếm. Chiến dịch này dẫn đến thiếu bộ nhớ đệm mỗi khi có một luồng mới truy cập vào bộ đếm. Nếu cập nhật thường xuyên và luân phiên giữa các luồng, sẽ nhanh hơn nhiều tránh phải cập nhật bộ đếm dùng chung mỗi lần bằng cách ví dụ: sử dụng bộ đếm luồng cục bộ và tổng hợp chúng ở cuối.
  • Kỹ thuật này có thể kết hợp với phần trước: Có thể đọc đồng thời giá trị gần đúng và giá trị không đáng tin cậy trong khi cập nhật, cho tất cả các thao tác sử dụng memory_order_relaxed. Nhưng điều quan trọng là phải coi giá trị thu được là hoàn toàn không đáng tin cậy. Chỉ vì số lượng dường như đã tăng lên một lần nhưng không có nghĩa là có thể tính vào một chuỗi khác để đạt đến điểm này mà tại đó bước tăng đã được thực hiện. Thay vào đó, giá trị gia tăng này có thể có được đặt lại bằng mã trước đó. (Đối với trường hợp tương tự, chúng tôi đã đề cập trước đó, C++ đảm bảo rằng lần tải thứ hai của bộ đếm như vậy sẽ không trả về một giá trị nhỏ hơn lần tải trước đó trong cùng một luồng. Trừ phi tất nhiên là bộ đếm đã bị tràn.)
  • Thông thường, bạn sẽ thấy mã cố gắng tính toán gần đúng đếm các giá trị bằng cách thực hiện đọc và ghi nguyên tử (hoặc không) riêng lẻ, nhưng không làm tăng mức tăng như một nguyên tử. Lý lẽ thông thường là giá trị này là "đủ gần" để xem bộ đếm hiệu suất hoặc các mục tương tự. Thường thì không phải vậy. Khi quá trình cập nhật đủ thường xuyên (trường hợp mà bạn có thể quan tâm), thì một phần lớn số liệu thường là thua. Trên thiết bị lõi tứ, hơn một nửa số lượng dữ liệu có thể thường bị mất. (Bài tập dễ thực hiện: xây dựng kịch bản 2 chuỗi trong đó bộ đếm là cập nhật cả triệu lần, nhưng giá trị bộ đếm cuối cùng là một.)

Thông tin giao tiếp đơn giản do cờ

Một cửa hàng memory_order_release (hoặc thao tác đọc-sửa đổi-ghi) đảm bảo rằng nếu sau đó một lượt tải memory_order_acquire (hay thao tác đọc-sửa đổi-ghi) đọc giá trị đã ghi, sau đó nó sẽ cũng quan sát mọi kho hàng (bình thường hoặc nguyên tử) trước thời điểm Một cửa hàng ở memory_order_release. Ngược lại, bất kỳ lượt tải nào trước memory_order_release sẽ không nhận thấy bất kỳ các kho lưu trữ tuân theo tải memory_order_acquire. Không giống như memory_order_relaxed, thao tác này cho phép các thao tác nguyên tử như vậy được dùng để thông báo tiến trình của một luồng với luồng khác.

Ví dụ: chúng ta có thể viết lại ví dụ về khoá được kiểm tra kỹ ở trên trong C++ dưới dạng

class MyClass {
  private:
    atomic<Helper*> helper {nullptr};
    mutex mtx;
  public:
    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper == nullptr) {
        lock_guard<mutex> lg(mtx);
        myHelper = helper.load(memory_order_relaxed);
        if (myHelper == nullptr) {
          myHelper = new Helper();
          helper.store(myHelper, memory_order_release);
        }
      }
      return myHelper;
    }
};

Cửa hàng tải và phát hành thu nạp đảm bảo rằng nếu chúng ta thấy giá trị không rỗng helper, thì chúng ta cũng sẽ thấy các trường của lớp này được khởi chạy chính xác. Chúng tôi cũng đã kết hợp quan sát trước đó rằng tải không phải là chạy đua có thể sử dụng memory_order_relaxed.

Một lập trình viên Java có thể hình dung ra là helper dưới dạng một java.util.concurrent.atomic.AtomicReference<Helper> và dùng lazySet() làm cửa hàng phát hành. Tải trọng các thao tác sẽ tiếp tục sử dụng các lệnh gọi get() thuần tuý.

Trong cả hai trường hợp, việc tinh chỉnh hiệu suất của chúng tôi tập trung vào việc khởi chạy mà không có khả năng là yếu tố quan trọng về hiệu suất. Một hình thức xâm phạm dễ đọc hơn có thể là:

    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper != nullptr) {
        return myHelper;
      }
      lock_guard&ltmutex> lg(mtx);
      if (helper == nullptr) {
        helper = new Helper();
      }
      return helper;
    }

Lựa chọn này cung cấp cùng một đường dẫn nhanh, nhưng các khu nghỉ dưỡng mặc định, tuần tự nhất quán, các hoạt động trên chậm không quan trọng về hiệu suất đường dẫn.

Ngay cả ở đây, helper.load(memory_order_acquire) có khả năng tạo ra cùng một mã trên ứng dụng Android hiện được hỗ trợ dưới dạng một tham chiếu đơn giản (có tính nhất quán tuần tự) đến helper Phương pháp tối ưu hoá thực sự có lợi nhất ở đây có thể là việc giới thiệu myHelper để loại bỏ tải thứ hai, mặc dù trình biên dịch tương lai có thể tự động thực hiện điều đó.

Việc thêm/phát hành không ngăn cản việc cửa hàng xuất hiện bị trễ và không đảm bảo rằng cửa hàng sẽ hiển thị với các chuỗi tin nhắn khác theo thứ tự nhất quán. Do đó, công cụ này không hỗ trợ nhưng mô hình lập trình khá phổ biến được minh hoạ bằng cách loại trừ lẫn nhau của Dekker thuật toán: Trước tiên, tất cả các luồng đều đặt một cờ cho biết rằng họ muốn thực hiện một thứ gì đó; nếu một luồng t thì nhận thấy rằng không có luồng nào khác cố gắng làm điều gì đó, Gemini có thể tiến hành một cách an toàn, khi biết rằng ở đó sẽ không bị can thiệp. Sẽ không có chuỗi nào khác có thể tiếp tục vì cờ của t vẫn được đặt. Không thực hiện được nếu cờ này được truy cập bằng thứ tự thu nạp/phát hành, vì cách này không ngăn người khác nhìn thấy cờ của một chuỗi sau khi họ đã đã xử lý nhầm. memory_order_seq_cst mặc định ngăn chặn điều đó.

Các trường không thể thay đổi

Nếu một trường đối tượng được khởi tạo trong lần sử dụng đầu tiên và sau đó không bao giờ thay đổi, có thể khởi chạy và sau đó đọc mã đó bằng cách sử dụng một cách yếu lượt truy cập theo thứ tự. Trong C++, giá trị này có thể được khai báo là atomic và truy cập bằng memory_order_relaxed hoặc trong Java, có thể được khai báo mà không cần volatile và được truy cập mà không cần các biện pháp đặc biệt. Điều này yêu cầu tất cả các yêu cầu lưu giữ dữ liệu sau đây:

  • Phải có thể nhận biết được từ giá trị của chính trường đó liệu đã được khởi tạo hay chưa. Để truy cập vào trường này, giá trị kiểm tra và trả về của đường dẫn nhanh chỉ được đọc trường một lần. Trong Java, thuật ngữ sau là một phần không thể thiếu. Ngay cả khi kiểm tra trường khi khởi tạo, lần tải thứ hai có thể đọc giá trị chưa khởi tạo trước đó. Trong C++ lời nhắc "đọc một lần" quy tắc chỉ là một phương pháp hay.
  • Cả quá trình khởi chạy và tải tiếp theo đều phải là nguyên tử, trong đó, các bản cập nhật một phần sẽ không hiển thị. Đối với Java, trường không được là long hoặc double. Đối với C++, yêu cầu chỉ định nguyên tử; việc xây dựng mã tại chỗ sẽ không hiệu quả, vì cấu trúc của atomic không phải là một nguyên tử.
  • Các quá trình khởi chạy lặp lại phải an toàn vì nhiều luồng có thể đọc đồng thời giá trị chưa khởi tạo. Trong C++, việc này thường theo sau từ mục "có thể sao chép ít" áp dụng cho tất cả loại nguyên tử; các loại có con trỏ được sở hữu lồng nhau sẽ đòi hỏi sắp xếp trong hàm khởi tạo sao chép và không thể sao chép được. Đối với Java, có thể chấp nhận một số loại tham chiếu sau đây:
  • Tham chiếu Java được giới hạn ở các loại không thể thay đổi chỉ chứa cuối cùng mới. Không được xuất bản hàm khởi tạo thuộc loại không thể thay đổi tham chiếu đến đối tượng. Trong trường hợp này, các quy tắc của trường cuối cùng trong Java đảm bảo rằng nếu độc giả xem tệp tham chiếu, họ cũng sẽ thấy các trường cuối cùng đã khởi tạo. C++ không có điểm tương đồng với các quy tắc này và con trỏ đến các đối tượng được sở hữu cũng không được chấp nhận vì lý do này (trong ngoài việc vi phạm chính sách "có thể sao chép không đáng kể" ).

Ghi chú kết thúc

Mặc dù tài liệu này không chỉ đơn thuần là vết xước, nhưng nó không quản lý nhiều hơn một lỗ hổng quá mức. Đây là một chủ đề rất rộng và sâu. Hơi nhiều các khía cạnh để tìm hiểu thêm:

  • Mô hình bộ nhớ Java và C++ thực tế được biểu thị dưới dạng Mối quan hệ xảy ra trước chỉ định thời điểm đảm bảo hai hành động xảy ra theo thứ tự nhất định. Khi xác định một cuộc đua dữ liệu, chúng tôi không chính thức nói về hai hoạt động truy cập bộ nhớ diễn ra "cùng một lúc". Chính thức, điều này được định nghĩa là không có sự kiện nào xảy ra trước sự kiện còn lại. Hướng dẫn tìm hiểu định nghĩa thực tế về những điều xảy ra trước đâyđồng bộ hoá-with trong Mô hình bộ nhớ Java hoặc C++. Mặc dù khái niệm trực quan là "đồng thời" thường tốt đầy đủ, các định nghĩa này mang tính hướng dẫn, đặc biệt nếu bạn đang dự tính sử dụng các toán tử nguyên tử có thứ tự yếu trong C++. (Thông số kỹ thuật Java hiện tại chỉ xác định lazySet() rất thân mật.)
  • Tìm hiểu những trình biên dịch được phép và không được phép làm khi sắp xếp lại mã. (Thông số kỹ thuật JSR-133 có một số ví dụ tiêu biểu về những chuyển đổi pháp lý dẫn đến kết quả không mong muốn).
  • Tìm hiểu cách viết các lớp không thay đổi được trong Java và C++. (Và còn nhiều lựa chọn khác thay vì chỉ "không thay đổi bất cứ điều gì sau khi xây dựng".)
  • Nội bộ hoá các đề xuất trong phần Đồng thời của bài viết Hiệu quả Java, phiên bản thứ 2. (Ví dụ: bạn nên tránh gọi các phương thức có nghĩa là sẽ bị ghi đè khi ở bên trong khối được đồng bộ hoá.)
  • Hãy đọc qua API java.util.concurrentjava.util.concurrent.atomic để xem các API có sẵn. Cân nhắc sử dụng các chú giải đồng thời như @ThreadSafe@GuardedBy (từ net.jcip.annotations).

Phần Đọc thêm trong phần phụ lục có đường liên kết đến các tài liệu và trang web sẽ giúp bạn hiểu rõ hơn về những chủ đề này.

Phụ lục

Triển khai các cửa hàng đồng bộ hoá

(Đây không phải là điều mà hầu hết các lập trình viên sẽ tự tìm thấy, nhưng cuộc thảo luận lại mang tính sáng tạo.)

Đối với các loại tích hợp nhỏ như int và phần cứng được hỗ trợ bởi Android, các hướng dẫn tải thông thường và hướng dẫn lưu trữ giúp đảm bảo rằng cửa hàng sẽ được hiển thị toàn bộ hoặc hoàn toàn không hiển thị với người khác trình xử lý tải cùng một vị trí. Do đó, có một số khái niệm cơ bản của "tính nguyên tử" được cung cấp miễn phí.

Như chúng ta đã thấy, như vậy là chưa đủ. Để đảm bảo tính tuần tự tính nhất quán, chúng ta cũng cần ngăn chặn việc sắp xếp lại hoạt động và để đảm bảo hoạt động bộ nhớ hiển thị với các quy trình khác một cách nhất quán đơn đặt hàng. Hoá ra là chế độ cài đặt tự động được Android hỗ trợ phần cứng, miễn là chúng ta đưa ra các lựa chọn sáng suốt để thực thi phần cứng, nên chúng tôi hầu như bỏ qua ở đây.

Thứ tự của các thao tác đối với bộ nhớ được duy trì bằng cả tính năng ngăn việc sắp xếp lại thứ tự theo trình biên dịch và ngăn việc sắp xếp lại theo phần cứng. Ở đây chúng ta tập trung về phần tiếp theo.

Thứ tự bộ nhớ trên ARMv7, x86 và MIPS được thực thi bằng "hàng rào" các hướng dẫn ngăn chặn hoàn toàn những chỉ dẫn đi theo hàng rào hiện ra trước khi có chỉ dẫn đứng trước hàng rào. (Đây cũng thường là có tên là "rào cản" nhưng có nguy cơ nhầm lẫn với rào cản kiểu pthread_barrier, làm được nhiều việc hơn hơn thế này.) Ý nghĩa chính xác của hướng dẫn về hàng rào là một chủ đề khá phức tạp phải giải quyết cách thức đảm bảo được cung cấp bởi nhiều loại hàng rào tương tác và cách những cam kết này kết hợp với các cam kết đặt hàng khác thường do phần cứng cung cấp. Đây là thông tin tổng quan, vì vậy chúng tôi sẽ bóng loáng hơn những chi tiết này.

Phương thức đảm bảo sắp xếp theo thứ tự cơ bản nhất là do C++ cung cấp memory_order_acquirememory_order_release hoạt động nguyên tử: hoạt động bộ nhớ trước khi lưu trữ bản phát hành phải hiển thị sau khi tải. Trên ARMv7, đây là thực thi bởi:

  • Thêm hướng dẫn về hàng rào phù hợp vào trước hướng dẫn cửa hàng. Điều này ngăn việc sắp xếp lại tất cả các quyền truy cập bộ nhớ trước đó bằng hướng dẫn cửa hàng. (Điều này cũng không cần thiết ngăn việc sắp xếp lại với hướng dẫn cửa hàng sau này).
  • Làm theo hướng dẫn tải cùng với hướng dẫn phù hợp về hàng rào, ngăn không cho tải được sắp xếp lại với các lần truy cập tiếp theo. (Và một lần nữa cung cấp thứ tự không cần thiết ít nhất là tải sớm hơn.)

Kết hợp các yếu tố này là đủ cho thứ tự thu nạp/phát hành C++. Các chỉ số này cần thiết, nhưng chưa đủ, đối với volatile của Java hoặc C++ nhất quán tuần tự atomic.

Để biết chúng ta cần thêm những gì, hãy xem xét đoạn thuật toán Dekker mà chúng tôi đã đề cập ngắn gọn trước đó. flag1flag2 là C++ atomic hoặc Java volatile, cả hai ban đầu đều có giá trị false.

Chuỗi 1 Chuỗi 2
flag1 = true
if (flag2 == false)
    critical-stuff
flag2 = true
if (flag1 == false)
    critical-stuff

Tính nhất quán tuần tự ngụ ý rằng một trong những chỉ định để flagn phải được thực thi trước tiên và sẽ hiển thị với kiểm thử trong luồng khác. Do đó, chúng tôi sẽ không bao giờ thấy các luồng này thực thi đồng thời "nội dung quan trọng".

Tuy nhiên, tính năng khoanh vùng bắt buộc đối với thứ tự thu nạp bản phát hành chỉ thêm vào hàng rào ở đầu và cuối mỗi chuỗi, điều này không giúp ích vào đây. Ngoài ra, chúng tôi cần đảm bảo rằng nếu một volatile/atomic cửa hàng đứng trước tải volatile/atomic, cả hai không được sắp xếp lại. Việc này thường được thực thi bằng cách thêm hàng rào, không chỉ trước cửa hàng nhất quán tuần tự mà cả sau đó. (Một lần nữa, hàng rào này mạnh hơn nhiều so với yêu cầu, vì hàng rào này thường yêu cầu tất cả các lượt truy cập bộ nhớ trước đó so với tất cả các lượt truy cập sau này).

Thay vào đó, chúng ta có thể liên kết hàng rào bổ sung với tải nhất quán. Vì tần suất đến cửa hàng ít hơn, nên quy ước mà chúng tôi mô tả là phổ biến hơn và được sử dụng trên Android.

Như đã thấy trong phần trước, chúng ta cần chèn rào cản cửa hàng/tải hàng giữa hai phép toán. Mã được thực thi trong máy ảo cho quyền truy cập biến động sẽ có dạng như sau:

tải biến động cửa hàng tự tính toán lại
reg = A
fence for "acquire" (1)
fence for "release" (2)
A = reg
fence for later atomic load (3)

Kiến trúc máy thực thường cung cấp nhiều loại hàng rào, có thứ tự các loại quyền truy cập khác nhau và có thể có chi phí khác nhau. Lựa chọn giữa những câu hỏi này rất tinh tế và chịu ảnh hưởng bởi nhu cầu đảm bảo rằng các cửa hàng được hiển thị đến những điểm cốt lõi khác trong thứ tự nhất quán và thứ tự bộ nhớ do kết hợp nhiều hàng rào một cách chính xác. Để biết thêm thông tin, vui lòng xem trang của Đại học Cambridge với thu thập được ánh xạ của các nguyên tử đến bộ xử lý thực tế.

Trên một số kiến trúc, đặc biệt là x86, tính năng "mua lại" và "release" rào cản là không cần thiết vì phần cứng luôn ngầm ẩn thực thi đủ thứ tự. Do đó trên x86 chỉ có hàng rào cuối cùng (3) thực sự được tạo ra. Tương tự như trên x86, atom read-modify-write hoạt động ngầm bao gồm một hàng rào chắc chắn. Do đó, những bộ lọc này không bao giờ cần có hàng rào. Trên ARMv7, tất cả hàng rào mà chúng ta đã thảo luận ở trên là là bắt buộc.

ARMv8 cung cấp hướng dẫn LDAR và STLR trực tiếp thực thi các yêu cầu của Java volatile hoặc C++ theo tuần tự nhất quán tải và lưu trữ. Chúng ta tránh được những ràng buộc không cần thiết khi sắp xếp lại thứ tự đề cập ở trên. Mã Android 64 bit trên ARM sử dụng các định dạng này; chúng tôi chọn tập trung vào vị trí hàng rào ARMv7 ở đây vì nó làm sáng tỏ nhiều ánh sáng hơn các yêu cầu thực tế.

Tài liệu đọc thêm

Các trang web và tài liệu cung cấp thông tin chuyên sâu hơn. Thông tin thường hữu ích hơn bài viết ở gần đầu danh sách.

Các mô hình về tính nhất quán của bộ nhớ dùng chung: Hướng dẫn
Được viết vào năm 1995 bởi Adve & Gharachorloo, đây là nơi phù hợp để bắt đầu nếu bạn muốn tìm hiểu sâu hơn về các mô hình tính nhất quán của bộ nhớ.
http://www.hpl.HP.com/techreports/Compaq-DEC/Wrl-95-7.pdf
Rào cản về bộ nhớ
Bài viết rất ngắn gọn tóm tắt các vấn đề.
https://vi.wikipedia.org/wiki/Memory_barrier
Kiến thức cơ bản về luồng
Giới thiệu về lập trình đa luồng trong C++ và Java của Hans Boehm. Nội dung thảo luận về các cuộc đua dữ liệu và các phương pháp đồng bộ hoá cơ bản.
http://www.hboehm.info/c++mm/threadsintro.html
Mô hình đồng thời Java trong thực tế
Xuất bản năm 2006, cuốn sách này trình bày rất chi tiết về nhiều chủ đề. Rất nên dùng cho những ai viết mã đa luồng trong Java.
http://www.javaconcurrencyinpractice.com
Câu hỏi thường gặp về JSR-133 (Mô hình bộ nhớ Java)
Giới thiệu sơ bộ về mô hình bộ nhớ Java, bao gồm nội dung giải thích về quá trình đồng bộ hoá, các biến tự tính toán lại và cấu trúc của các trường cuối cùng. (Hơi lỗi thời, đặc biệt là khi thảo luận về các ngôn ngữ khác.)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Tính hợp lệ của các phép biến đổi chương trình trong mô hình bộ nhớ Java
Giải thích khá kỹ thuật về các vấn đề còn lại của Mô hình bộ nhớ Java. Những vấn đề này không áp dụng cho cơ chế không có cuộc đua dữ liệu các chương trình.
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf
Tổng quan về gói java.util.concurrent
Tài liệu về gói java.util.concurrent. Ở gần cuối trang là một phần có tiêu đề "Thuộc tính nhất quán của bộ nhớ", phần này giải thích nội dung đảm bảo của nhiều lớp.
Tóm tắt về gói java.util.concurrent
Lý thuyết và thực hành Java: Kỹ thuật xây dựng an toàn trong Java
Bài viết này xem xét chi tiết các mối nguy hiểm của việc thoát tệp tham chiếu trong quá trình tạo đối tượng, đồng thời cung cấp các nguyên tắc về hàm khởi tạo an toàn cho luồng.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html
Lý thuyết và thực hành Java: Quản lý sự biến động
Một bài viết hay mô tả những việc bạn có thể và không thể làm với các trường biến động trong Java.
http://www.ibm.com/developerworks/java/library/j-jtp06197.html
Nội dung khai báo "Khoá được kiểm tra kỹ đã bị hỏng"
Bill Pugh giải thích chi tiết về các cách phá khoá khi kiểm tra kỹ mà không cần dùng volatile hoặc atomic. Bao gồm C/C++ và Java.
http://www.cs.umd.edu/~pugh/java/memoryModel/Double SelectedLocking.html
[ARM] Sách hướng dẫn và cách kiểm tra rào cản
Một cuộc thảo luận về các vấn đề liên quan đến ARM SMP, được trình bày bằng các đoạn mã ngắn của mã ARM. Nếu bạn thấy các ví dụ trên trang này quá không cụ thể hoặc muốn đọc nội dung mô tả chính thức của hướng dẫn DMB, hãy đọc nội dung này. Đồng thời mô tả các hướng dẫn dùng cho rào cản bộ nhớ trên mã thực thi (có thể hữu ích nếu bạn đang tạo mã nhanh chóng). Lưu ý rằng phiên bản này ra mắt trước ARMv8, cũng là hỗ trợ các lệnh sắp xếp thứ tự bộ nhớ bổ sung và được chuyển sang một cách mạnh mẽ hơn mô hình bộ nhớ. (Xem "Hướng dẫn tham khảo kiến trúc ARM® ARMv8, dành cho cấu hình kiến trúc ARMv8-A" để biết thông tin chi tiết.)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf
Rào cản bộ nhớ hạt nhân Linux
Tài liệu về các rào cản bộ nhớ của nhân hệ điều hành Linux. Bao gồm một số ví dụ hữu ích và nghệ thuật ASCII.
http://www.kernel.org/doc/Document/memory-barriers.txt
ISO/IEC JTC1 SC22 WG21 (tiêu chuẩn C++) 14882 (Ngôn ngữ lập trình C++), mục 1.10 và điều khoản 29 ("Thư viện hoạt động nguyên tử")
Tiêu chuẩn dự thảo cho các tính năng thao tác ở cấp độ nguyên tử trong C++. Phiên bản này gần với tiêu chuẩn C++14, bao gồm cả những thay đổi nhỏ trong lĩnh vực này từ C++11.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(giới thiệu: http://www.hpl.HP.com/techreports/2008/HPL-2008-56.pdf)
ISO/IEC JTC1 SC22 WG14 (Tiêu chuẩn C) 9899 (Ngôn ngữ lập trình C) chương 7.16 (“Nguyên tử <stdatom.h>”)
Tiêu chuẩn dự thảo cho các tính năng vận hành ở cấp nguyên tử ISO/IEC 9899-201x C. Để biết thông tin chi tiết, hãy kiểm tra cả các báo cáo sai sót sau này.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
Ánh xạ C/C++11 tới bộ xử lý (Đại học Cambridge)
Bộ sưu tập bản dịch của Jaroslav Sev Cách và Peter Sewell của nguyên tử C++ sang tập lệnh phổ biến của bộ xử lý.
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
Thuật toán Dekker
"Giải pháp chính xác đầu tiên được biết đến cho bài toán loại trừ lẫn nhau trong lập trình đồng thời". Bài viết wikipedia có thuật toán đầy đủ, kèm theo nội dung thảo luận về cách thuật toán cần được cập nhật để hoạt động với các trình biên dịch tối ưu hoá và phần cứng SMP hiện đại.
https://vi.wikipedia.org/wiki/Dekker's_algorithm
Nhận xét về ARM so với Alpha và các phần phụ thuộc địa chỉ
Một email về danh sách gửi thư chi tiết từ Catalin Marinas. Bao gồm một bản tóm tắt thú vị về các phần phụ thuộc kiểm soát và địa chỉ.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html
Những điều mọi lập trình viên nên biết về bộ nhớ
Một bài viết rất dài và chi tiết về các loại bộ nhớ, đặc biệt là bộ nhớ đệm của CPU, của Ulrich Drepper.
http://www.akkadia.org/drepper/cpumemory.pdf
Lý do về mô hình bộ nhớ ARM yếu
Bài viết này do Chong & Ishtiaq của ARM, Ltd. Công cụ này cố gắng mô tả mô hình bộ nhớ ARM SMP một cách nghiêm ngặt nhưng dễ tiếp cận. Định nghĩa về "khả năng ghi nhận" được sử dụng ở đây được lấy trong bài viết này. Xin nhắc lại rằng mã này lại có trước ARMv8.
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll='dl kỹ=CFID=96099715&CFTOKEN=57505711
Sách nấu ăn JSR-133 dành cho người viết trình biên dịch
Doug Lea viết mã này trong tài liệu về JSR-133 (Mô hình bộ nhớ Java). Tài liệu này có bộ nguyên tắc triển khai ban đầu cho mô hình bộ nhớ Java được nhiều trình biên dịch sử dụng và là vẫn được trích dẫn nhiều và có khả năng cung cấp thông tin chi tiết. Rất tiếc, 4 loại hàng rào được thảo luận ở đây không phải là một lựa chọn tốt phù hợp với các cấu trúc được Android hỗ trợ và ánh xạ C++11 ở trên giờ đây là một nguồn công thức chính xác tốt hơn, ngay cả đối với Java.
http://g.oswego.edu/dl/jmm/cookbook.html
x86-TSO: Mô hình lập trình nghiêm ngặt và dễ sử dụng dành cho bộ đa xử lý x86
Thông tin mô tả chính xác về mô hình bộ nhớ x86. Mô tả chính xác về không may, mô hình bộ nhớ ARM phức tạp hơn đáng kể.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf