Câu lệnh khóa đắt như thế nào?


111

Tôi đã thử nghiệm với xử lý đa luồng và song song và tôi cần một bộ đếm để thực hiện một số phân tích thống kê và đếm cơ bản về tốc độ xử lý. Để tránh các vấn đề với việc sử dụng đồng thời lớp của tôi, tôi đã sử dụng câu lệnh khóa trên một biến riêng trong lớp của mình:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Nhưng tôi đã tự hỏi ... làm thế nào đắt là khóa một biến? Những tác động tiêu cực đến hiệu suất là gì?


10
Khóa biến không quá đắt; đó là sự chờ đợi trên một biến bị khóa mà bạn muốn tránh.
Gabe

53
đó là ít hơn rất nhiều tốn kém hơn chi tiêu giờ trên việc theo dõi một tình trạng chủng tộc ;-)
BrokenGlass

2
Chà ... nếu một ổ khóa đắt tiền, bạn có thể muốn tránh chúng bằng cách thay đổi chương trình để nó cần ít ổ khóa hơn. Tôi có thể thực hiện một số loại đồng bộ hóa.
Kees C. Bakker

1
Tôi đã có một sự cải thiện đáng kể về hiệu suất (ngay bây giờ, sau khi đọc bình luận của @Gabe) chỉ bằng cách di chuyển nhiều mã ra khỏi các khối khóa của tôi. Tóm lại: từ bây giờ tôi sẽ chỉ để lại quyền truy cập biến (thường là một dòng) bên trong một khối khóa, kiểu "khóa đúng lúc". Nó có ý nghĩa không?
heltonbiker

2
@heltonbiker Tất nhiên là có lý. Đó cũng là nguyên tắc kiến ​​trúc, bạn phải làm cho ổ khóa càng ngắn, đơn giản và nhanh chóng càng tốt. Chỉ những dữ liệu thực sự cần thiết mới được đồng bộ. Trên các hộp máy chủ, bạn cũng nên xem xét tính chất kết hợp của khóa. Sự tranh cãi ngay cả khi không quan trọng đối với mã của bạn là nhờ tính chất hỗn hợp của khóa khiến các lõi quay trong mỗi lần truy cập nếu khóa được giữ bởi người khác. Bạn đang sử dụng hiệu quả một số tài nguyên cpu từ các dịch vụ khác trên máy chủ trong một thời gian trước khi luồng của bạn bị tạm ngưng.
ipavlu

Câu trả lời:


86

Đây là một bài báo đi vào chi phí. Câu trả lời ngắn gọn là 50ns.


39
Câu trả lời ngắn gọn hơn: 50ns + thời gian chờ nếu luồng khác đang giữ khóa.
Herman

4
Càng nhiều luồng vào và ra khỏi khóa, nó càng đắt hơn. Chi phí mở rộng theo cấp số nhân với số lượng đề
Arsen Zahray

16
Một số ngữ cảnh: chia hai số trên 3Ghz x86 mất khoảng 10ns (không bao gồm thời gian tìm nạp / giải mã lệnh) ; và tải một biến đơn từ bộ nhớ (không được lưu trong bộ nhớ đệm) vào một thanh ghi mất khoảng 40ns. Vì vậy, 50ns là điên cuồng, blindingly nhanh - bạn không nên lo lắng về chi phí của việc sử dụng lockbất kỳ hơn bạn muốn lo lắng về chi phí của việc sử dụng một biến.
BlueRaja - Danny Pflughoeft

3
Ngoài ra, bài báo đó đã cũ khi câu hỏi này được hỏi.
Otis

3
Số liệu thực sự tuyệt vời, "gần như không có chi phí", chưa kể là không chính xác. Các bạn đừng cân nhắc, rằng nó chỉ ngắn gọn và nhanh chóng và DUY NHẤT nếu không có tranh chấp gì cả, một chủ đề. TRONG TRƯỜNG HỢP ĐÓ, bạn KHÔNG CẦN KHÓA MỌI. Vấn đề thứ hai, khóa không phải là khóa, mà là khóa hỗn hợp, nó phát hiện bên trong CLR rằng khóa không được giữ bởi bất kỳ ai dựa trên các hoạt động nguyên tử và trong trường hợp đó, nó tránh các cuộc gọi đến lõi hệ điều hành, đó là vòng khác không được đo bằng các các bài kiểm tra. Những gì được đo như 25ns đến 50ns thực sự là mức ứng dụng đan cài hướng dẫn mã nếu khóa không lấy
ipavlu

50

Câu trả lời kỹ thuật là không thể định lượng được điều này, nó phụ thuộc nhiều vào trạng thái của bộ đệm ghi ngược bộ nhớ CPU và lượng dữ liệu mà trình cài đặt sẵn thu thập được phải được loại bỏ và đọc lại. Cả hai đều rất không xác định. Tôi sử dụng 150 chu kỳ CPU như một phép xấp xỉ phía sau để tránh những thất vọng lớn.

