When should & shouldn't I use this C# utility class to control threads via Interlocked

妖精的绣舞 提交于 2020-01-13 16:29:10

问题


I'm trying to understand the logic behind how this class was written, and when I should and shouldn't use it. Any insight would be appreciated

internal struct SpinLock
{
    private volatile int lockHeld;

    private readonly static int processorCount;

    public bool IsHeld
    {
        get
        {
            return this.lockHeld != 0;
        }
    }

    static SpinLock()
    {
        SpinLock.processorCount = Environment.ProcessorCount;
    }

    public void Enter()
    {
        if (Interlocked.CompareExchange(ref this.lockHeld, 1, 0) != 0)
        {
            this.EnterSpin();
        }
    }

    private void EnterSpin()
    {
        int num = 0;
        while (this.lockHeld != null || Interlocked.CompareExchange(ref this.lockHeld, 1, 0) != 0)
        {
            if (num >= 20 || SpinLock.processorCount <= 1)
            {
                if (num >= 25)
                {
                    Thread.Sleep(1);
                }
                else
                {
                    Thread.Sleep(0);
                }
            }
            else
            {
                Thread.SpinWait(100);
            }
            num++;
        }
    }

    public void Exit()
    {
        this.lockHeld = 0;
    }
}

Update: I found a sample usage in my source code... This indicates how to use the above object, though I don't understand "why"

    internal class FastReaderWriterLock
    {
        private SpinLock myLock;

        private uint numReadWaiters;

        private uint numWriteWaiters;

        private int owners;

        private EventWaitHandle readEvent;

        private EventWaitHandle writeEvent;

        public FastReaderWriterLock()
        {
        }

        public void AcquireReaderLock(int millisecondsTimeout)
        {
            this.myLock.Enter();
            while (this.owners < 0 || this.numWriteWaiters != 0)
            {
                if (this.readEvent != null)
                {
                    this.WaitOnEvent(this.readEvent, ref this.numReadWaiters, millisecondsTimeout);
                }
                else
                {
                    this.LazyCreateEvent(ref this.readEvent, false);
                }
            }
            FastReaderWriterLock fastReaderWriterLock = this;
            fastReaderWriterLock.owners = fastReaderWriterLock.owners + 1;
            this.myLock.Exit();
        }

private void WaitOnEvent(EventWaitHandle waitEvent, ref uint numWaiters, int millisecondsTimeout)
{
    waitEvent.Reset();
    uint& numPointer = numWaiters;
    bool flag = false;
    this.myLock.Exit();
    try
    {
        if (waitEvent.WaitOne(millisecondsTimeout, false))
        {
            flag = true;
        }
        else
        {
            throw new TimeoutException("ReaderWriterLock timeout expired");
        }
    }
    finally
    {
        this.myLock.Enter();
        uint& numPointer1 = numWaiters;
        if (!flag)
        {
            this.myLock.Exit();
        }
    }
}
    }

回答1:


SpinLocks in general are a form of lock that keeps waiting threads awake (cycling over and over on a check condition in a tight loop- think "Mommy are we there yet?") rather than relying on heavier, slower, kernel mode signals. They are usually intended for situations where the expected wait time is very short, where they outperform the overhead of creating and waiting on an OS handle for a traditional lock. They incur more CPU cost than a traditional lock though, so for more than a very short wait time a traditional lock (like the Monitor class or the various WaitHandle implementations) is preferred.

This short wait time concept is demonstrated in your code above:

waitEvent.Reset();
// All that we are doing here is setting some variables.  
// It has to be atomic, but it's going to be *really* fast
uint& numPointer = numWaiters;
bool flag = false;
// And we are done.  No need for an OS wait handle for 2 lines of code.
this.myLock.Exit();

There is a perfectly good SpinLock built into the BCL, however it is only in v4.0+, so if you are working in an older version of the .NET framework or on code that was migrated from an older version, someone may have written their own implementation.

To answer your question: You should use the built-in SpinLock if you are writing new code on .NET 4.0 or higher. For code on 3.5 or older, especially if you are extending Nesper, I'd argue that this implementation is time-tested and appropriate. Only use a SpinLock where you know that the time a thread may wait on it is very small, as in the example above.

EDIT: Looks like your implementation came from Nesper- the .NET port of the Esper CEP library:

https://svn.codehaus.org/esper/esper/tagsnet/release_1.12.0_beta_1/trunk/NEsper/compat/SpinLock.cs

and

https://svn.codehaus.org/esper/esper/tagsnet/release_1.12.0_beta_1/trunk/NEsper/compat/FastReaderWriterLock.cs

I can confirm that Nesper existed long before the .NET framework 4, so that explains the need for a home-spun SpinLock.




回答2:


It appears that the original author wanted a faster version of ReaderWriterLock. This old class was painfully slow. My own tests (which I did a long time ago) indicates that RWL had ~8x the overhead of a plain old lock. ReaderWriterLockSlim improved things quite a bit (though it still has ~2x the overhead as compared to lock). At this point I would say ditch the custom code and just use the newer ReaderWriterLockSlim class.

But, for what it is worth let me explain some of that custom SpinLock code.

  • Interlocked.CompareExchange is .NET's version of a CAS operation. It is the most fundamental synchronization primitive. You can literally build everything else from this single operation including your own custom Monitor-like class, reader writer locks, etc. Obviously it was used here to create a spin lock.
  • Thread.Sleep(0) yields to any thread of with the same or higher priority on any processor.
  • Thread.Sleep(1) yields to any thread on any processor.
  • Thread.SpinWait puts the thread into a tight loop for the specified number of iterations.

Although it was not used in the code you posted there is another useful mechanism for creating spin locks (or other low lock strategies).

  • Thread.Yield yields to any thread on the same processor.

Microsoft uses all of these calls in their highly concurrent synchronization mechanisms and collections. If you decompile SpinLock, SpinWait, ManualResetEventSlim, etc. you will see a fairly complex song-and-dance going on with these calls...much more complex than the code you posted.

Again, ditch the custom code and just use ReaderWriterLockSlim instead of that custom FastReaderWriterLock class.


By the way, this.lockHeld != null should produce a compiler warning since lockHeld is a value type.



来源:https://stackoverflow.com/questions/10509112/when-should-shouldnt-i-use-this-c-sharp-utility-class-to-control-threads-via

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!