it-swarm-vi.tech

Khi nào nên sử dụng từ khóa dễ bay hơi trong C #?

Bất cứ ai cũng có thể cung cấp một lời giải thích tốt về từ khóa dễ bay hơi trong C #? 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?

279
Doron Yaacoby

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

Trong C #, "volility" 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 cho biến này". Nó cũng có nghĩa là "nói cho các bộ xử lý biết 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ộ xử lý khác và tạo Chúng đồng bộ hóa bộ nhớ chính với bộ nhớ cache ".

Thật ra, chút 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 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ý đều dừng những gì nó đang thực hiện và cập nhật bộ nhớ cache vào/từ bộ nhớ chính. Thay vào đó, họ cung cấp đả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 để được ra lệnh đối với nhau . Một số thao tác như tạo một luồng mới, nhập khóa hoặc sử dụng một trong các phương thức lồng vào nhau giới thiệu mạnh hơn đảm bảo về việc quan sát đặt hàng. Nếu bạn muốn biết thêm chi tiết, đọc phần 3.10 và 10.5.3 của thông số kỹ thuật C # 4.0.

Thành thật mà nói, Tôi không khuyến khích bạn tạo ra một lĩnh vực đầy biến động. Bay hơi các trường là một dấu hiệu cho thấy bạn đang làm điều gì đó hết sức điên rồ: bạn cố gắng đọc và viết cùng một giá trị trên hai luồng khác nhau mà không đặt mộ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à phù hợp, đảm bảo khóa rằng chỉ có một luồng truy cập vào một đoạn bộ nhớ nhất định tại một thời điểm, và vì vậy trên. 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 bởi vì bạn không hiểu mô hình bộ nhớ chính xác là rất lớn. TÔI đừng cố viết bất kỳ mã khóa thấp nào ngoại trừ tầm thường nhất tập quán của các hoạt động lồng vào nhau. Tôi để việc sử dụng "dễ bay hơi" thành các chuyên gia thực sự.

Để đọc thêm xem:

254
Ohad Schneider

Nếu bạn muốn có thêm một chút kỹ thuật về những gì từ khóa dễ bay hơi, hãy xem xét chương trình sau (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 cài đặt trình biên dịch (phát hành) được tối ưu hóa tiêu chuẩn, trình biên dịch sẽ tạo trình biên dịch chương trình (IA32) sau:

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 trữ giá trị của biến j. Đối với vòng lặp không bay hơi (đầu tiên) trình biên dịch đã gán i cho thanh ghi eax. Khá đơn giản. Mặc dù vậy, có một số bit thú vị - lệnh eb ebx, [ebx] thực sự là một lệnh nop đa bào để vòng lặp nhảy đến địa chỉ bộ nhớ được căn chỉnh 16 byte. Khác là sử dụng edx để tăng bộ đếm vòng thay vì sử dụng lệnh inc eax. Lệnh add reg, reg có độ trễ thấp hơn trên một vài lõi IA32 so với lệnh inc 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 [đặc biệt] và từ khóa dễ bay hơi cho trình biên dịch biết giá trị phải 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 không thực hiện tải/tăng/lưu trữ theo ba bước riêng biệt (tải eax, inc eax, save 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 (thêm mem , reg). Cách mã được tạo ra đảm bảo giá trị của bộ đếm vòng lặp luôn cập nhật trong bối cảnh của một lõi CPU. Không có thao tác nào trên dữ liệu có thể dẫn đến hỏng hoặc mất dữ liệu (do đó không sử dụng tải/inc/store 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ụ 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ớ không được phân bổ.

Khi bạn giới thiệu CPU thứ hai cho hệ thống, từ khóa dễ bay hơi sẽ không bảo vệ dữ liệu được cập nhật bởi CPU khác cùng lúc. Trong ví dụ trên, bạn sẽ cần dữ liệu không được sắp xếp để có được tham nhũng tiềm năng. Từ khóa dễ bay hơi sẽ không ngăn ngừa tham nhũng tiềm ẩn nếu dữ liệu không thể được xử lý nguyên tử, ví dụ, nếu bộ đếm vòng lặp có loại dài (64 bit) thì sẽ cần hai thao tác 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 các thanh ghi riêng sao cho các hoạt động luôn luôn là nguyên tử.

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

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

54
Skizz

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 khóa kiểm tra kép. Tại sao? Bởi vì trước .NET 2.0, kịch bản sau đây có thể khiến luồng thứ hai truy cập vào một đối tượng không null, nhưng chưa được xây dựng đầy đủ:

  1. Chủ đề 1 hỏi nếu một biến là null . // 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 hỏi LẠI 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 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 chỉ định phiên bản mới của Foo, trước khi hàm tạo hoàn tất chạy. Trong trường hợp này, một luồng thứ hai có thể đi vào (trong cuộc gọi của luồng 1 đến hàm tạo của Foo) và trải nghiệm như sau:

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

Trước .NET 2.0, bạn có thể khai báo this.foo là không ổn định để khắc phục 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 hay về Khóa kiểm tra hai lần và chạm nhanh vào chủ đề này: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

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

22
Dr. Bob

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

Bạn có thể nghĩ về 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 lấy giá trị mới nhất.

20
Benoit

CLR thích tối ưu hóa các hướng dẫn, vì vậy khi bạn truy cập vào một trường trong mã, có thể không phải lúc nào cũng truy cập vào giá trị hiện tại của trường (có thể là 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 theo hướng dẫn. Điều này hữu ích khi giá trị có thể được sửa đổi (trong kịch bản không khóa) bởi một luồng đồ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 mất một số tối ưu hóa, nhưng nó giữ cho mã đơn giản hơn.

13
Joseph Daigle

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

 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 sẽ không có kết quả đầu ra hoặc "Giá trị: 10". Nó có thể là trình biên dịch chuyển dòng bên trong chức năng t1. Nếu t2 sau đó thực thi, có thể là _flag có giá trị là 5, nhưng _value có 0. Vì vậy logic dự kiến ​​có thể bị phá vỡ. 

Để khắc phục điều này, bạn có thể sử dụng biến động từ khóa mà bạn có thể áp dụng cho trường. 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 thứ tự đúng 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 giảm hiệu suất. 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ó), vì vậy nó cản trở khả năng tương tác ngôn ngữ.

0
Aliaksei Maniuk

Vì vậy, để tổng hợp tất cả những điều này, câu trả lời chính xác cho câu hỏi là: Nếu mã của bạn đang chạy trong thời gian chạy 2.0 trở lên, từ khóa dễ bay hơi gần như không bao giờ cần thiết và gây hại nhiều hơn là tốt nếu sử dụng không cần thiết. I E. Đừng bao giờ sử dụng nó. NHƯNG trong các phiên bản trước của thời gian chạy, nó IS cần thiết cho khóa kiểm tra kép thích hợp trên các trường tĩnh. Các trường tĩnh cụ thể có lớp có mã khởi tạo lớp tĩnh.

0
Paul Easter