2010-09-09 36 views
11

Đọc this question, tôi muốn kiểm tra xem tôi có thể chứng minh tính không nguyên tử của các lần đọc và viết trên một loại mà nguyên tử của các hoạt động đó không được đảm bảo hay không.Tại sao mã này không thể hiện tính nguyên tử của các lần đọc/ghi?

private static double _d; 

[STAThread] 
static void Main() 
{ 
    new Thread(KeepMutating).Start(); 
    KeepReading(); 
} 

private static void KeepReading() 
{ 
    while (true) 
    { 
     double dCopy = _d; 

     // In release: if (...) throw ... 
     Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails 
    } 
} 

private static void KeepMutating() 
{ 
    Random rand = new Random(); 
    while (true) 
    { 
     _d = rand.Next(2) == 0 ? 0D : double.MaxValue; 
    } 
} 

Thật ngạc nhiên, khẳng định từ chối không thành công ngay cả sau ba phút thực thi đầy đủ. Điều gì mang lại?

  1. Bài kiểm tra không chính xác.
  2. Đặc điểm thời gian cụ thể của thử nghiệm làm cho nó khó xảy ra/không thể xác nhận sẽ thất bại.
  3. Xác suất quá thấp đến mức tôi phải chạy thử nghiệm lâu hơn nữa để có khả năng nó sẽ kích hoạt.
  4. CLR cung cấp đảm bảo mạnh hơn về nguyên tử so với thông số C#.
  5. Hệ điều hành/phần cứng của tôi cung cấp đảm bảo mạnh hơn CLR.
  6. Cái gì khác?

Tất nhiên, tôi không có ý định dựa vào bất kỳ hành vi nào không được đảm bảo rõ ràng bởi thông số kỹ thuật, nhưng tôi muốn hiểu rõ hơn về vấn đề này.

FYI, tôi chạy này trên cả hai Debug và phát hành (thay đổi Debug.Assert-if(..) throw) cấu hình trong hai môi trường riêng biệt:

  1. Windows 7 64-bit + .NET 3.5 SP1
  2. Windows XP 32-bit + NET 2,0

EDIT: để loại trừ khả năng bình luận John Kugelman của "trình gỡ lỗi là không Schrodinger-an toàn" là vấn đề, tôi đã thêm dòng someList.Add(dCopy); đến KeepReading phương pháp và veri fied rằng danh sách này không nhìn thấy một giá trị cũ duy nhất từ ​​bộ nhớ cache.

EDIT: Dựa trên đề xuất của Dan Bryant: Sử dụng long thay vì xóa double ngay lập tức.

+0

Đoán của tôi (và rất nhiều chỉ là vậy) là # 3. – AakashM

+0

Tôi muốn kiểm tra IL để đảm bảo rằng trình biên dịch không chơi bất kỳ thủ thuật nào, nhưng khác với điều tôi không nhận được. Tôi hy vọng điều này sẽ phá vỡ trên một máy 32-bit. – mquander

+1

Tôi sẽ tăng số lượng các chủ đề viết - cố gắng giữ các luồng N + 1 chạy trên hệ thống N-core. –

Trả lời

12

Bạn có thể thử chạy qua CHESS để xem liệu nó có thể buộc một sự xen kẽ làm hỏng thử nghiệm hay không.

Nếu bạn nhìn vào x86 diassembly (có thể nhìn thấy từ trình gỡ lỗi), bạn cũng có thể xem liệu jitter có tạo ra các hướng dẫn bảo toàn nguyên tử hay không.


EDIT: Tôi đã tiếp tục và chạy tháo gỡ (buộc mục tiêu x86). Các dòng có liên quan là:

   double dCopy = _d; 
00000039 fld   qword ptr ds:[00511650h] 
0000003f fstp  qword ptr [ebp-40h] 

       _d = rand.Next(2) == 0 ? 0D : double.MaxValue; 
00000054 mov   ecx,dword ptr [ebp-3Ch] 
00000057 mov   edx,2 
0000005c mov   eax,dword ptr [ecx] 
0000005e mov   eax,dword ptr [eax+28h] 
00000061 call  dword ptr [eax+1Ch] 
00000064 mov   dword ptr [ebp-48h],eax 
00000067 cmp   dword ptr [ebp-48h],0 
0000006b je   00000079 
0000006d nop 
0000006e fld   qword ptr ds:[002423D8h] 
00000074 fstp  qword ptr [ebp-50h] 
00000077 jmp   0000007E 
00000079 fldz 
0000007b fstp  qword ptr [ebp-50h] 
0000007e fld   qword ptr [ebp-50h] 
00000081 fstp  qword ptr ds:[00159E78h] 

