I have developed the following code to hold n latest items it has received, and when asked for provide these n latest items. The interface is:
public interface ICircularBuffer<T> {
void Put(T item); // put an item
T[] Read(); // provides the last "n" requests
}
The idea is to build a buffer that will hold "n" latest items that have been put and return those items when asked for.
For example:
var buf = new ConcurrentBuffer<int>(3); // creates a buffer of size = 3
buf.Put(15);
buf.Put(10);
buf.Put(20);
int[] arr = buf.Read() // returns - 20,10,15
buf.Put(25);
int[] arr = buf.Read() // returns - 25,20,10
The implementation uses "locks" and array to represent the circular buffer:
public class ConcuurentCircularBuffer<T> : ICircularBuffer<T> {
private T[] buffer;
private int last = 0;
private int sz;
private object lockObject = new object();
public ConcuurentCircularBuffer(int sz) {
// array index starts at 1
this.sz = sz;
buffer = new T[sz + 1];
}
public void Put(T item) {
lock (lockObject) {
last++;
last = last > sz ? 1 : last;
buffer[last] = item;
}
}
public T[] Read() {
T[] arr = new T[sz];
lock (lockObject) {
int iterator = 0;
for (int read = 0; read < sz; read++) {
int index = last - iterator;
index = index <= 0 ? (sz + index) : index;
if (buffer[index] != null) {
arr[iterator] = buffer[index];
} else {
break;
}
iterator++;
}
}
return arr;
}
}
The unit tests I am using to check the performance are as follows:
[TestMethod()]
public void TestParallelPut() {
int sz = 10;
ConcuurentCircularBuffer<int> buf = new ConcuurentCircularBuffer<int>(sz);
Stopwatch watch = new Stopwatch();
ManualResetEventSlim evt = new ManualResetEventSlim(false);
int nTimes = 100000;
Task[] tasks = new Task[nTimes];
watch.Start();
for (int i = 0; i < nTimes; i++) {
tasks[i] = Task.Run(() => {
evt.Wait();
buf.Put(i);
});
}
// signal all to proceed
evt.Set();
Task.WaitAll(tasks);
watch.Stop();
double timeMs = watch.Elapsed.TotalMilliseconds;
TestContext.WriteLine("Buffer size: {0}", sz);
TestContext.WriteLine("Average time per put: {0} ms, total time taken for {1} parallel requests: {2} ms",timeMs/nTimes,nTimes,timeMs);
}
This test tries to simulate maximum contention, by trying to put all n items in parallel.
The following test, puts n items in parallel, and also continues to read in parallel:
[TestMethod()]
public void TestParallelPutAndRead() {
int sz = 10;
ConcuurentCircularBuffer<int> buf = new ConcuurentCircularBuffer<int>(sz);
Stopwatch watch = new Stopwatch();
ManualResetEventSlim evt = new ManualResetEventSlim(false);
ManualResetEventSlim completeEvt = new ManualResetEventSlim(false);
int nTimes = 100000;
Task[] tasks = new Task[nTimes];
watch.Start();
for (int i = 0; i < nTimes; i++) {
tasks[i] = Task.Run(() => {
evt.Wait();
buf.Put(i);
});
}
// start reading, until told to stop
Task.Run(() => {
for(;;) {
if (completeEvt.IsSet) {
break;
} else {
int[] arr = buf.Read();
Assert.IsTrue(arr.Length == sz);
}
}
});
// signal all to proceed
evt.Set();
Task.WaitAll(tasks);
completeEvt.Set();
watch.Stop();
double timeMs = watch.Elapsed.TotalMilliseconds;
TestContext.WriteLine("Test ParallelPut and Read - Buffer size: {0}", sz);
TestContext.WriteLine("Average time per put: {0} ms, total time taken for {1} parallel requests: {2} ms", timeMs / nTimes, nTimes, timeMs);
}
The following test, start "nTasks" in parallel and each task puts nTimes/nTasks. Another tasks keeps reading it as well:
[TestMethod()]
public void TestPutAndNParallelAndRead() {
int sz = 10;
ConcuurentCircularBuffer<int> buf = new ConcuurentCircularBuffer<int>(sz);
Stopwatch watch = new Stopwatch();
ManualResetEventSlim evt = new ManualResetEventSlim(false);
ManualResetEventSlim completeEvt = new ManualResetEventSlim(false);
int nTimes = 1000000;
int nTasks = 100;
Task[] tasks = new Task[nTasks];
watch.Start();
for (int i = 0; i < nTasks; i++) {
tasks[i] = Task.Run(() => {
evt.Wait();
for (int n = 0; n < nTimes / nTasks; n++) {
buf.Put(i);
}
});
}
// start reading, until told to stop
Task.Run(() => {
for (;;) {
if (completeEvt.IsSet) {
break;
} else {
int[] arr = buf.Read();
Assert.IsTrue(arr.Length == sz);
}
}
});
// signal all to proceed
evt.Set();
Task.WaitAll(tasks);
completeEvt.Set();
watch.Stop();
double timeMs = watch.Elapsed.TotalMilliseconds;
TestContext.WriteLine("Test Put and {0} Parallel Put and Read - Buffer size: {1}", nTasks,sz);
TestContext.WriteLine("Average time per put: {0} ms, total time taken for {1} requests: {2} ms", timeMs / nTimes, nTimes, timeMs);
}
The tests results are follows on Windows 7 Enterprise with Intel Core i5-4310U CPU @ 2.00 GHz:
Test Name: TestParallelPut
TestContext Messages:
Test ParallelPut - Buffer size: 10 Average time per put: 0.002393078 ms, total time taken for 100000 parallel requests: 239.3078 ms
Test Name: TestParallelPutAndRead
Test Outcome: Passed
TestContext Messages: Test ParallelPut and Read - Buffer size: 10 Average time per put: 0.002982335 ms, total time taken for 100000 parallel requests:
298.2335 ms
Test Name: TestPutAndNParallelAndRead
Test Outcome: Passed
TestContext Messages: Test Put and 100 Parallel Put and Read - Buffer size: 10 Average time per put: 0.0003211726 ms, total time taken for 1000000 requests: 321.1726 ms
Is there any alternative approach (with better performance) that can be followed to achieve the required functionality?
Please review the code and tests.