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 ECX
và LOCK 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 EnterCriticalSection
và LeaveCriticalSection
, 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 InterlockExchange
và InterlockCompareExchange
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.
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." –
@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ì? –
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. –