Nó sử dụng một cụm từ mật khẩu fstp để thực hiện thao tác ghi trong cả hai trường hợp. Tôi đoán là CPU Intel đảm bảo nguyên tử của hoạt động này, mặc dù tôi không tìm thấy bất kỳ tài liệu nào để hỗ trợ điều này. Bất kỳ chuyên gia x86 nào có thể xác nhận điều này?


UPDATE:

này không như mong đợi nếu bạn sử dụng Int64, trong đó sử dụng các thanh ghi 32-bit trên CPU x86 chứ không phải là thanh ghi FPU đặc biệt.Bạn có thể thấy điều này dưới đây:

   Int64 dCopy = _d; 
00000042 mov   eax,dword ptr ds:[001A9E78h] 
00000047 mov   edx,dword ptr ds:[001A9E7Ch] 
0000004d mov   dword ptr [ebp-40h],eax 
00000050 mov   dword ptr [ebp-3Ch],edx 

UPDATE:

Tôi đã tò mò nếu điều này sẽ thất bại nếu tôi buộc phải liên kết phi 8byte của trường đôi trong bộ nhớ, vì vậy tôi đặt cùng mã này:

[StructLayout(LayoutKind.Explicit)] 
    private struct Test 
    { 
     [FieldOffset(0)] 
     public double _d1; 

     [FieldOffset(4)] 
     public double _d2; 
    } 

    private static Test _test; 

    [STAThread] 
    static void Main() 
    { 
     new Thread(KeepMutating).Start(); 
     KeepReading(); 
    } 

    private static void KeepReading() 
    { 
     while (true) 
     { 
      double dummy = _test._d1; 
      double dCopy = _test._d2; 

      // In release: if (...) throw ... 
      Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails 
     } 
    } 

    private static void KeepMutating() 
    { 
     Random rand = new Random(); 
     while (true) 
     { 
      _test._d2 = rand.Next(2) == 0 ? 0D : double.MaxValue; 
     } 
    } 

Nó không thất bại và hướng dẫn x86 tạo cơ bản là giống như trước:

   double dummy = _test._d1; 
0000003e mov   eax,dword ptr ds:[03A75B20h] 
00000043 fld   qword ptr [eax+4] 
00000046 fstp  qword ptr [ebp-40h] 
       double dCopy = _test._d2; 
00000049 mov   eax,dword ptr ds:[03A75B20h] 
0000004e fld   qword ptr [eax+8] 
00000051 fstp  qword ptr [ebp-48h] 

Tôi đã thử nghiệm với việc hoán đổi _d1 và _d2 để sử dụng với dCopy/set và cũng đã thử FieldOffset 2. Tất cả đều tạo cùng một hướng dẫn cơ bản (với các offset khác nhau ở trên) và tất cả đều không thành công sau vài giây (có thể là hàng tỷ lần thử)). Tôi rất tự tin, với những kết quả này, ít nhất là các CPU Intel x86 cung cấp nguyên tử của các hoạt động tải/lưu trữ kép, bất kể sự liên kết.

3

Trình biên dịch được phép tối ưu hóa các lần đọc lặp đi lặp lại của _d. Theo như nó biết chỉ phân tích tĩnh vòng lặp của bạn, _d không bao giờ thay đổi. Điều này có nghĩa là nó có thể cache giá trị và không bao giờ đọc lại trường.

Để ngăn chặn điều này, bạn cần phải đồng bộ hóa quyền truy cập vào _d (ví dụ: bao quanh tuyên bố lock) hoặc đánh dấu _dvolatile. Làm cho nó dễ bay hơi nói với trình biên dịch rằng giá trị của nó có thể thay đổi bất cứ lúc nào và vì vậy nó không bao giờ nên nhớ giá trị.

Thật không may (hoặc may mắn thay), bạn không thể đánh dấu trường doublevolatile, chính xác vì điểm bạn đang thử kiểm tra — double không thể truy cập nguyên tử! Đồng bộ hóa quyền truy cập vào _d là lực lượng trình biên dịch đọc lại giá trị, nhưng điều đó cũng phá vỡ thử nghiệm. Oh well!

+0

Tôi có thể thấy rằng phương thức 'KeepReading' thấy các giá trị khác nhau của' _d' trong trình gỡ lỗi. Ngoài ra, nhìn vào IL, dòng đầu tiên * bên trong * vòng lặp là 'ldsfld float64 Tester.Program :: d', do đó, không có trình biên dịch tối ưu hóa đang diễn ra. – Ani

+0

