2008-09-16 21 views
250

Có ai có thể cung cấp giải thích tốt về từ khóa dễ bay hơi trong C# không? Những vấn đề nào nó giải quyết và nó không? Trong trường hợp nào nó sẽ tiết kiệm cho tôi việc sử dụng khóa?Khi nào thì từ khóa dễ bay hơi được sử dụng trong C#?

+5

Tại sao bạn muốn lưu vào việc sử dụng khóa? Khóa không được kiểm soát thêm vài nano giây vào chương trình của bạn. Bạn có thực sự không đủ khả năng một vài nano giây không? –

Trả lời

231

Tôi không nghĩ rằng có một người tốt hơn để trả lời câu này hơn Eric Lippert (nhấn mạnh trong bản gốc):

Trong C#, "không ổn định" có nghĩa là không chỉ "đảm bảo rằng trình biên dịch và jitter không thực hiện bất kỳ mã nào sắp xếp lại hoặc đăng ký bộ nhớ đệm tối ưu hóa trên biến này ". Nó cũng có nghĩa là "yêu cầu bộ vi xử lý làm bất cứ điều gì họ cần làm để đảm bảo rằng tôi đang đọc giá trị mới nhất, ngay cả khi điều đó có nghĩa là tạm dừng các bộ vi xử lý khác và thực hiện chúng đồng bộ hóa bộ nhớ chính với bộ đệm của chúng".

Thực ra, bit cuối cùng đó là một lời nói dối. Các ngữ nghĩa thực sự của đọc dễ bay hơi và viết là phức tạp hơn đáng kể so với tôi đã nêu ở đây; trong thực tế họ không thực sự đảm bảo rằng mọi bộ xử lý dừng lại những gì nó đang thực hiện và cập nhật bộ nhớ cache đến/từ bộ nhớ chính. Thay vào đó, , chúng cung cấp các đảm bảo yếu hơn về cách truy cập bộ nhớ trước và sau khi đọc và viết có thể được quan sát theo thứ tự tương ứng với nhau. Một số hoạt động nhất định như tạo chủ đề mới, nhập khóa hoặc bằng cách sử dụng một trong các nhóm phương pháp được liên kết giới thiệu mạnh mẽ hơn đảm bảo về quan sát đặt hàng. Nếu bạn muốn biết thêm chi tiết, đọc các phần 3.10 và 10.5.3 của đặc tả C# 4.0.

Thẳng thắn, Tôi không khuyến khích bạn tạo ra một trường dễ bay hơi. Các lĩnh vực dễ bay hơi là dấu hiệu cho thấy bạn đang làm điều gì đó hết sức điên rồ: bạn đang cố đọc và viết cùng một giá trị trên hai chủ đề khác nhau mà không đặt khóa tại chỗ. Khóa đảm bảo rằng bộ nhớ đọc hoặc sửa đổi bên trong khóa được quan sát là nhất quán, bảo đảm khóa chỉ có một luồng truy cập một đoạn bộ nhớ đã cho tại một thời điểm, và do đó bật. Số lượng tình huống trong đó khóa quá chậm là rất nhỏ và xác suất bạn sẽ nhận được mã sai vì bạn không hiểu mô hình bộ nhớ chính xác là rất lớn. Tôi không cố gắng viết bất kỳ mã khóa thấp nào trừ việc sử dụng các thao tác khóa liên động nhỏ nhất . Tôi để lại việc sử dụng "dễ bay hơi" cho chuyên gia thực sự.

Để biết thêm đọc see:

+19

Tôi sẽ bỏ phiếu nếu tôi có thể. Có rất nhiều thông tin thú vị ở đó, nhưng nó không thực sự trả lời câu hỏi của anh ấy. Anh ta hỏi về việc sử dụng từ khóa dễ bay hơi vì nó liên quan đến khóa. Trong một thời gian khá dài (trước 2.0 RT), từ khóa dễ bay hơi là cần thiết để sử dụng để tạo ra một trường tĩnh an toàn nếu cá thể trường có bất kỳ mã khởi tạo nào trong hàm tạo (xem câu trả lời của AndrewTek). Có rất nhiều 1.1 mã RT vẫn còn trong môi trường sản xuất và các nhà phát triển duy trì nó nên biết lý do tại sao từ khóa đó là có và nếu nó an toàn để loại bỏ. –

+3

@PaulEaster thực tế là nó * có thể * được sử dụng để khóa kiểm tra doulbe (thường là trong mẫu đơn) không có nghĩa là * nên *. Dựa vào mô hình bộ nhớ .NET có lẽ là một thực hành tồi - bạn nên dựa vào mô hình ECMA để thay thế. Ví dụ: bạn có thể muốn chuyển sang mono một ngày, có thể có một mô hình khác. Tôi cũng hiểu rằng các kiến ​​trúc phần cứng khác nhau có thể thay đổi mọi thứ. Để biết thêm thông tin, hãy xem: http://stackoverflow.com/a/7230679/67824. Đối với các lựa chọn thay thế singleton tốt hơn (cho tất cả các phiên bản .NET), hãy xem: http://csharpindepth.com/articles/general/singleton.aspx –

+0

Tôi không nói cách này hay cách khác cho dù nó nên được sử dụng. Nếu một phần công việc của bạn là duy trì một ứng dụng 1.1 cũ, bạn không thể luôn áp dụng các mô hình/thực tiễn tốt nhất từ ​​thời gian chạy hiện tại. Trên thực tế, có thể cần phải giữ từ khóa bên trong mã nếu nó không thể được chuyển sang thời gian chạy mới hơn. Ngoài ra, bạn nên dựa vào mô hình bộ nhớ bạn sẽ chạy trong sản xuất. Nếu ứng dụng của bạn có vấn đề về bộ nhớ, hãy nói với khách hàng của bạn rằng nó được viết theo cách đó trong trường hợp nó được chuyển sang mono sẽ là điều cuối cùng bạn nói với họ. Nó sẽ không quan trọng nếu mã của bạn là chính xác về mặt học thuật. –

13

CLR thích tối ưu hóa hướng dẫn, vì vậy khi bạn truy cập một trường bằng mã, nó có thể không luôn truy cập giá trị hiện tại của trường (có thể từ ngăn xếp, v.v.). Đánh dấu một trường là volatile đảm bảo rằng giá trị hiện tại của trường được truy cập bằng lệnh. Điều này rất hữu ích khi giá trị có thể được sửa đổi (trong một kịch bản không khóa) bởi một chuỗi đồng thời trong chương trình của bạn hoặc một số mã khác đang chạy trong hệ điều hành.

Bạn rõ ràng sẽ mất một số tối ưu hóa, nhưng nó giữ mã đơn giản hơn.

22

Từ MSDN: Công cụ sửa đổi dễ bay hơi thường được sử dụng cho trường được truy cập bởi nhiều luồng mà không sử dụng lệnh khóa để truy cập tuần tự. Sử dụng công cụ sửa đổi dễ bay hơi, đảm bảo rằng một chuỗi truy lục giá trị cập nhật mới nhất được viết bởi một chuỗi khác.

20

Đôi khi, trình biên dịch sẽ tối ưu hóa một trường và sử dụng thanh ghi để lưu trữ nó. Nếu thread 1 không ghi vào trường và một luồng khác truy cập vào trường đó, vì bản cập nhật được lưu trữ trong sổ đăng ký (và không phải bộ nhớ), luồng thứ hai sẽ lấy dữ liệu cũ.

Bạn có thể nghĩ từ khóa dễ bay hơi khi nói với trình biên dịch "Tôi muốn bạn lưu trữ giá trị này trong bộ nhớ". Điều này đảm bảo rằng luồng thứ 2 truy xuất giá trị mới nhất.

54

Nếu bạn muốn nhận được một chút kỹ thuật thêm về những gì từ khóa dễ bay hơi không, hãy xem xét các chương trình sau đây (Tôi đang sử dụng DevStudio 2005):

#include <iostream> 
void main() 
{ 
    int j = 0; 
    for (int i = 0 ; i < 100 ; ++i) 
    { 
    j += i; 
    } 
    for (volatile int i = 0 ; i < 100 ; ++i) 
    { 
    j += i; 
    } 
    std::cout << j; 
} 

Sử dụng các tiêu chuẩn tối ưu hóa (phát hành) cài đặt trình biên dịch, các trình biên dịch tạo bộ kết hợp sau (IA32):

void main() 
{ 
00401000 push  ecx 
    int j = 0; 
00401001 xor   ecx,ecx 
    for (int i = 0 ; i < 100 ; ++i) 
00401003 xor   eax,eax 
00401005 mov   edx,1 
0040100A lea   ebx,[ebx] 
    { 
    j += i; 
00401010 add   ecx,eax 
00401012 add   eax,edx 
00401014 cmp   eax,64h 
00401017 jl   main+10h (401010h) 
    } 
    for (volatile int i = 0 ; i < 100 ; ++i) 
00401019 mov   dword ptr [esp],0 
00401020 mov   eax,dword ptr [esp] 
00401023 cmp   eax,64h 
00401026 jge   main+3Eh (40103Eh) 
00401028 jmp   main+30h (401030h) 
0040102A lea   ebx,[ebx] 
    { 
    j += i; 
00401030 add   ecx,dword ptr [esp] 
00401033 add   dword ptr [esp],edx 
00401036 mov   eax,dword ptr [esp] 
00401039 cmp   eax,64h 
0040103C jl   main+30h (401030h) 
    } 
    std::cout << j; 
0040103E push  ecx 
0040103F mov   ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045 call  dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
} 
0040104B xor   eax,eax 
0040104D pop   ecx 
0040104E ret    

