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:
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.