Khi nào thì ReaderWriterLockSlim tốt hơn một khóa đơn giản?


80

Tôi đang thực hiện một điểm chuẩn rất ngớ ngẩn trên ReaderWriterLock với mã này, nơi đọc xảy ra thường xuyên hơn gấp 4 lần so với ghi:

class Program
{
    static void Main()
    {
        ISynchro[] test = { new Locked(), new RWLocked() };

        Stopwatch sw = new Stopwatch();

        foreach ( var isynchro in test )
        {
            sw.Reset();
            sw.Start();
            Thread w1 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w1.Start( isynchro );

            Thread w2 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w2.Start( isynchro );

            Thread r1 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r1.Start( isynchro );

            Thread r2 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r2.Start( isynchro );

            w1.Join();
            w2.Join();
            r1.Join();
            r2.Join();
            sw.Stop();

            Console.WriteLine( isynchro.ToString() + ": " + sw.ElapsedMilliseconds.ToString() + "ms." );
        }

        Console.WriteLine( "End" );
        Console.ReadKey( true );
    }

    static void ReadThread(Object o)
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 500; i++ )
        {
            Int32? value = synchro.Get( i );
            Thread.Sleep( 50 );
        }
    }

    static void WriteThread( Object o )
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 125; i++ )
        {
            synchro.Add( i );
            Thread.Sleep( 200 );
        }
    }

}

interface ISynchro
{
    void Add( Int32 value );
    Int32? Get( Int32 index );
}

class Locked:List<Int32>, ISynchro
{
    readonly Object locker = new object();

    #region ISynchro Members

    public new void Add( int value )
    {
        lock ( locker ) 
            base.Add( value );
    }

    public int? Get( int index )
    {
        lock ( locker )
        {
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
    }

    #endregion
    public override string ToString()
    {
        return "Locked";
    }
}

class RWLocked : List<Int32>, ISynchro
{
    ReaderWriterLockSlim locker = new ReaderWriterLockSlim();

    #region ISynchro Members

    public new void Add( int value )
    {
        try
        {
            locker.EnterWriteLock();
            base.Add( value );
        }
        finally
        {
            locker.ExitWriteLock();
        }
    }