Nhìn vào đầu ra, trình biên dịch đã quyết định sử dụng thanh ghi ecx để lưu giá trị của biến j. Đối với vòng lặp không dễ bay hơi (đầu tiên) trình biên dịch đã gán i cho thanh ghi eax. Khá đơn giản. Có một vài bit thú vị mặc dù - các ebx lea, hướng dẫn [ebx] là một lệnh nop multibyte hiệu quả để vòng lặp nhảy đến một địa chỉ bộ nhớ 16 byte liên kết. Cách khác là sử dụng edx để tăng bộ đếm vòng lặp thay vì sử dụng lệnh inc eax. Lệnh thêm reg, reg có độ trễ thấp hơn trên một số lõi IA32 so với lệnh reg, nhưng không bao giờ có độ trễ cao hơn.

Bây giờ cho vòng lặp với bộ đếm vòng lặp dễ bay hơi. Bộ đếm được lưu trữ tại [esp] và từ khóa dễ bay hơi cho trình biên dịch biết giá trị luôn luôn được đọc từ/ghi vào bộ nhớ và không bao giờ được gán cho một thanh ghi. Trình biên dịch thậm chí còn đi xa đến mức không làm tải/tăng/lưu trữ như ba bước riêng biệt (tải eax, inc eax, lưu eax) khi cập nhật giá trị bộ đếm, thay vào đó bộ nhớ được sửa đổi trực tiếp trong một lệnh duy nhất , reg). Cách mã được tạo ra đảm bảo giá trị của bộ đếm vòng lặp luôn được cập nhật trong ngữ cảnh của một lõi CPU đơn. Không có hoạt động trên dữ liệu có thể dẫn đến tham nhũng hoặc mất dữ liệu (do đó không sử dụng tải/inc/cửa hàng vì giá trị có thể thay đổi trong quá trình inc do đó bị mất trên cửa hàng). Vì các ngắt chỉ có thể được phục vụ sau khi lệnh hiện tại đã hoàn thành, dữ liệu không bao giờ có thể bị hỏng, ngay cả với bộ nhớ chưa được ký.

Khi bạn giới thiệu CPU thứ hai vào hệ thống, từ khóa dễ bay hơi sẽ không bảo vệ chống lại dữ liệu đang được cập nhật bởi một CPU khác cùng một lúc.Trong ví dụ trên, bạn sẽ cần các dữ liệu để được unaligned để có được một tham nhũng tiềm năng. Từ khóa dễ bay hơi sẽ không ngăn cản tham nhũng tiềm ẩn nếu dữ liệu không thể xử lý nguyên tử, ví dụ, nếu bộ đếm vòng lặp dài loại (64 bit) thì nó sẽ yêu cầu hai hoạt động 32 bit để cập nhật giá trị, ở giữa mà một ngắt có thể xảy ra và thay đổi dữ liệu.

Vì vậy, từ khóa dễ bay hơi chỉ tốt cho dữ liệu được căn chỉnh nhỏ hơn hoặc bằng kích thước của thanh ghi gốc để các phép toán luôn luôn nguyên tử.

Từ khóa dễ bay hơi được hình thành để sử dụng với các hoạt động IO trong đó IO sẽ liên tục thay đổi nhưng có địa chỉ liên tục, chẳng hạn như bộ nhớ UART được ánh xạ bộ nhớ và trình biên dịch sẽ không tiếp tục sử dụng lại giá trị đầu tiên được đọc từ địa chỉ nhà.

Nếu bạn đang xử lý dữ liệu lớn hoặc có nhiều CPU thì bạn sẽ cần hệ thống khóa cấp cao hơn (OS) để xử lý truy cập dữ liệu đúng cách.

+0

Đây là C++ nhưng nguyên tắc áp dụng cho C#. – Skizz

+8

Không chính xác: http://www.ddj.com/hpc-high-performance-computing/212701484 –

+4

Eric Lippert viết rằng dễ bay hơi trong C++ chỉ ngăn trình biên dịch thực hiện một số tối ưu, trong khi trong C# dễ bay hơi thêm một số thông tin liên lạc giữa các lõi/bộ xử lý khác để đảm bảo rằng giá trị mới nhất được đọc. –

-2

nhiều luồng có thể truy cập vào một biến. Cập nhật mới nhất sẽ có trên biến

33

