It's my third post about
TransactionScope. This time I'll write about using it with multi-threading. Let's start with the following code:
using (var t = new TransactionScope())
{
var t1 = Task.Factory.StartNew(UpdateDatabase);
var t2 = Task.Factory.StartNew(UpdateDatabase);
Task.WaitAll(t1, t2);
t.Complete();
}
private static void UpdateDatabase()
{
using (var c = new SqlConnection(connectionString))
{
c.Open();
WriteDebugInfo();
new SqlCommand(updateCommand, c).ExecuteNonQuery();
}
}
private static void WriteDebugInfo()
{
Console.WriteLine("Thread= {0}, LocalIdentifier = {1}, DistributedIdentifier = {2}",
Thread.CurrentThread.ManagedThreadId,
Transaction.Current?.TransactionInformation.LocalIdentifier,
Transaction.Current?.TransactionInformation.DistributedIdentifier);
}
It seems simple but it doesn't work. The problem is that a connection that is created in
UpdateDatabase method will not participate in any transaction. We can also observe that
WriteDebugInfo will write empty transaction identifiers to the console. It happens because in order to read an ambient transaction (the transaction the code is executed in)
TransactionScope uses
Transaction.Current property which is thread static (i.e. specific for a thread).
To overcome this issue we have two possibilities. The first one is to use
DependentTransacion. However, I'll not show how to do it because since .NET 4.5.1 there is a better way -
TransactionScopeAsyncFlowOption enum. Let's try.
using (var t = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
...
}
Unfortunately, there is a big chance that this time we will get
TransactionException with the message
The operation is not valid for the state of the transaction. in the line with
ExecuteNonQuery. The simplified stack trace is:
at System.Transactions.TransactionStatePSPEOperation.get_Status(InternalTransaction tx)
at System.Transactions.TransactionInformation.get_Status()
...
at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
at Sandbox.Program.UpdateDatabase(Object o)
I read a lot about this but nobody was able to explain why it happens. I also looked into the source code of
TransactionStatePSPEOperation class. It was instructive because I learned what is
PSPE - Promotable Single Phase Enlistment. However, it also didn't give me an exact answer.
So, I played a little bit with the code and I noticed that the problem occurs when:
- One thread tries to run ExecuteNonQuery.
- Another thread waits for the opening of the connection.
However, when both connections were already opened then the exception wasn't thrown.
At this point it is worth reminding one thing - when there are 2 or more connections opened in a transaction scope at the same time then a transaction is promoted to a distributed one. I'm not 100% sure but I think that the problem occurs because it is not allowed to use a connection which participates in a transaction which is in the middle of promotion to the distributed one. So, the solution is to assure that a transaction will be distributed from the beginning. Here is a fixed code with a magic line (I found it
here):
using (var t = new TransactionScope())
{
//The magic line that makes a transaction distributed
TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
var t1 = Task.Factory.StartNew(UpdateDatabase);
var t2 = Task.Factory.StartNew(UpdateDatabase);
Task.WaitAll(t1, t2);
t.Complete();
}
Nonetheless, the more I think about this the more convinced I'm that using
TransactionScope with multi-threading is asking for problems.