Managing Concurrency with DependentTransaction

The Transaction object is created using the DependentClone method. Its sole purpose is to guarantee that the transaction cannot commit while some other pieces of code (for example, a worker thread) are still performing work on the transaction. When the work done within the cloned transaction is complete and ready to be committed, it can notify the creator of the transaction using the Complete method. Thus, you can preserve the consistency and correctness of data.

The DependentTransaction class can also be used to manage concurrency between asynchronous tasks. In this scenario, the parent can continue to execute any code while the dependent clone works on its own tasks. In other words, the parent's execution is not blocked until the dependent completes.

Creating a Dependent Clone

To create a dependent transaction, call the DependentClone method and pass the DependentCloneOption enumeration as a parameter. This parameter defines the behavior of the transaction if Commit is called on the parent transaction before the dependent clone indicates that it is ready for the transaction to commit (by calling the Complete method). The following values are valid for this parameter:

  • BlockCommitUntilComplete creates a dependent transaction that blocks the commit process of the parent transaction until the parent transaction times out, or until Complete is called on all dependents indicating their completion. This is useful when the client does not want the parent transaction to commit until the dependent transactions have completed. If the parent finishes its work earlier than the dependent transaction and calls Commit on the transaction, the commit process is blocked in a state where additional work can be done on the transaction and new enlistments can be created, until all of the dependents call Complete. As soon as all of them have finished their work and call Complete, the commit process for the transaction begins.

  • RollbackIfNotComplete, on the other hand, creates a dependent transaction that automatically aborts if Commit is called on the parent transaction before Complete is called. In this case, all the work done in the dependent transaction is intact within one transaction lifetime, and no one has a chance to commit just a portion of it.

The Complete method must be called only once when your application finishes its work on the dependent transaction; otherwise, a InvalidOperationException is thrown. After this call is invoked, you must not attempt any additional work on the transaction, or an exception is thrown.

The following code example shows how to create a dependent transaction to manage two concurrent tasks by cloning a dependent transaction and passing it to a worker thread.

public class WorkerThread  
{  
    public void DoWork(DependentTransaction dependentTransaction)  
    {  
        Thread thread = new Thread(ThreadMethod);  
        thread.Start(dependentTransaction);
    }  
  
    public void ThreadMethod(object transaction)
    {
        DependentTransaction dependentTransaction = transaction as DependentTransaction;  
        Debug.Assert(dependentTransaction != null);
        try  
        {  
            using(TransactionScope ts = new TransactionScope(dependentTransaction))  
            {  
                /* Perform transactional work here */
                ts.Complete();  
            }  
        }  
        finally  
        {  
            dependentTransaction.Complete();
             dependentTransaction.Dispose();
        }  
    }  
  
//Client code
using(TransactionScope scope = new TransactionScope())  
{  
    Transaction currentTransaction = Transaction.Current;  
    DependentTransaction dependentTransaction;
    dependentTransaction = currentTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete);  
    WorkerThread workerThread = new WorkerThread();  
    workerThread.DoWork(dependentTransaction);  
    /* Do some transactional work here, then: */  
    scope.Complete();  
}  

The client code creates a transactional scope that also sets the ambient transaction. You should not pass the ambient transaction to the worker thread. Instead, you should clone the current (ambient) transaction by calling the DependentClone method on the current transaction, and pass the dependent to the worker thread.

The ThreadMethod method executes on the new thread. The client starts a new thread, passing the dependent transaction as the ThreadMethod parameter.

Because the dependent transaction is created with BlockCommitUntilComplete, you are guaranteed that the transaction cannot be committed until all of the transactional work done on the second thread is finished and Complete is called on the dependent transaction. This means that if the client's scope ends (when it tries to dispose of the transaction object at the end of the using statement) before the new thread calls Complete on the dependent transaction, the client code blocks until Complete is called on the dependent. Then the transaction can finish committing or aborting.

Concurrency Issues

There are a few additional concurrency issues that you need to be aware of when using the DependentTransaction class:

  • If the worker thread rolls back the transaction but the parent tries to commit it, a TransactionAbortedException is thrown.

  • You should create a new dependent clone for each worker thread in the transaction. Do not pass the same dependent clone to multiple threads, because only one of them can call Complete on it.

  • If the worker thread spawns a new worker thread, make sure to create a dependent clone from the dependent clone and pass it to the new thread.

See also