简体   繁体   中英

Read and write same memory from different threads

I have a simple class which sends requests asynchronously.

public class MyClass
{
    private readonly ISender _sender;

    public MyClass(ISender sender)
    {
        _sender = sender;
    }

    public Task<string> SendAsync(string input, CancellationToken cancellationToken)
    {
        return _sender.SendAsync(input, cancellationToken);
    }
}

public interface ISender
{
    Task<string> SendAsync(string input, CancellationToken cancellationToken);
}

All looks simple, until the following requirement: _sender can be changed at runtime.

New implementation of MyClass:

public class MyClass
{
    private readonly ISender _sender;

    public MyClass(ISender sender)
    {
        _sender = sender;
    }

    public Task<string> SendAsync(string input, CancellationToken cancellationToken)
    {
        return _sender.SendAsync(input, cancellationToken);
    }

    public void SenderChanged(object unused, SenderEventArgs e)
    {
        ISender previous = Interlocked.Exchange(ref _sender, SenderFactory.Create(e.NewSenderConfig));
        previous.Dispose();
    }
}

Obviously this code is not thread-safe. I would need to introduce lock in both SendAsync and SenderChanged to make sure that _sender is always the up-to-date object. But I expect the SenderChanged to be called once per day let's say, and SendAsync (reading the _sender object) to be called 10000/second.
Lock and context switching would kill the performance of this code.

Is there a possible way to handle this with low level locking? Or how would you solve this problem knowing the above requirement?

The usual way to do this is to use a reader-writer lock, specifically ReaderWriterLockSlim . This is a monitor-like lock that optimizes for frequent read access and infrequent write access, and it supports multiple concurrent readers and a single writer, which appears to be exactly your use case.

However, it does seem to come at a moderate cost. I wrote two tests - one that uses a ReaderWriterLockSlim to do things correctly, and one which uses your implementation with the only change being an disposed-exception retry loop. In my case I changed senders 20 times, once every 10 seconds. This is vastly shorter than your proposed use case, but does serve as an estimate of the performance difference.

In the end:

  • The reader/writer lock was able to get through 2878 workunits per millisecond.
  • The bare with retry was able to get through 9940 workunits per millisecond.

Where a 'workunit' is calling the DoWork method which calls Thread.SpinWait(100) . Code is posted below if you'd like to test for yourself.

Edit:

I adjusted the Thread.SpinWait() call to change the balance of how much time is spent on locking versus 'working'. With a spin-wait of around 900-1000 on my machine, both implementations ran at the same, about 1000 work-units / millisecond. That should've been obvious from the results above, but I did want to just run the sanity check.

As it is, the original results show that we're able to process about 2.8 million requests a second using the lock; at least on my machine which is a 4-core Intel CPU, "Intel Core 2 Quad CPU Q9650 @ 3.00 GHz". Given that you're striving for 10k requests/second, it looks like you've got about an order of magnitude headroom before the locking starts to become a significant proportion of your CPU usage.


#define USE_READERWRITER

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestProject
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            SenderDispatch dispatch = new SenderDispatch();

            List<Worker> workers = new List<Worker>();

            workers.Add( new Worker( dispatch, "A" ) );
            workers.Add( new Worker( dispatch, "B" ) );
            workers.Add( new Worker( dispatch, "C" ) );
            workers.Add( new Worker( dispatch, "D" ) );

            Thread.CurrentThread.Name = "Main thread";
            Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

            Stopwatch watch = new Stopwatch();

            watch.Start();
            workers.ForEach( x => x.Start() );

            for( int i = 0; i < 20; i++ )
            {
                Thread.Sleep( 10000 );
                dispatch.NewSender();
            }

            Console.WriteLine( "Stopping..." );

            workers.ForEach( x => x.Stop() );
            watch.Stop();

            Console.WriteLine( "Stopped" );

            long sum = workers.Sum( x => x.FinalCount );

            string message = 
                "Sum of worker iterations: " + sum.ToString( "n0" ) + "\r\n" +
                "Total time:               " + ( watch.ElapsedMilliseconds / 1000.0 ).ToString( "0.000" ) + "\r\n" +
                "Iterations/ms:            " + sum / watch.ElapsedMilliseconds;

            MessageBox.Show( message );
        }
    }

    public class Worker
    {
        private SenderDispatch dispatcher;
        private Thread thread;
        private bool working;

        private string workerName;

        public Worker( SenderDispatch dispatcher, string workerName )
        {
            this.dispatcher = dispatcher;
            this.workerName = workerName;

            this.working = false;
        }

        public long FinalCount { get; private set; }

        public void Start()
        {
            this.thread = new Thread( Run );
            this.thread.Name = "Worker " + this.workerName;

            this.working = true;
            this.thread.Start();
        }

        private void Run()
        {
            long state = 0;

            while( this.working )
            {
                this.dispatcher.DoOperation( workerName, state );
                state++;
            }

            this.FinalCount = state;
        }

        public void Stop()
        {
            this.working = false;

            this.thread.Join();
        }
    }

    public class SenderDispatch
    {
        private Sender sender;

        private ReaderWriterLockSlim senderLock;

        public SenderDispatch()
        {
            this.sender = new Sender();
            this.senderLock = new ReaderWriterLockSlim( LockRecursionPolicy.NoRecursion );
        }

        public void DoOperation( string workerName, long value )
        {

#if USE_READERWRITER
            this.senderLock.EnterReadLock();
            try
            {
                this.sender.DoOperation( workerName, value );
            }
            finally
            {
                this.senderLock.ExitReadLock();
            }
#else 
            bool done = false;

            do
            {
                try
                {
                    this.sender.DoOperation( workerName, value );
                    done = true;
                }
                catch (ObjectDisposedException) { }
            }
            while( !done );
#endif

        }

        public void NewSender()
        {
            Sender prevSender;
            Sender newSender;

            newSender = new Sender();

#if USE_READERWRITER
            this.senderLock.EnterWriteLock();
            try
            {
                prevSender = Interlocked.Exchange( ref this.sender, newSender );
            }
            finally
            {
                this.senderLock.ExitWriteLock();
            }
#else
            prevSender = Interlocked.Exchange( ref this.sender, newSender );
            prevSender.Dispose();

#endif
            prevSender.Dispose();

        }
    }

    public class Sender : IDisposable
    {
        private bool disposed;

        public Sender()
        {
            this.disposed = false;
        }

        public void DoOperation( string workerName, long value )
        {
            if( this.disposed )
            {
                throw new ObjectDisposedException( 
                    "Sender",
                    string.Format( "Worker {0} tried to queue work item {1}", workerName, value ) 
                );
            }

            Thread.SpinWait( 100 );
        }

        public void Dispose()
        {
            this.disposed = true;
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM