phép gán tham chiếu là nguyên tử, vậy tại sao lại cần Interlocked.Exchange (ref Object, Object)?


108

Trong dịch vụ web asmx đa luồng của tôi, tôi có một trường lớp _allData thuộc loại SystemData của riêng tôi, bao gồm một số ít List<T>Dictionary<T>được đánh dấu là volatile. Dữ liệu hệ thống ( _allData) thỉnh thoảng được làm mới và tôi làm điều đó bằng cách tạo một đối tượng khác được gọi newDatavà điền vào cấu trúc dữ liệu của nó bằng dữ liệu mới. Khi nó hoàn thành, tôi chỉ cần chỉ định

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Điều này sẽ hoạt động vì phép gán là nguyên tử và các luồng có tham chiếu đến dữ liệu cũ tiếp tục sử dụng nó và phần còn lại có dữ liệu hệ thống mới ngay sau khi gán. Tuy nhiên, đồng nghiệp của tôi nói rằng thay vì sử dụng volatiletừ khóa và InterLocked.Exchangephép gán đơn giản, tôi nên sử dụng vì anh ấy nói rằng trên một số nền tảng, không đảm bảo rằng phép gán tham chiếu là nguyên tử. Hơn nữa: khi tôi tuyên bố the _allDatalĩnh vực như volatilecác

Interlocked.Exchange<SystemData>(ref _allData, newData); 

tạo ra cảnh báo "tham chiếu đến một trường dễ bay hơi sẽ không được coi là dễ bay hơi" Tôi nên nghĩ gì về điều này?

Câu trả lời:


180

Có rất nhiều câu hỏi ở đây. Xem xét chúng tại một thời điểm:

phép gán tham chiếu là nguyên tử, vậy tại sao lại cần Interlocked.Exchange (ref Object, Object)?

Phép gán tham chiếu là nguyên tử. Interlocked.Exchange không chỉ thực hiện việc gán tham chiếu. Nó thực hiện việc đọc giá trị hiện tại của một biến, xóa giá trị cũ và gán giá trị mới cho biến, tất cả đều như một phép toán nguyên tử.

đồng nghiệp của tôi nói rằng trên một số nền tảng, không đảm bảo rằng việc gán tham chiếu là nguyên tử. Đồng nghiệp của tôi có đúng không?

Không. Việc gán tham chiếu được đảm bảo là nguyên tử trên tất cả các nền tảng .NET.

Đồng nghiệp của tôi đang suy luận từ những tiền đề sai lầm. Điều đó có nghĩa là kết luận của họ không chính xác?

Không cần thiết. Đồng nghiệp của bạn có thể đưa ra lời khuyên tốt cho bạn vì những lý do xấu. Có lẽ có một số lý do khác khiến bạn nên sử dụng Interlocked.Exchange. Lập trình không có khóa cực kỳ khó khăn và thời điểm bạn rời khỏi các phương pháp thực hành đã được thiết lập tốt bởi các chuyên gia trong lĩnh vực này, bạn sẽ lạc lối và mạo hiểm với loại điều kiện chủng tộc tồi tệ nhất. Tôi không phải là chuyên gia trong lĩnh vực này và cũng không phải là chuyên gia về mã của bạn, vì vậy tôi không thể đưa ra nhận định theo cách này hay cách khác.

tạo ra cảnh báo "tham chiếu đến một trường dễ bay hơi sẽ không được coi là dễ bay hơi" Tôi nên nghĩ gì về điều này?

Bạn nên hiểu tại sao đây là một vấn đề nói chung. Điều đó sẽ dẫn đến sự hiểu biết về lý do tại sao cảnh báo không quan trọng trong trường hợp cụ thể này.

Lý do mà trình biên dịch đưa ra cảnh báo này là vì việc đánh dấu một trường là dễ bay hơi có nghĩa là "trường này sẽ được cập nhật trên nhiều luồng - không tạo bất kỳ mã nào lưu trữ các giá trị của trường này và đảm bảo rằng mọi lần đọc hoặc ghi trường này không được "chuyển tới và lùi theo thời gian" thông qua sự mâu thuẫn trong bộ nhớ cache của bộ xử lý. "

(Tôi cho rằng bạn đã hiểu tất cả những điều đó rồi. Nếu bạn không hiểu chi tiết về ý nghĩa của biến động và cách nó tác động đến ngữ nghĩa bộ nhớ đệm của bộ xử lý thì bạn không hiểu nó hoạt động như thế nào và không nên sử dụng dễ bay hơi. Các chương trình không khóa rất khó để đi đúng; hãy đảm bảo rằng chương trình của bạn đúng vì bạn hiểu cách hoạt động của nó, không phải do ngẫu nhiên mà có.)

Bây giờ, giả sử bạn tạo một biến là bí danh của trường biến động bằng cách chuyển một tham chiếu đến trường đó. Bên trong phương thức được gọi, trình biên dịch không có lý do gì để biết rằng tham chiếu cần phải có ngữ nghĩa dễ bay hơi! Trình biên dịch sẽ vui vẻ tạo mã cho phương thức không triển khai các quy tắc cho trường biến động, nhưng biến một trường dễ bay hơi. Điều đó hoàn toàn có thể phá hỏng logic không có khóa của bạn; giả định luôn luôn là một trường biến động luôn được truy cập với ngữ nghĩa biến động. Sẽ chẳng có nghĩa lý gì khi coi nó là đôi khi dễ bay hơi chứ không phải lúc khác; bạn phải luôn nhất quán nếu không bạn không thể đảm bảo tính nhất quán trên các truy cập khác.

Do đó, trình biên dịch cảnh báo khi bạn làm điều này, vì nó có thể sẽ làm rối tung hoàn toàn logic không có khóa được phát triển cẩn thận của bạn.

Tất nhiên, Interlocked.Exchange được viết để mong đợi một trường biến động và làm điều đúng đắn. Cảnh báo do đó gây hiểu lầm. Tôi rất hối hận về điều này; những gì chúng ta nên làm là thực hiện một số cơ chế theo đó tác giả của một phương thức như Interlocked.Exchange có thể đặt một thuộc tính trên phương thức nói rằng "phương thức này có tham chiếu thực thi ngữ nghĩa dễ bay hơi trên biến, vì vậy hãy loại bỏ cảnh báo". Có lẽ trong một phiên bản tương lai của trình biên dịch, chúng tôi sẽ làm như vậy.


1
Từ những gì tôi đã nghe Interlocked.Exchange cũng đảm bảo rằng một rào cản bộ nhớ được tạo ra. Vì vậy, nếu bạn chẳng hạn tạo một đối tượng mới, sau đó gán một vài thuộc tính và sau đó lưu trữ đối tượng đó trong một tham chiếu khác mà không sử dụng Interlocked.Exchange thì trình biên dịch có thể làm rối thứ tự của các hoạt động đó, do đó làm cho việc truy cập tham chiếu thứ hai không phải là luồng- an toàn. Có thực sự như vậy không? Sử dụng Interlocked.Exchange có phải là loại kịch bản như vậy không?
Mike

12
@Mike: Khi nói đến những gì có thể quan sát được trong các tình huống đa luồng khóa thấp, tôi cũng không biết gì về người kế tiếp. Câu trả lời có thể sẽ khác nhau giữa các bộ vi xử lý. Bạn nên giải đáp câu hỏi của mình cho một chuyên gia hoặc đọc về chủ đề này nếu bạn quan tâm. Sách của Joe Duffy và blog của anh ấy là những nơi tốt để bắt đầu. Quy tắc của tôi: không sử dụng đa luồng. Nếu bạn phải sử dụng cấu trúc dữ liệu bất biến. Nếu bạn không thể, hãy sử dụng ổ khóa. Chỉ khi bạn phải có dữ liệu có thể thay đổi mà không có khóa, bạn mới nên xem xét các kỹ thuật khóa thấp.
Eric Lippert

Cảm ơn câu trả lời của bạn Eric. Nó thực sự làm tôi quan tâm, đó là lý do tại sao tôi đã đọc sách và blog về các chiến lược đa luồng và khóa và cũng cố gắng triển khai những chiến lược đó trong mã của mình. Nhưng vẫn còn rất nhiều điều để học ...
Mike

2
@EricLippert Giữa "không sử dụng đa luồng" và "nếu bạn phải, hãy sử dụng cấu trúc dữ liệu bất biến", tôi sẽ chèn mức trung gian và rất phổ biến là "có một luồng con chỉ sử dụng các đối tượng đầu vào thuộc sở hữu độc quyền và luồng mẹ sử dụng kết quả chỉ khi sinh con xong ”. Như trong var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
John