Nếu bạn đang sử dụng .NET 1.1, từ khóa dễ bay hơi là cần thiết khi thực hiện kiểm tra khóa kép. Tại sao? Bởi vì trước .NET 2.0, kịch bản sau có thể khiến luồng thứ hai truy cập đối tượng không rỗng, chưa được xây dựng đầy đủ:

  1. Thread 1 hỏi có biến không. //if(this.foo == null)
  2. Chủ đề 1 xác định biến là null, vì vậy hãy nhập khóa. //lock(this.bar)
  3. Chủ đề 1 yêu cầu AGAIN nếu biến đó là null. //if(this.foo == null)
  4. Chủ đề 1 vẫn xác định biến là null, vì vậy nó gọi một hàm tạo và gán giá trị cho biến đó. //this.foo = new Foo();

Trước .NET 2.0, this.foo có thể được gán phiên bản mới của Foo, trước khi hàm tạo được chạy xong. Trong trường hợp này, một chuỗi thứ hai có thể xuất hiện (trong suốt quá trình tạo luồng của hàm 1 đến hàm tạo của Foo) và trải nghiệm những điều sau đây:

  1. Thread 2 hỏi if variable is null. //if(this.foo == null)
  2. Chủ đề 2 xác định biến KHÔNG phải là rỗng, vì vậy hãy thử sử dụng nó. //this.foo.MakeFoo()

Trước .NET 2.0, bạn có thể tuyên bố this.foo là dễ bay hơi để giải quyết vấn đề này. Kể từ .NET 2.0, bạn không còn cần phải sử dụng từ khóa dễ bay hơi để thực hiện khóa đã kiểm tra kép.

Wikipedia thực sự có một bài viết tốt về Double Checked Locking, và một thời gian ngắn đề cập đến chủ đề này: http://en.wikipedia.org/wiki/Double-checked_locking

+1

đây chính xác là những gì tôi thấy trong một mã di sản và đã tự hỏi về nó. đó là lý do tại sao tôi bắt đầu một nghiên cứu sâu hơn. Cảm ơn! –

1

Trình biên dịch đôi khi thay đổi thứ tự của báo cáo trong mã để tối ưu hóa nó. Thông thường, đây không phải là một vấn đề trong môi trường luồng đơn, nhưng nó có thể là một vấn đề trong môi trường đa luồng. Xem sau Ví dụ:

private static int _flag = 0; 
private static int _value = 0; 

var t1 = Task.Run(() => 
{ 
    _value = 10; /* compiler could switch these lines */ 
    _flag = 5; 
}); 

var t2 = Task.Run(() => 
{ 
    if (_flag == 5) 
    { 
     Console.WriteLine("Value: {0}", _value); 
    } 
}); 

Nếu bạn chạy t1 và t2, bạn mong chờ không có đầu ra hoặc "Giá trị: 10" như kết quả. Nó có thể là trình biên dịch chuyển đổi dòng bên trong hàm t1. Nếu t2 sau đó thực hiện, nó có thể là _flag có giá trị là 5, nhưng _value có 0. Vì vậy, logic dự kiến ​​có thể bị hỏng.

Để khắc phục điều này, bạn có thể sử dụng dễ bay hơi từ khóa mà bạn có thể áp dụng cho trường này. Câu lệnh này vô hiệu hóa tối ưu hóa trình biên dịch để bạn có thể buộc đúng thứ tự trong mã của bạn.

private static volatile int _flag = 0; 

Bạn nên sử dụng dễ bay hơi chỉ khi bạn thực sự cần nó, bởi vì nó vô hiệu hóa tối ưu hóa trình biên dịch nhất định, nó sẽ làm tổn thương thực hiện. Nó cũng không được hỗ trợ bởi tất cả các ngôn ngữ .NET (Visual Basic không hỗ trợ nó), do đó nó cản trở khả năng tương tác ngôn ngữ.

+1

Ví dụ của bạn thực sự là xấu. Lập trình viên sẽ không bao giờ có bất kỳ kỳ vọng nào về giá trị của _flag trong nhiệm vụ t2 dựa trên thực tế là mã của t1 được viết đầu tiên. Viết đầu tiên! Nó không quan trọng nếu trình biên dịch DOES chuyển đổi hai dòng trong t1. Ngay cả khi trình biên dịch không chuyển các câu lệnh đó, Console.WriteLne của bạn trong nhánh khác vẫn có thể thực thi, ngay cả VỚI từ khóa dễ bay hơi trên _flag. – Jakotheshadows

+0

@ jakotheshadows, bạn nói đúng, tôi đã chỉnh sửa câu trả lời của tôi. Ý tưởng chính của tôi là để cho thấy rằng logic dự kiến ​​có thể bị phá vỡ khi chúng tôi chạy t1 và t2 cùng một lúc –

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