Các triển khai "không khóa" hiện tại hầu hết đều tuân theo cùng một mẫu:
- * đọc một số tiểu bang và tạo một bản sao của nó **
- * sửa đổi bản sao **
- thực hiện một hoạt động đan xen
- thử lại nếu nó không thành công
(* tùy chọn: phụ thuộc vào cấu trúc dữ liệu / thuật toán)
Bit cuối cùng tương tự như một spinlock. Trên thực tế, nó là một spinlock cơ bản . :)
Tôi đồng ý với @nobugz về điều này: chi phí của các hoạt động được lồng vào nhau được sử dụng trong đa luồng không khóa bị chi phối bởi các tác vụ bộ nhớ đệm và đồng bộ nhớ mà nó phải thực hiện .
Tuy nhiên, những gì bạn đạt được với cấu trúc dữ liệu "không có khóa" là "ổ khóa" của bạn rất chi tiết . Điều này làm giảm khả năng hai luồng đồng thời truy cập cùng một "khóa" (vị trí bộ nhớ).
Thủ thuật thường gặp là bạn không có các khóa chuyên dụng - thay vào đó bạn coi tất cả các phần tử trong một mảng hoặc tất cả các nút trong danh sách được liên kết như một "khóa quay". Bạn đọc, sửa đổi và cố gắng cập nhật nếu không có bản cập nhật nào kể từ lần đọc cuối cùng của bạn. Nếu có, bạn thử lại.
Điều này làm cho "khóa" của bạn (ồ, xin lỗi, không khóa :) rất chi tiết, mà không giới thiệu thêm bộ nhớ hoặc yêu cầu tài nguyên.
Làm cho nó trở nên chi tiết hơn sẽ làm giảm xác suất đợi. Làm cho nó càng chi tiết càng tốt mà không cần thêm yêu cầu tài nguyên nghe có vẻ tuyệt vời, phải không?
Tuy nhiên, hầu hết niềm vui có thể đến từ việc đảm bảo xếp hàng đúng tải / cửa hàng .
Trái ngược với trực giác của mọi người, CPU có thể tự do sắp xếp lại các lần đọc / ghi của bộ nhớ - nhân tiện, chúng rất thông minh: bạn sẽ khó quan sát điều này từ một luồng duy nhất. Tuy nhiên, bạn sẽ gặp phải sự cố khi bắt đầu thực hiện đa luồng trên nhiều lõi. Trực giác của bạn sẽ bị phá vỡ: chỉ vì một chỉ dẫn sớm hơn trong mã của bạn, điều đó không có nghĩa là nó thực sự sẽ xảy ra sớm hơn. CPU có thể xử lý các lệnh không theo thứ tự: và chúng đặc biệt thích làm điều này với các lệnh có quyền truy cập bộ nhớ, để ẩn độ trễ của bộ nhớ chính và sử dụng tốt hơn bộ nhớ cache của chúng.
Bây giờ, chắc chắn chống lại trực giác rằng một chuỗi mã không chảy "từ trên xuống", thay vào đó nó chạy như thể không có trình tự nào - và có thể được gọi là "sân chơi của ma quỷ". Tôi tin rằng không thể đưa ra câu trả lời chính xác về việc tải / lưu trữ lại quy trình sẽ diễn ra. Thay vào đó, người ta luôn nói về mays và mights và lon và chuẩn bị cho điều tồi tệ nhất. "Ồ, CPU có thể sắp xếp lại thứ tự đọc này trước khi ghi, vì vậy tốt nhất là đặt một rào cản bộ nhớ ngay tại đây, tại chỗ này."
Những vấn đề khá phức tạp bởi thực tế là ngay cả những mays và mights có thể khác nhau giữa các kiến trúc CPU. Chẳng hạn, có thể xảy ra trường hợp điều gì đó được đảm bảo không xảy ra trong một kiến trúc có thể xảy ra trên một kiến trúc khác.
Để có được quyền đa luồng "không có khóa", bạn phải hiểu các mô hình bộ nhớ.
Tuy nhiên, việc bắt được mô hình bộ nhớ và các đảm bảo chính xác không phải là chuyện nhỏ, như đã được minh chứng trong câu chuyện này, theo đó Intel và AMD đã thực hiện một số chỉnh sửa đối với tài liệu MFENCE
gây ra một số xôn xao trong giới phát triển JVM . Hóa ra, tài liệu mà các nhà phát triển dựa vào ngay từ đầu đã không chính xác như vậy.
Các ổ khóa trong .NET dẫn đến một rào cản bộ nhớ ngầm, vì vậy bạn có thể an toàn khi sử dụng chúng (hầu hết thời gian, nghĩa là ... hãy xem ví dụ này Joe Duffy - Brad Abrams - Vance Morrison sự tuyệt vời về khởi tạo lười biếng, khóa, chất bay hơi và bộ nhớ rào cản. :) (Hãy chắc chắn theo các liên kết trên trang đó.)
Như một phần thưởng bổ sung, bạn sẽ được giới thiệu với mô hình bộ nhớ .NET trong một nhiệm vụ phụ . :)
Ngoài ra còn có một "oldie but goldie" từ Vance Morrison: Những gì mọi nhà phát triển phải biết về ứng dụng đa luồng .
... và tất nhiên, như @Eric đã đề cập, Joe Duffy là một người đọc rõ ràng về chủ đề này.
Một STM tốt có thể đạt được gần đến mức khóa chi tiết và có thể sẽ cung cấp hiệu suất gần bằng hoặc ngang bằng với việc triển khai thủ công. Một trong số đó là STM.NET từ các dự án DevLabs của MS.
Nếu bạn không phải là người đam mê .NET, Doug Lea đã thực hiện một số công việc tuyệt vời trong JSR-166 .
Cliff Click có một điểm thú vị đối với các bảng băm không dựa vào dải khóa - như các bảng băm đồng thời của Java và .NET - và dường như có thể mở rộng quy mô thành 750 CPU.
Nếu bạn không ngại dấn thân vào lãnh thổ Linux, bài viết dưới đây sẽ cung cấp thêm thông tin chi tiết về nội hàm của kiến trúc bộ nhớ hiện tại và cách chia sẻ dòng bộ nhớ đệm có thể phá hủy hiệu suất: Điều mà mọi lập trình viên nên biết về bộ nhớ .
@Ben đưa ra nhiều nhận xét về Bộ KH & ĐT: Tôi chân thành đồng ý rằng Bộ KH & ĐT có thể tỏa sáng trong một số lĩnh vực. Một giải pháp dựa trên MPI có thể dễ lập luận hơn, dễ thực hiện hơn và ít bị lỗi hơn so với việc triển khai khóa nửa chừng cố gắng trở nên thông minh. (Tuy nhiên - về mặt chủ quan - cũng đúng đối với giải pháp dựa trên STM.) Tôi cũng dám cá rằng việc viết chính xác một ứng dụng phân tán tốt trong ví dụ Erlang sẽ dễ dàng hơn nhiều năm như nhiều ví dụ thành công cho thấy.
Tuy nhiên, MPI có những chi phí riêng và những rắc rối riêng khi nó được chạy trên một hệ thống đa lõi, đơn . Ví dụ: ở Erlang, có những vấn đề cần giải quyết xung quanh việc đồng bộ hóa lịch trình quy trình và hàng đợi tin nhắn .
Ngoài ra, về cốt lõi, các hệ thống MPI thường triển khai loại lập lịch N: M hợp tác cho "các quy trình nhẹ". Ví dụ, điều này có nghĩa là không thể tránh khỏi sự chuyển đổi ngữ cảnh giữa các quy trình nhẹ. Đúng là nó không phải là một "công tắc ngữ cảnh cổ điển" mà chủ yếu là một hoạt động không gian của người dùng và nó có thể được thực hiện nhanh chóng - tuy nhiên tôi thực sự nghi ngờ rằng nó có thể được thực hiện theo chu kỳ 20-200 một hoạt động được khóa liên tục . Chuyển đổi ngữ cảnh chế độ người dùng chắc chắn chậm hơnngay cả trong thư viện Intel McRT. Việc lập lịch trình N: M không phải là mới. Các LWP đã có ở Solaris trong một thời gian dài. Họ đã bị bỏ rơi. Đã có sợi trong NT. Bây giờ chúng hầu hết là một di tích. Đã có "kích hoạt" trong NetBSD. Họ đã bị bỏ rơi. Linux có chủ đề riêng về luồng N: M. Bây giờ nó dường như đã chết.
Đôi khi, có những đối thủ mới: ví dụ như McRT của Intel , hoặc gần đây nhất là Lập lịch chế độ người dùng cùng với ConCRT của Microsoft.
Ở cấp thấp nhất, họ làm những gì một bộ lập lịch N: M MPI làm. Erlang - hoặc bất kỳ hệ thống MPI nào -, có thể được hưởng lợi rất nhiều trên các hệ thống SMP bằng cách khai thác UMS mới .
Tôi đoán câu hỏi của OP không phải về giá trị của và lập luận chủ quan cho / chống lại bất kỳ giải pháp nào, nhưng nếu tôi phải trả lời câu hỏi đó, tôi đoán nó phụ thuộc vào nhiệm vụ: để xây dựng cấu trúc dữ liệu cơ bản cấp thấp, hiệu suất cao chạy trên hệ thống đơn với nhiều lõi , kỹ thuật khóa thấp / "không khóa" hoặc một STM sẽ mang lại kết quả tốt nhất về hiệu suất và có thể sẽ đánh bại giải pháp MPI bất kỳ lúc nào về hiệu suất, ngay cả khi các nếp nhăn trên đã được ủi phẳng ví dụ như ở Erlang.
Đối với việc xây dựng bất cứ thứ gì phức tạp hơn vừa phải chạy trên một hệ thống, có lẽ tôi sẽ chọn khóa hạt thô cổ điển hoặc nếu hiệu suất là vấn đề đáng quan tâm, thì một STM.
Để xây dựng một hệ thống phân tán, một hệ thống MPI có thể sẽ là một lựa chọn tự nhiên.
Lưu ý rằng cũng có các triển khai MPI cho .NET (mặc dù chúng dường như không hoạt động).