2011-08-28 27 views
11

Tôi chỉ đọc một bài viết MSDN, "Synchronization and Multiprocessor Issues", địa chỉ các vấn đề về bộ nhớ cache nhất quán trên các máy đa xử lý. Đây thực sự là con mắt mở với tôi, bởi vì tôi sẽ không nghĩ rằng có thể là một điều kiện chủng tộc trong ví dụ mà họ cung cấp. Bài viết này giải thích rằng ghi vào bộ nhớ có thể không thực sự xảy ra (từ quan điểm của cpu khác) theo thứ tự được viết trong mã của tôi. Đây là một khái niệm mới với tôi!Tránh các vấn đề nhất quán về bộ nhớ cache trong Delphi với một phần quan trọng?

Bài viết này cung cấp 2 giải pháp:

  1. Sử dụng từ khóa "không ổn định" trên các biến cần bộ nhớ cache nhất quán trên nhiều CPU. Đây là một từ khóa C/C++, và không có sẵn cho tôi trong Delphi.
  2. Sử dụng InterlockExchange() và InterlockCompareExchange(). Đây là điều tôi có thể làm ở Delphi nếu tôi phải làm vậy. Nó chỉ có vẻ hơi lộn xộn một chút.

Bài viết cũng đề cập rằng "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 chức năng nhập hoặc rời các phần quan trọng".

Đây là phần tôi không hiểu. Điều này có nghĩa rằng bất kỳ ghi vào bộ nhớ được giới hạn cho các chức năng sử dụng các phần quan trọng được miễn dịch từ sự nhất quán bộ nhớ cache và các vấn đề đặt hàng bộ nhớ? Tôi không có gì chống lại các chức năng Interlock *(), nhưng một công cụ khác trong vành đai dụng cụ của tôi sẽ rất tốt!

Trả lời

7

Trước hết, theo các tiêu chuẩn ngôn ngữ, dễ bay hơi không làm những gì bài báo nói. Các ngữ nghĩa thu được và phát hành của biến động là MSVC cụ thể. Điều này có thể là một vấn đề nếu bạn biên dịch với các trình biên dịch khác hoặc trên các nền tảng khác. C++ 11 giới thiệu các biến nguyên tử được hỗ trợ ngôn ngữ mà hy vọng, trong khóa học do đó, cuối cùng đã chấm dứt việc sử dụng sai (sai) như là một cấu trúc luồng.

Các phần quan trọng và mutexes thực sự được thực hiện để đọc và ghi các biến được bảo vệ sẽ được xem chính xác từ tất cả các chuỗi.

Tôi nghĩ rằng cách tốt nhất để nghĩ về các phần quan trọng và mutexes (khóa) là thiết bị để mang về serialization. Đó là, các khối mã được bảo vệ bởi các khóa như vậy được thực hiện serially, cái khác mà không trùng lặp. Việc serialization cũng áp dụng cho việc truy cập bộ nhớ. Có thể không có vấn đề gì do sự kết hợp bộ nhớ cache hoặc đọc/ghi sắp xếp lại.

Chức năng khóa liên động được thực hiện bằng cách sử dụng khóa dựa trên phần cứng trên bus bộ nhớ. Các chức năng này được sử dụng bởi các thuật toán khóa miễn phí. Điều này có nghĩa là họ không sử dụng ổ khóa nặng như những phần quan trọng, mà đúng hơn là những ổ khóa phần cứng trọng lượng nhẹ này.

Khóa các thuật toán miễn phí có thể hiệu quả hơn các thuật toán dựa trên khóa, nhưng khóa các thuật toán miễn phí có thể khó viết hơn rất nhiều. Ưu tiên các phần quan trọng về khóa tự do trừ khi các hàm ý hiệu suất là đáng chú ý.

Một bài viết khác đáng đọc là The "Double-Checked Locking is Broken" Declaration.

+1

David, thường có tham chiếu đến boolean, số nguyên, v.v ... là nguyên tử (nếu được căn chỉnh đúng) và do đó luồng an toàn. Tôi nghĩ rằng câu trả lời được chấp nhận ở đây http://stackoverflow.com/questions/510031/list-of-delphi-data-types-with-atomic-read-write-operations đặt điều này theo quan điểm. quote "Đọc là chủ đề an toàn. Viết không phải là chủ đề an toàn." –

+0

@LU RD Bạn cần phải chính xác. Bạn có ý nghĩa gì bởi threadsafe? Và chính xác những gì "Đọc là thread an toàn, viết không phải là thread an toàn" nghĩa là gì? –

+0

nó chỉ là một cảnh báo chung không giả định người ta có thể xem xét hoạt động trên cái gọi là biến nguyên tử an toàn để sử dụng trong tất cả các bối cảnh luồng. –

7

Bài viết MSDN này chỉ là bước đầu tiên của phát triển ứng dụng đa luồng: ngắn gọn, có nghĩa là "bảo vệ các biến được chia sẻ với khóa (còn gọi là các phần quan trọng), vì bạn không chắc chắn rằng dữ liệu bạn đọc/ghi là tương tự cho tất cả các chủ đề ".

Bộ nhớ cache lõi CPU chỉ là một trong những vấn đề có thể xảy ra, điều này sẽ dẫn đến việc đọc các giá trị sai.Một vấn đề khác có thể dẫn đến tình trạng đua là hai luồng ghi vào một tài nguyên cùng một lúc: không thể biết được giá trị nào sẽ được lưu trữ sau đó.

