2

I have a problem with multithreaded transaction and entity framework. I have a thread, which operates in transaction and I would like to have a few more worker threads working within same transaction. The following code illustrates situation (there is one dummy entity in EF context, the code basically spawns 5 threads, I would like to insert some entities within each thread and at the end in main thread, I would like to continue working with DB, but to keep whole process isolated in ONE transaction):

using(var scope = new TransactionScope())
{
    int cnt = 5;
    ManualResetEvent[] evt = new ManualResetEvent[cnt];

    for(int i = 0; i < cnt; i++)
    {
        var sink = new ManualResetEvent(false);
        evt[i] = sink;

        var tr = Transaction.Current.DependentClone(
            DependentCloneOption.BlockCommitUntilComplete);

        Action run = () =>
        {
            using (var scope2 = new TransactionScope(tr))
            {
                using (var mc = new ModelContainer())
                {
                    mc.EntitySet.Add(new Entity()
                    {
                        MyProp = "test"
                    });
                    mc.SaveChanges();
                }
            }

            sink.Set();
        };

        ThreadPool.QueueUserWorkItem(r => run());
    }

    ManualResetEvent.WaitAll(evt);

    using (var mc = new ModelContainer())
    {
        Console.WriteLine(mc.EntitySet.Count());
    }
    Console.ReadKey();
}

The problem is, that exception is thrown on mc.SaveChanges();. Inner exception is TransactionException: "The operation is not valid for the state of the transaction." It seems that at some point, transaction is aborted. I think it is after first thread calls SaveChanges(), but Im not sure. Any idea why transaction is aborted?

4

2 回答 2

1

I discovered what was the problem happening here. Based on this article, I found that it is impossible to work with two MSSQL server connections within one transaction at the same time. I also discovered that I was not properly handling dependent transaction in previous code. My working illustration code follows:

    class Context
    {
        public ManualResetEvent sink;
        public DependentTransaction transaction;
    }

    static object syncRoot = new object();

    static void Main(string[] args)
    {
        using (var scope = new TransactionScope())
        {
            int cnt = 5;
            ManualResetEvent[] evt = new ManualResetEvent[cnt];

            for (int i = 0; i < cnt; i++)
            {
                var sink = new ManualResetEvent(false);
                evt[i] = sink;

                var context = new Context()
                {
                    // clone transaction
                    transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete),
                    sink = sink
                };

                ThreadPool.QueueUserWorkItem(new WaitCallback(Run), context);
            }

            // wait for all threads to finish
            ManualResetEvent.WaitAll(evt);

            using (var mc = new ModelContainer())
            {
                // check database content
                Console.WriteLine(mc.EntitySet.Count());
            }

            // after test is done, the transaction is rolled back and the database state is untouched
            Console.ReadKey();
        }
    }

    static void Run(object state)
    {
        var context = state as Context;

        // set ambient transaction
        Transaction oldTran = Transaction.Current;
        Transaction.Current = context.transaction;

        using (var mc = new ModelContainer())
        {
            mc.EntitySet.Add(new Entity()
            {
                MyProp = "test"
            });

            // synchronize database access
            lock (syncRoot)
            {
                mc.SaveChanges();
            }
        }

        // release dependent transaction
        context.transaction.Complete();            
        context.transaction.Dispose();

        Transaction.Current = oldTran;

        context.sink.Set();            
    }
}

It is probably not very nice way of writing multithreaded bussiness layer, but this shared transaction approach is WERY useful for testing in my case. The only modification need to make this work is to override Db context and synchronize save method in test runs.

于 2012-10-21T14:34:12.303 回答
0

A SqlConnection is not thread-safe (and an EF ObjectContext/DbContect isn't thread-safe as well), so this will only work when you synchronize the access to the context and connection. You coming up with a model where you process the CPU intensive stuff in parallel and write all changes in one thread after all threads have finished.

于 2012-10-16T15:52:12.677 回答