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?
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?
Câu trả lời:
Tôi không nghĩ có người nào trả lời tốt hơn Eric Lippert (nhấn mạnh vào 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ý tối ưu hóa bộ đệm ẩn trên biến này". Nó cũng có nghĩa là "bảo các bộ 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ộ xử lý khác và khiến chúng đồng bộ hóa bộ nhớ chính với bộ nhớ cache của chúng".
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 việc đọc và viết dễ bay hơi phức tạp hơn đáng kể so với những gì 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 làm và cập nhật bộ nhớ cache vào / từ bộ nhớ chính. Thay vào đó, họ 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à ghi có thể được quan sát để được sắp xếp theo thứ tự đối với nhau . Một số hoạt động nhất định 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 các đảm bảo mạnh mẽ hơn về quan sát đặt hàng. Nếu bạn muốn biết thêm chi tiết, hãy đọc cá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 làm một lĩnh vực đầy biến động . Các trường dễ bay hơi 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 đang cố đọc và viết cùng một giá trị trên hai luồng khác nhau mà không đặt khóa. 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, khóa đảm bảo 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. Số lượng tình huống 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 ngoại trừ các cách sử dụng tầm thường nhất của các hoạt động lồng vào nhau. Tôi để việc sử dụng "dễ bay hơi" cho các chuyên gia thực sự.
Để đọc thêm xem:
volatile
sẽ có ở đó nhờ vào khóa
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 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 (lần đầ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 lặp 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 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 tham nhũ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 hướng dẫn hiện tại đã hoàn thành,
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ị UART được ánh xạ bộ nhớ 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.
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 đủ:
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:
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
foo
? Không phải là khóa 1 this.bar
và do đó chỉ có luồng 1 mới có thể khởi tạo foo tại một thời điểm givne? Ý tôi là, bạn hãy kiểm tra giá trị sau khi khóa được phát hành lại, khi nào thì nó cũng phải có giá trị mới từ luồng 1.
Đô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 thực hiện ghi vào trường và một 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.
Từ MSDN : Công cụ sửa đổi dễ bay hơi thường được sử dụng cho một trường được truy cập bởi nhiều luồng 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.
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ã, nó 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 lĩnh vực làvolatile
đảm bảo rằng giá trị hiện tại của trường được truy cập bởi lệnh. Đ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.
Tôi thấy bài viết này của Joydip Kanjilal rất hữu ích!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
Tôi sẽ chỉ để nó ở đây để tham khảo
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 từ khóa dễ bay hơi 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 chỉ nên sử dụng biến động nếu 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ữ.
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. IE Đừ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ó 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.
nhiều chủ đề có thể truy cập một biến. Bản cập nhật mới nhất sẽ có trên biến