Lock free multiple readers single writer

前端 未结 4 1990
既然无缘
既然无缘 2020-12-17 03:14

I have got an in memory data structure that is read by multiple threads and written by only one thread. Currently I am using a critical section to make this access threadsaf

相关标签:
4条回答
  • 2020-12-17 03:22

    Just an addition - what you are looking at here is generally known as Hazard Pointers. I have no idea if you can do something similar in Delphi.

    0 讨论(0)
  • 2020-12-17 03:27

    I don't know of any lock-free (or micro-locking as in your example above) MREW approach that could be implemented on Intel86 code.

    For small (fast-expiring) locks a spinning approach from the OmniThreadLibrary works fine:

    type
    TOmniMREW = record
    strict private
      omrewReference: integer;      //Reference.Bit0 is 'writing in progress' flag
    public
      procedure EnterReadLock; inline;
      procedure EnterWriteLock; inline;
      procedure ExitReadLock; inline;
      procedure ExitWriteLock; inline;
    end; { TOmniMREW }
    
    procedure TOmniMREW.EnterReadLock;
    var
      currentReference: integer;
    begin
      //Wait on writer to reset write flag so Reference.Bit0 must be 0 than increase Reference
      repeat
        currentReference := omrewReference AND NOT 1;
      until currentReference = InterlockedCompareExchange(omrewReference, currentReference + 2, currentReference);
    end; { TOmniMREW.EnterReadLock }
    
    procedure TOmniMREW.EnterWriteLock;
    var
      currentReference: integer;
    begin
      //Wait on writer to reset write flag so omrewReference.Bit0 must be 0 then set omrewReference.Bit0
      repeat
        currentReference := omrewReference AND NOT 1;
      until currentReference = InterlockedCompareExchange(omrewReference, currentReference + 1, currentReference);
      //Now wait on all readers
      repeat
      until omrewReference = 1;
    end; { TOmniMREW.EnterWriteLock }
    
    procedure TOmniMREW.ExitReadLock;
    begin
      //Decrease omrewReference
      InterlockedExchangeAdd(omrewReference, -2);
    end; { TOmniMREW.ExitReadLock }
    
    procedure TOmniMREW.ExitWriteLock;
    begin
      omrewReference := 0;
    end; { TOmniMREW.ExitWriteLock }
    

    I just noticed a possible alignment issue here - the code should check that omrewReference is 4-aligned. Will notify the author.

    0 讨论(0)
  • 2020-12-17 03:30

    I've got a potential solution for you; it lets new readers start anytime until the writer wishes to write. The writer then waits for the readers to finish and performs its write. After the writing is done the readers can read once more.

    Furthermore, this solution does not need locks or mutexs, but it does need an atomic test-and-set operation. I don't know Delphi and I wrote my solution in Lisp, so I'll try to describe it in pseudo code.

    (CAPS are function names, all these functions take and return no arguments)

    integer access-mode = 1; // start in reader mode. 
    
    WRITE  loop with current = accessmode, 
                with new = (current & 0xFFFFFFFe) 
                until test-and-set(access-mode, current to new)
           loop until access-mode = 0; 
    
    ENDWRITE assert( access-mode = 0)
             set access-mode to 1
    
    READ loop with current = ( accessmode | 1 ),
              with new = (current + 2),
              until test-and-set(access-mode, current to new)
    ENDREAD loop with current = accessmode
                 with new = (current - 2),
                 until test-and-set(access-mode, current to new)
    

    To use, a reader calls READ before reading and ENDREAD when done. The lone writer calls WRITE before writing and ENDWRITE when done.

    The idea is an integer called access-mode holds a boolean in the lowest bit, and a count in the higher bits. WRITE sets the bit to 0, and then spins until the enough ENDREADs count down access mode to zero. Endwrite sets access-mode back to 1. READ ORs the current access-mode with 1, so their test-and-set will only ever pass if the low-bit was high to begin with. I add and subtract by 2 to leave the low-bit alone.

    To get a count of readers just take access-mode right shifted by one.

    0 讨论(0)
  • 2020-12-17 03:48

    It's been a while since I got my hands dirty in Delphi, so verify this before using, but... from memory, you can get reference-counted behaviour if you use an interface and an implementation using TInterfacedObject.

    type
        IDataClass = interface
            function GetSome: integer;
            function GetData: double;
    
            property Some: integer read GetSome;
            property Data: double read GetData;
        end;
    
        TDataClass = class(TInterfacedObject, IDataClass)
        private
            FSome: integer;
            FData: double;
        protected
            function GetSome: integer;
            function GetData: double;
        public
            constructor Create(ASome: integer; AData: double);
        end;
    

    Then you make all your variables of type ISomeData instead (mixing ISomeData and TSomeData is a very bad idea... you easily get reference-counting problems).

    Basically this would cause the reference count to increment automatically in your reader code where it loads the local reference to the data, and it gets decremented when the variable leaves scope, at which point it would de-allocate there.

    I know it's a bit tedious to duplicate the API of your data class in an interface and a class implementation, but it is the easiest way to get your desired behaviour.

    0 讨论(0)
提交回复
热议问题