Vì mã mong đợi dữ liệu được kết hợp, một số chương trình đa luồng có thể hoạt động sai. Với đa luồng, bạn không chắc chắn rằng mã bạn viết, thông qua các lệnh riêng lẻ, được thực thi như mong đợi, khi nó xử lý các biến chia sẻ. Các chức năng

InterlockedExchange/InterlockedIncrement là các mã opmode cấp thấp với tiền tố LOCK (hoặc bị khóa bằng thiết kế, như mã vạch XCHG EDX,[EAX]), thực sự sẽ buộc kết hợp bộ nhớ cache cho tất cả các lõi CPU. .

Ví dụ, ở đây là làm thế nào một số chuỗi tài liệu tham khảo được thực hiện khi bạn gán một giá trị chuỗi (xem _LStrAsg trong System.pas - đây là từ our optimized version of the RTL for Delphi 7/2002 - kể từ Delphi mã ban đầu là có bản quyền):

  MOV  ECX,[EDX-skew].StrRec.refCnt 
      INC  ECX { thread-unsafe increment ECX = reference count } 
      JG  @@1 { ECX=-1 -> literal string -> jump not taken } 
      ..... 
     @@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count } 
      MOV  ECX,[EAX] 
      ... 

Có sự khác biệt giữa INC ECXLOCK INC [EDX-skew].StrRec.refCnt đầu tiên - không chỉ gia số ECX đầu tiên mà không phải biến số tham chiếu, nhưng biến đầu tiên không an toàn trong luồng, trong khi mã thứ hai được đặt trước bởi LOCK do đó sẽ an toàn chỉ.

Nhân tiện, tiền tố LOCK này là một trong những vấn đề của multi-thread scaling in the RTL - nó tốt hơn với các CPU mới hơn, nhưng vẫn không hoàn hảo.

Vì vậy, sử dụng các phần quan trọng là cách dễ nhất làm một mã thread-safe:

var GlobalVariable: string; 
    GlobalSection: TRTLCriticalSection; 

procedure TThreadOne.Execute; 
var LocalVariable: string; 
begin 
    ... 
    EnterCriticalSection(GlobalSection); 
    LocalVariable := GlobalVariable+'a'; { modify GlobalVariable } 
    GlobalVariable := LocalVariable; 
    LeaveCriticalSection(GlobalSection); 
    .... 
end; 

procedure TThreadTwp.Execute; 
var LocalVariable: string; 
begin 
    ... 
    EnterCriticalSection(GlobalSection); 
    LocalVariable := GlobalVariable; { thread-safe read GlobalVariable } 
    LeaveCriticalSection(GlobalSection); 
    .... 
end; 

Sử dụng một biến địa phương làm cho các phần quan trọng ngắn hơn, do đó ứng dụng của bạn sẽ quy mô tốt hơn và tận dụng toàn bộ sức mạnh lõi CPU của bạn. Giữa EnterCriticalSectionLeaveCriticalSection, chỉ một chuỗi sẽ chạy: các chuỗi khác sẽ chờ trong cuộc gọi EnterCriticalSection ... Vì vậy, phần càng ngắn càng quan trọng, ứng dụng của bạn càng nhanh. Một số ứng dụng đa luồng được thiết kế sai có thể thực sự chậm hơn các ứng dụng đơn luồng! Và đừng quên rằng nếu mã của bạn bên trong phần quan trọng có thể làm tăng ngoại lệ, bạn nên luôn viết một khối try ... finally LeaveCriticalSection() end; rõ ràng để bảo vệ việc khóa và ngăn chặn bất kỳ khóa chết nào trong ứng dụng của bạn.

Delphi hoàn toàn an toàn theo chủ đề nếu bạn bảo vệ dữ liệu được chia sẻ bằng khóa, tức là phần quan trọng. Hãy lưu ý rằng ngay cả khi các biến được tính tham chiếu (như chuỗi) cần được bảo vệ, ngay cả khi có một LOCK bên trong các hàm RTL của chúng: LOCK này là để tính toán tham chiếu chính xác và tránh rò rỉ bộ nhớ, nhưng nó sẽ không an toàn thread . Để làm cho nó càng nhanh càng tốt, see this SO question.

Mục đích của InterlockExchangeInterlockCompareExchange là thay đổi giá trị biến con trỏ được chia sẻ. Bạn có thể xem nó như là một phiên bản "nhẹ" của phần quan trọng để truy cập một giá trị con trỏ.

Trong mọi trường hợp, viết mã đa luồng hoạt động không phải là dễ dàng - thậm chí là cứng, as a Delphi expert just wrote in his blog.

Bạn nên viết các luồng đơn giản mà không có dữ liệu được chia sẻ (tạo bản sao dữ liệu riêng trước khi bắt đầu chuỗi hoặc sử dụng dữ liệu được chia sẻ chỉ đọc - chủ đề an toàn) hoặc gọi một số các thư viện được thiết kế và đã được chứng minh - như http://otl.17slon.com - sẽ giúp bạn tiết kiệm rất nhiều thời gian gỡ lỗi.

+0

Cảm ơn lời khuyên. Rất tốt mẹo để tránh bộ nhớ chia sẻ và chỉ cần làm một bản sao để làm việc trên. Ngoài ra, tôi có thể thấy InterlockExchange có thể giúp thực hiện như thế nào. Tôi chỉ không muốn tối ưu hóa sớm. Nếu một phần quan trọng làm cho mã của tôi dễ đọc hơn, tôi nên bắt đầu ở đó. Ví dụ trong bài viết MSDN của việc sử dụng Interlock *() hoạt động, nhưng nó gây nhầm lẫn. Tôi sẽ đi kiểm tra các liên kết của bạn ngay bây giờ. – Troy

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