Câu trả lời thực tế là nó là waaaay rẻ hơn so với số lượng thời gian bạn sẽ ghi vào gỡ lỗi mã của bạn khi bạn nghĩ rằng bạn có thể bỏ qua một khóa.

Để có được một con số khó, bạn sẽ phải đo lường. Visual Studio có một trình phân tích đồng thời mượt mà có sẵn dưới dạng tiện ích mở rộng.


1
Thực tế là không, nó có thể được định lượng và đo lường. Nó không dễ dàng như viết những ổ khóa đó xung quanh mã, sau đó nói rằng tất cả chỉ là 50ns, một huyền thoại được đo lường trên quyền truy cập một luồng vào ổ khóa.
ipavlu

8
"nghĩ rằng bạn có thể bỏ qua một khóa" ... Tôi nghĩ rằng, nơi có rất nhiều người đang ở khi họ đọc câu hỏi này ...
Snoop

30

Đọc thêm:

Tôi muốn trình bày một số bài báo của tôi, quan tâm đến các nguyên thủy đồng bộ hóa chung và họ đang đào sâu vào hành vi, thuộc tính và chi phí của câu lệnh khóa Monitor, C # tùy thuộc vào các kịch bản và số lượng luồng riêng biệt. Nó đặc biệt quan tâm đến thời gian lãng phí CPU và thông lượng để hiểu mức độ công việc có thể được đẩy qua trong nhiều trường hợp:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Câu trả lời ban đầu:

Ôi chao!

Có vẻ như câu trả lời đúng được gắn cờ ở đây là CÂU TRẢ LỜI vốn dĩ không chính xác! Em xin tác giả câu trả lời, kính mong anh chị đọc bài đã liên kết đến cuối. bài báo

Tác giả của bài báo từ năm 2003 bài viết được đo trên chỉ máy Dual Core và trong trường hợp đo đầu tiên, ông đo khóa với chỉ một chủ đề duy nhất và kết quả là khoảng 50ns mỗi truy cập khóa.

Nó không nói gì về một khóa trong môi trường đồng thời. Vì vậy, chúng ta phải tiếp tục đọc bài báo và trong nửa sau, tác giả đã đo lường kịch bản khóa với hai và ba luồng, tiến gần hơn đến mức đồng thời của các bộ xử lý ngày nay.

Vì vậy, tác giả nói rằng với hai luồng trên Dual Core, các ổ khóa có giá 120ns, và với 3 luồng, nó lên đến 180ns. Vì vậy, nó dường như phụ thuộc rõ ràng vào số lượng luồng truy cập đồng thời vào khóa.

Vì vậy, nó rất đơn giản, nó không phải là 50 ns trừ khi nó là một sợi duy nhất, nơi ổ khóa trở nên vô dụng.

Một vấn đề khác cần xem xét là nó được đo bằng thời gian trung bình !

Nếu thời gian lặp lại được đo, thậm chí sẽ có thời gian từ 1 mili giây đến 20 mili giây, đơn giản vì phần lớn tốc độ nhanh, nhưng một số luồng sẽ đợi thời gian của bộ xử lý và gây ra sự chậm trễ thậm chí là mili giây.

Đây là tin xấu cho bất kỳ loại ứng dụng nào yêu cầu thông lượng cao, độ trễ thấp.

Và vấn đề cuối cùng cần xem xét là có thể có các hoạt động chậm hơn bên trong khóa và rất thường xuyên xảy ra trường hợp đó. Khối mã được thực thi bên trong khóa càng lâu thì sự tranh chấp càng cao và sự chậm trễ sẽ tăng cao.

Vui lòng xem xét, đã hơn một thập kỷ trôi qua kể từ năm 2003, có rất ít thế hệ bộ vi xử lý được thiết kế đặc biệt để chạy hoàn toàn đồng thời và việc khóa có hại đáng kể đến hiệu suất của chúng.


1
Để làm rõ, bài báo không nói rằng hiệu suất khóa suy giảm theo số luồng trong ứng dụng; hiệu suất suy giảm với số lượng luồng cạnh tranh trên khóa. (Đó là ngụ ý, nhưng không nêu rõ, trong câu trả lời ở trên.)
Gooseberry

Tôi cho rằng ý của bạn là: "Vì vậy, nó có vẻ như phụ thuộc rõ ràng vào số lượng các chủ đề được truy cập đồng thời và càng nhiều thì càng tệ." Vâng, từ ngữ có thể tốt hơn. Ý tôi là "được truy cập đồng thời" là các luồng đồng thời truy cập vào khóa, do đó tạo ra sự tranh cãi.
ipavlu

20