1
@John: Đó là một ý kiến ​​hay. Tôi cố gắng coi các luồng như các quy trình rẻ tiền: chúng ở đó để thực hiện một công việc và tạo ra kết quả, không phải để chạy xung quanh là một luồng điều khiển thứ hai bên trong cấu trúc dữ liệu của chương trình chính. Nhưng nếu khối lượng công việc mà luồng đang làm quá lớn đến mức hợp lý để coi nó như một quá trình, thì tôi nói chỉ cần biến nó thành một quá trình!
Eric Lippert

9

Đồng nghiệp của bạn nhầm lẫn hoặc anh ta biết điều gì đó mà đặc tả ngôn ngữ C # không có.

5.5 Tính nguyên tử của các tham chiếu biến :

"Đọc và ghi các kiểu dữ liệu sau là kiểu nguyên tử: bool, char, byte, sbyte, short, ushort, uint, int, float và các kiểu tham chiếu."

Vì vậy, bạn có thể ghi vào tham chiếu biến động mà không có nguy cơ nhận được giá trị bị hỏng.

Tất nhiên, bạn nên cẩn thận với cách bạn quyết định luồng nào sẽ tìm nạp dữ liệu mới, để giảm thiểu rủi ro có nhiều luồng cùng một lúc thực hiện điều đó.


3
@guffa: vâng, tôi cũng đã đọc nó. điều này khiến câu hỏi ban đầu đặt ra "phép gán tham chiếu là nguyên tử, vậy tại sao lại cần Interlocked.Exchange (ref Object, Object)?" unaswered
char m

@zebrabox: ý bạn là gì? khi họ không? bạn sẽ làm gì?
char m

@matti: Nó cần thiết khi bạn phải đọc và ghi một giá trị như một phép toán nguyên tử.
Guffa

Bạn có thường xuyên phải lo lắng về việc bộ nhớ không được căn chỉnh chính xác trong .NET không? Interop-nội dung nặng?
Skurmedel

1
@zebrabox: Thông số kỹ thuật không liệt kê cảnh báo đó, nó đưa ra một tuyên bố rất rõ ràng. Bạn có tài liệu tham khảo cho trường hợp không căn chỉnh bộ nhớ trong đó một tham chiếu đọc hoặc ghi không phải là nguyên tử? Có vẻ như điều đó sẽ vi phạm ngôn ngữ rất rõ ràng trong đặc tả.
TJ Crowder

6

Interlocked.Exchange <T>

Đặt một biến của kiểu được chỉ định T thành một giá trị được chỉ định và trả về giá trị ban đầu, như một phép toán nguyên tử.

Nó thay đổi và trả về giá trị ban đầu, nó vô dụng vì bạn chỉ muốn thay đổi nó và như Guffa đã nói, nó đã là nguyên tử.

Trừ khi một hồ sơ được chứng minh là nút cổ chai trong ứng dụng của bạn, bạn nên xem xét việc mở khóa, sẽ dễ hiểu hơn và chứng minh rằng mã của bạn là đúng.


3

Iterlocked.Exchange() không chỉ là nguyên tử, nó còn quan tâm đến khả năng hiển thị của bộ nhớ:

Các chức năng đồng bộ hóa sau sử dụng các rào cản thích hợp để đảm bảo thứ tự bộ nhớ:

Các hàm nhập hoặc rời các phần quan trọng

Các hàm báo hiệu các đối tượng đồng bộ hóa

Chờ chức năng

Các chức năng được lồng vào nhau

Vấn đề về đồng bộ hóa và đa xử lý

Điều này có nghĩa là ngoài tính nguyên tử, nó đảm bảo rằng:

  • Đối với chuỗi gọi nó:
    • Không có thứ tự nào của các hướng dẫn được thực hiện (bởi trình biên dịch, thời gian chạy hoặc phần cứng).
  • Đối với tất cả các chủ đề:
    • Không có lần đọc vào bộ nhớ nào xảy ra trước lệnh này sẽ thấy sự thay đổi của lệnh này.
    • Tất cả các lần đọc sau hướng dẫn này sẽ thấy sự thay đổi được thực hiện bởi hướng dẫn này.
    • Tất cả việc ghi vào bộ nhớ sau lệnh này sẽ xảy ra sau khi lệnh này thay đổi đến bộ nhớ chính (bằng cách chuyển lệnh này thay đổi vào bộ nhớ chính khi nó hoàn thành và không để phần cứng tự xóa nó theo thời gian).
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.