There's a server which does the following:
- Receive request with transaction id
- Load corresponding transaction from storage. New transaction object is returned each time
- Process transaction
- Save updated transaction back to storage
The task is to properly synchronize steps 2, 3 and 4.
Since for each request new instance of transaction object will be created, I decided to create TransactionService
class with AcquireTransactionLock
method to be called by request handling code.
class Transaction
{
public int Id { get; set; }
public int Counter { get; set; }
public void Process()
{
Counter++;
}
}
class TransactionService
{
private readonly object syncRoot;
private readonly Dictionary<int, Tuple<int, object>> transactionLockMap; // id -> (referenceCount, lock).
public TransactionService()
{
syncRoot = new object();
transactionLockMap = new Dictionary<int, Tuple<int, object>>();
}
public IDisposable AcquireTransactionLock(int transactionId)
{
return new TransactionLock(transactionId, this);
}
class TransactionLock : IDisposable
{
private readonly int transactionId;
private readonly TransactionService transactionService;
public TransactionLock(int transactionId, TransactionService transactionService)
{
this.transactionId = transactionId;
this.transactionService = transactionService;
Tuple<int, object> transactionLock;
lock (transactionService.syncRoot)
{
if (!transactionService.transactionLockMap.TryGetValue(transactionId, out transactionLock))
{
transactionLock = Tuple.Create(1, new object());
}
else
{
transactionLock = Tuple.Create(transactionLock.Item1 + 1, transactionLock.Item2);
}
transactionService.transactionLockMap[transactionId] = transactionLock;
}
Monitor.Enter(transactionLock.Item2);
}
public void Dispose()
{
Tuple<int, object> transactionLock;
lock (transactionService.syncRoot)
{
transactionLock = transactionService.transactionLockMap[transactionId];
transactionLock = Tuple.Create(transactionLock.Item1 - 1, transactionLock.Item2);
if (0 == transactionLock.Item1)
{
transactionService.transactionLockMap.Remove(transactionId);
}
else
{
transactionService.transactionLockMap[transactionId] = transactionLock;
}
}
Monitor.Exit(transactionLock.Item2);
}
}
}
The idea is that processing thread will use transactionService.AcquireTransactionLock(transactionId)
in using
block to synchronize transaction processing code.
Usage demo:
public class EntryPoint
{
public static int Main()
{
var random = new Random();
var transactionPersistenceContext = new TransactionPersistenceContext();
var transactionService = new TransactionService();
Action<object> transactionProcessor = (object parameter) =>
{
var transactionId = (int) parameter;
for (int i = 0; i < 1000; i++)
{
using (transactionService.AcquireTransactionLock(transactionId))
{
var transaction = transactionPersistenceContext.GetTransaction(transactionId);
transaction.Process();
Thread.Sleep(TimeSpan.FromMilliseconds(random.Next(10)));
transactionPersistenceContext.PersistTransaction(transaction);
}
}
};
var threads = Enumerable.
Range(1, 8).
Select(_ => new Thread(new ParameterizedThreadStart(transactionProcessor))).
ToArray();
for (var i = 0; i < threads.Length; i++)
{
threads[i].Start(0 == i % 2 ? 1 : 2);
}
foreach (var thread in threads)
{
thread.Join();
}
// Counters should count to 4000.
var transaction1 = transactionPersistenceContext.GetTransaction(1);
Console.WriteLine("transaction1.Counter: {0}.", transaction1.Counter);
var transaction2 = transactionPersistenceContext.GetTransaction(2);
Console.WriteLine("transaction2.Counter: {0}.", transaction2.Counter);
return 0;
}
}
Full code here: https://dotnetfiddle.net/ztd2Ar
Questions:
- Is there any easier or more elegant way to do this?
- Is there any synchronization issue present in this implementation?
Lock
which will askTransactionService
for the actualobject
to lock. WhenLock
instance is released- it will call another method ofTransactionService
via lambda to return thatobject
back. \$\endgroup\$