    public int? Get( int index )
    {
        try
        {
            locker.EnterReadLock();
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
        finally
        {
            locker.ExitReadLock();
        }
    }

    #endregion

    public override string ToString()
    {
        return "RW Locked";
    }
}

Nhưng tôi hiểu rằng cả hai đều hoạt động theo cách ít nhiều giống nhau:

Locked: 25003ms.
RW Locked: 25002ms.
End

Ngay cả khi làm cho việc đọc thường xuyên hơn 20 lần so với viết, hiệu suất vẫn (gần như) như nhau.

Tôi đang làm gì đó sai ở đây?

Trân trọng.


3
btw, nếu tôi tắt chế độ ngủ: "Đã khóa: 89ms. RW đã khóa: 32ms." - hoặc tăng các số: "Đã khóa: 1871ms. RW đã khóa: 2506ms.".
Marc Gravell

1
Nếu tôi loại bỏ các ngủ, khóa được nhanh hơn rwlocked
vtortola

1
Đó là vì mã bạn đang đồng bộ hóa quá nhanh để tạo ra bất kỳ sự tranh chấp nào. Xem câu trả lời của Hans và chỉnh sửa của tôi.
Dan Tao

Câu trả lời:


107

Trong ví dụ của bạn, giấc ngủ có nghĩa là nói chung không có tranh chấp. Một ổ khóa không theo ý muốn là rất nhanh. Đối với vấn đề này, bạn sẽ cần một khóa cạnh tranh ; nếu có các bài viết trong cuộc tranh cãi đó, chúng sẽ giống nhau ( lockthậm chí có thể nhanh hơn) - nhưng nếu nó chủ yếu là đọc (với một cuộc tranh luận viết hiếm khi), tôi hy vọng ReaderWriterLockSlimkhóa sẽ hoạt động tốt hơn lock.

Cá nhân, tôi thích một chiến lược khác ở đây, sử dụng tài liệu tham khảo-trao đổi - vì vậy đọc luôn có thể đọc mà không bao giờ kiểm tra / khóa / vv viết thực hiện thay đổi của họ cho một nhân bản copy, sau đó sử dụng Interlocked.CompareExchangeđể trao đổi các tài liệu tham khảo (tái áp dụng sự thay đổi của họ nếu thread khác đã thay đổi tham chiếu trong thời gian tạm thời).


1
Copy-and-swap-on-write chắc chắn là cách để thực hiện ở đây.
LBushkin

24
Copy-and-swap-on-write không khóa hoàn toàn tránh tranh chấp hoặc khóa tài nguyên đối với người đọc; nó nhanh chóng đối với người viết khi không có tranh luận, nhưng có thể trở nên kẹt cứng khi có tranh chấp. Có thể tốt nếu người viết có được một khóa trước khi tạo bản sao (độc giả sẽ không quan tâm đến khóa) và nhả khóa sau khi thực hiện hoán đổi. Mặt khác, nếu 100 luồng mỗi lần thử cập nhật đồng thời, trong trường hợp xấu nhất, chúng có thể phải thực hiện trung bình khoảng 50 thao tác sao chép-cập nhật-thử-hoán đổi mỗi lần (tổng cộng 5.000 thao tác sao chép-cập nhật-thử-hoán đổi để thực hiện 100 bản cập nhật).
supercat

1
dưới đây là cách tôi nghĩ rằng nó sẽ giống, xin vui lòng cho tôi biết nếu sai:bool bDone=false; while(!bDone) { object origObj = theObj; object newObj = origObj.DeepCopy(); // then make changes to newObj if(Interlocked.CompareExchange(ref theObj, tempObj, newObj)==origObj) bDone=true; }
mcmillab

1
@mcmillab về cơ bản, có; trong một số trường hợp đơn giản (thường là khi gói một giá trị duy nhất hoặc ở đâu cũng không thành vấn đề nếu sau này viết thừa những thứ mà họ chưa từng thấy), bạn thậm chí có thể thoát khỏi việc mất Interlockedvà chỉ sử dụng một volatiletrường và chỉ định cho nó - nhưng nếu bạn cần chắc chắn rằng thay đổi của bạn không bị mất hoàn toàn, thì đó Interlockedlà lý tưởng
Marc Gravell

3
@ jnm2: Các bộ sưu tập sử dụng CompareExchange rất nhiều, nhưng tôi không nghĩ rằng chúng sao chép nhiều. Khóa CAS + không nhất thiết là thừa, nếu người ta sử dụng nó để cho phép khả năng không phải tất cả người viết đều có được khóa. Ví dụ, một tài nguyên mà sự tranh cãi nói chung sẽ hiếm nhưng đôi khi có thể gay gắt, có thể có khóa và cờ cho biết liệu người viết có nên lấy nó hay không. Nếu lá cờ rõ ràng, người viết chỉ cần thực hiện CAS và nếu nó thành công, họ sẽ đi tiếp. Tuy nhiên, một người viết không thành công CAS nhiều hơn hai lần, có thể đặt cờ; lá cờ sau đó có thể vẫn được đặt ...
supercat

23

Các thử nghiệm của riêng tôi chỉ ra rằng ReaderWriterLockSlimcó khoảng 5x chi phí so với bình thường lock. Điều đó có nghĩa là để RWLS hoạt động tốt hơn một khóa cũ thông thường, các điều kiện sau thường sẽ xảy ra.

  • Số lượng độc giả đông hơn đáng kể so với người viết.
  • Khóa sẽ phải được giữ đủ lâu để vượt qua các chi phí bổ sung.

Trong hầu hết các ứng dụng thực tế, hai điều kiện này không đủ để vượt qua chi phí bổ sung đó. Cụ thể, trong mã của bạn, ổ khóa được giữ trong một khoảng thời gian ngắn đến mức chi phí của khóa có thể sẽ là yếu tố chi phối. Nếu bạn di chuyển những Thread.Sleepcuộc gọi đó vào bên trong khóa thì có thể bạn sẽ nhận được một kết quả khác.


17

Không có tranh chấp trong chương trình này. Các phương thức Get và Add thực thi trong vài nano giây. Tỷ lệ cược mà nhiều chủ đề trúng các phương pháp đó vào thời điểm chính xác là rất nhỏ.

Đặt một lệnh gọi Thread.Sleep (1) vào chúng và xóa giấc ngủ khỏi các chuỗi để xem sự khác biệt.


12

Chỉnh sửa 2 : Chỉ cần xóa các Thread.Sleepcuộc gọi khỏi ReadThreadWriteThread, tôi đã thấy Lockedhiệu quả tốt hơn RWLocked. Tôi tin rằng Hans đã đánh đinh vào đầu ở đây; phương pháp của bạn quá nhanh và không gây tranh cãi. Khi tôi đã thêm Thread.Sleep(1)vào GetAddphương pháp LockedRWLocked(và sử dụng 4 đọc chủ đề chống lại 1 ghi thread), RWLockedđánh bại quần tắt của Locked.


Chỉnh sửa : Được rồi, nếu tôi thực sự nghĩ khi lần đầu tiên đăng câu trả lời này, thì ít nhất tôi đã nhận ra lý do tại sao bạn đặt các Thread.Sleepcuộc gọi vào đó: bạn đang cố gắng tái tạo kịch bản đọc xảy ra thường xuyên hơn viết. Đây không phải là cách đúng đắn để làm điều đó. Thay vào đó, tôi sẽ giới thiệu thêm chi phí cho bạn AddGetcác phương pháp để tạo cơ hội tranh cãi lớn hơn (như Hans đã đề xuất ), tạo nhiều luồng đọc hơn là viết luồng (để đảm bảo đọc thường xuyên hơn ghi) và xóa các Thread.Sleeplệnh gọi từ ReadThreadWriteThread(thực sự là giảm tranh chấp, đạt được điều ngược lại với những gì bạn muốn).


Tôi thích những gì bạn đã làm cho đến nay. Nhưng đây là một số vấn đề tôi thấy ngay lập tức:

  1. Tại sao các Thread.Sleepcuộc gọi? Đây chỉ là tăng thời gian thực hiện của bạn lên một lượng không đổi, điều này sẽ làm cho kết quả hiệu suất hội tụ một cách giả tạo.
  2. Tôi cũng sẽ không bao gồm việc tạo các Threadđối tượng mới trong mã do bạn đo lường Stopwatch. Đó không phải là một đối tượng tầm thường để tạo ra.

Tôi không biết liệu bạn có thấy sự khác biệt đáng kể khi giải quyết hai vấn đề trên hay không. Nhưng tôi tin rằng chúng nên được giải quyết trước khi cuộc thảo luận tiếp tục.


+1: Điểm tốt ở # 2 - nó thực sự có thể làm sai lệch kết quả (sự khác biệt giống nhau giữa mỗi thời điểm, nhưng tỷ lệ khác nhau). Mặc dù nếu bạn chạy điều này đủ lâu, thời gian tạo các Threadđối tượng sẽ bắt đầu trở nên không đáng kể.
Nelson Rothermel

10

Bạn sẽ nhận được hiệu suất tốt hơn với ReaderWriterLockSlimmột khóa đơn giản nếu bạn khóa một phần mã cần thời gian lâu hơn để thực thi. Trong trường hợp này người đọc có thể làm việc song song. Để có được một ReaderWriterLockSlimcần nhiều thời gian hơn là nhập một đơn giản Monitor. Kiểm tra ReaderWriterLockTinyviệc triển khai của tôi để biết khóa người đọc-người viết thậm chí còn nhanh hơn câu lệnh khóa đơn giản và cung cấp chức năng người đọc-người viết: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/


Tôi thích điều này. Bài báo khiến tôi tự hỏi liệu Monitor có nhanh hơn ReaderWriterLockTiny trên mỗi nền tảng không?
jnm2

Mặc dù ReaderWriterLockTiny nhanh hơn ReaderWriterLockSlim (và cả Monitor), nhưng có một nhược điểm: nếu có nhiều người đọc mất khá nhiều thời gian, nó không bao giờ tạo cơ hội cho người viết (họ sẽ đợi gần như vô thời hạn). Mặt khác, ReaderWriterLockSlim, hãy quản lý cân bằng quyền truy cập vào nguồn chia sẻ lại cho người đọc / người viết.
tigrou


3

Các khóa không được kiểm tra sẽ mất theo thứ tự micro giây để có được, do đó, thời gian thực thi của bạn sẽ bị rút ngắn bởi các lệnh gọi của bạn Sleep.


3

Trừ khi bạn có phần cứng đa lõi (hoặc ít nhất là giống với môi trường sản xuất dự kiến ​​của bạn), bạn sẽ không nhận được một bài kiểm tra thực tế ở đây.

Một thử nghiệm hợp lý hơn sẽ là kéo dài tuổi thọ của các hoạt động bị khóa của bạn bằng cách đặt một khoảng thời gian ngắn bên trong khóa. Bằng cách đó, bạn sẽ thực sự có thể đối chiếu sự song song được thêm vào bằng cách sử dụng ReaderWriterLockSlimso với tuần tự hóa được ngụ ý bởi cơ bản lock().

Hiện tại, thời gian thực hiện bởi các hoạt động bị khóa của bạn bị mất trong tiếng ồn tạo ra bởi các cuộc gọi Ngủ xảy ra bên ngoài ổ khóa. Tổng thời gian trong cả hai trường hợp chủ yếu liên quan đến Ngủ.

Bạn có chắc ứng dụng trong thế giới thực của mình sẽ có số lần đọc và ghi bằng nhau không? ReaderWriterLockSlimthực sự tốt hơn cho trường hợp bạn có nhiều độc giả và người viết tương đối không thường xuyên. 1 luồng người viết so với 3 luồng người đọc sẽ thể hiện ReaderWriterLockSlimlợi ích tốt hơn, nhưng trong mọi trường hợp, thử nghiệm của bạn phải phù hợp với kiểu truy cập trong thế giới thực mà bạn mong đợi.


2

Tôi đoán điều này là do bạn ngủ quên trong chủ đề người đọc và người viết.
Chủ đề đã đọc của bạn có chế độ ngủ 500-500ms là 25000 Hầu hết thời gian nó ở chế độ ngủ


0

Khi nào thì ReaderWriterLockSlim tốt hơn một khóa đơn giản?

Khi bạn đọc nhiều hơn viết.

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.