Trình gỡ lỗi không an toàn cho Schrodinger. Bạn đang cố gắng kiểm tra mức độ rất thấp. Điều này là nhận được một chút vượt quá mức lương của tôi, nhưng tôi nghi ngờ các trình tối ưu hóa JIT có thể tối ưu hóa các lần đọc đi trong thời gian chạy. Thật khó để nói, nó có thể là # 2, # 3 hoặC# 4. –

2

Bạn có thể thử loại bỏ 'dCopy = _d' và chỉ cần sử dụng _d trong xác nhận của bạn.

Bằng cách đó, hai chủ đề đang đọc/ghi cùng một biến cùng một lúc.

phiên bản hiện tại của bạn làm cho một bản sao của _D mà tạo ra một trường hợp mới, tất cả trong cùng một thread, mà là một chủ đề hoạt động an toàn:

http://msdn.microsoft.com/en-us/library/system.double.aspx

Tất cả thành viên này loại là chủ đề an toàn. Các thành viên xuất hiện để sửa đổi trạng thái cá thể thực sự trả lại một cá thể mới được khởi tạo với giá trị mới. Như với bất kỳ loại nào khác, việc đọc và ghi vào một biến chia sẻ có chứa một thể hiện của loại này phải được bảo vệ bằng một khóa để đảm bảo an toàn luồng.

Tuy nhiên nếu cả hai chủ đề đang đọc/ghi vào các trường hợp biến cùng thì:

http://msdn.microsoft.com/en-us/library/system.double.aspx

Gán một thể hiện của loại hình này không phải là thread an toàn trên tất cả các nền tảng phần cứng bởi vì biểu diễn nhị phân của cá thể đó có thể quá lớn để gán trong một phép toán nguyên tử duy nhất.

Vì vậy, nếu cả hai chủ đề đang đọc/ghi vào các trường hợp biến cùng bạn sẽ cần một khóa để bảo vệ nó (hoặc Interlocked.Read/Increment/Exchange., Không chắc chắn nếu hoạt động trên đôi)

Chỉnh sửa

Như được chỉ ra bởi những người khác, trên CPU Intel đọc/ghi đôi là hoạt động nguyên tử. Tuy nhiên, nếu chương trình được biên dịch cho X86 và sử dụng kiểu dữ liệu số nguyên 64 bit, thì thao tác sẽ không phải là nguyên tử. Như đã trình bày trong chương trình sau. Thay thế Int64 với đôi và nó xuất hiện để làm việc.

Public Const ThreadCount As Integer = 2 
    Public thrdsWrite() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} 
    Public thrdsRead() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {} 
    Public d As Int64 

    <STAThread()> _ 
    Sub Main() 

     For i As Integer = 0 To thrdsWrite.Length - 1 

      thrdsWrite(i) = New Threading.Thread(AddressOf Write) 
      thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA) 
      thrdsWrite(i).IsBackground = True 
      thrdsWrite(i).Start() 

      thrdsRead(i) = New Threading.Thread(AddressOf Read) 
      thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA) 
      thrdsRead(i).IsBackground = True 
      thrdsRead(i).Start() 

     Next 

     Console.ReadKey() 

    End Sub 

    Public Sub Write() 

     Dim rnd As New Random(DateTime.Now.Millisecond) 
     While True 
      d = If(rnd.Next(2) = 0, 0, Int64.MaxValue) 
     End While 

    End Sub 

    Public Sub Read() 

     While True 
      Dim dc As Int64 = d 
      If (dc <> 0) And (dc <> Int64.MaxValue) Then 
       Console.WriteLine(dc) 
      End If 
     End While 

    End Sub 
+0

Là một sang một bên, bạn có thể thử sử dụng một cấu trúc khác với 'Xác nhận'. –

+0

Trong chế độ phát hành, tôi đã thay thế nó bằng 'if (...) throw'. – Ani

+0

Xóa dCopy không làm gì cả (ngoại trừ làm cho chuỗi đọc _d hai lần thay vì một lần). –

0

IMO câu trả lời đúng là # 5.

đôi dài 8 byte.

Giao diện bộ nhớ là 64 bit = 8 byte cho mỗi mô-đun cho mỗi đồng hồ (tức là nó trở thành 16 byte cho bộ nhớ kênh đôi).

Cũng có bộ đệm CPU. Trên máy của tôi, dòng bộ nhớ cache là 64 byte và trên tất cả các CPU, nó là bội số của 8.

Như đã nói ở trên, ngay cả khi CPU đang chạy ở chế độ 32 bit, các biến kép được tải và lưu trữ chỉ cần 1 hướng dẫn.

Đó là lý do tại sao miễn là biến kép của bạn được căn chỉnh (tôi nghi ngờ máy ảo thời gian chạy ngôn ngữ phổ biến không căn chỉnh cho bạn), việc đọc và ghi kép là nguyên tử.

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