Điều này không trả lời truy vấn của bạn về hiệu suất, nhưng tôi có thể nói rằng .NET Framework cung cấp một Interlocked.Addphương pháp cho phép bạn thêm của bạn amountvào donethành viên của mình mà không cần khóa thủ công trên đối tượng khác.


1
Vâng, đây có lẽ là câu trả lời tốt nhất. Nhưng chủ yếu là vì lý do mã ngắn hơn và sạch hơn. Sự khác biệt về tốc độ có thể không đáng chú ý.
Henk Holterman

cảm ơn vì câu trả lời này. Tôi đang làm nhiều thứ hơn với ổ khóa. Int được thêm vào là một trong nhiều. Yêu thích gợi ý, sẽ sử dụng nó từ bây giờ.
Kees C. Bakker

ổ khóa dễ lấy đúng hơn rất nhiều, ngay cả khi mã không khóa có khả năng nhanh hơn. Interlocked.Add có các vấn đề tương tự như + = không đồng bộ hóa.
nhà chứa máy bay

10

lock (Monitor.Enter / Exit) rất rẻ, rẻ hơn các lựa chọn thay thế như Waithandle hoặc Mutex.

Nhưng nếu nó chậm (một chút), bạn muốn có một chương trình nhanh với kết quả không chính xác thì sao?


5
Haha ... Tôi đã tham gia chương trình nhanh chóng và kết quả tốt.
Kees C. Bakker

@ henk-holterman Có nhiều vấn đề với phát biểu của bạn: Đầu tiên là câu hỏi và câu trả lời này đã chỉ ra rõ ràng, có rất ít hiểu biết về tác động của khóa đối với hiệu suất tổng thể, thậm chí mọi người còn nói rằng huyền thoại về 50ns chỉ áp dụng được với môi trường đơn luồng. Thứ hai, tuyên bố của bạn ở đây và sẽ tồn tại trong nhiều năm và theo thời gian, các bộ xử lý được phát triển trong các lõi, nhưng tốc độ của các lõi không quá nhiều. ** Các ứng dụng Thrid ** chỉ trở nên phức tạp hơn theo thời gian, và sau đó nó được xếp theo lớp của khóa trong môi trường nhiều lõi và số lượng đang tăng lên, 2,4,8,10,20,16,32
ipavlu

Cách tiếp cận thông thường của tôi là xây dựng đồng bộ hóa theo cách kết hợp lỏng lẻo với càng ít tương tác càng tốt. Điều đó diễn ra rất nhanh đối với các cấu trúc dữ liệu không có khóa. Tôi đã tạo cho các trình bao bọc mã của mình xung quanh spinlock để đơn giản hóa việc phát triển và ngay cả khi TPL có các bộ sưu tập đồng thời đặc biệt, tôi đã phát triển các bộ sưu tập bị khóa spin của riêng mình xung quanh danh sách, mảng, từ điển và hàng đợi, vì tôi cần ít kiểm soát hơn và đôi khi một số mã chạy dưới khóa quay. Tôi có thể nói với bạn, nó có thể và cho phép giải quyết nhiều tình huống mà bộ sưu tập TPL không thể làm và với hiệu suất / thông lượng đạt được tuyệt vời.
ipavlu

7

Chi phí cho một khóa trong một vòng lặp chặt chẽ, so với một giải pháp thay thế không có khóa, là rất lớn. Bạn có thể đủ khả năng lặp lại nhiều lần mà vẫn hiệu quả hơn khóa. Đó là lý do tại sao khóa hàng đợi miễn phí lại rất hiệu quả.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Đầu ra:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208

4
Đây có thể là một ví dụ xấu vì vòng lặp của bạn thực sự không làm được gì, ngoài một phép gán biến duy nhất và một khóa là ít nhất 2 lệnh gọi hàm. Ngoài ra, 20ns cho mỗi khóa bạn nhận được không phải là quá tệ.
Zar Shardan

5

Có một số cách khác nhau để xác định "chi phí". Có chi phí thực tế của việc lấy và giải phóng khóa; như Jake viết, điều đó không đáng kể trừ khi thao tác này được thực hiện hàng triệu lần.

Mức độ liên quan hơn là ảnh hưởng của điều này đối với luồng thực thi. Mã này chỉ có thể được nhập bởi một chủ đề tại một thời điểm. Nếu bạn có 5 luồng thực hiện thao tác này thường xuyên, thì 4 trong số đó sẽ kết thúc chờ khóa được giải phóng và sau đó là chuỗi đầu tiên được lên lịch để nhập đoạn mã đó sau khi khóa đó được giải phóng. Vì vậy, thuật toán của bạn sẽ bị ảnh hưởng đáng kể. Mức độ phụ thuộc vào thuật toán và tần suất hoạt động được gọi như thế nào .. Bạn thực sự không thể tránh nó mà không đưa ra các điều kiện chủng tộc, nhưng bạn có thể cải thiện nó bằng cách giảm thiểu số lần gọi đến mã bị khóa.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.