多线程事务与实体框架中止

时间:2012-10-16 15:40:53

标签: c# entity-framework-4 threadpool transactionscope

我遇到了多线程事务和实体框架的问题。我有一个在事务中运行的线程,我希望在同一个事务中有更多的工作线程。下面的代码说明了情况(在EF上下文中有一个虚拟实体,代码基本上产生了5个线程,我想在每个线程中插入一些实体,在主线程的最后,我想继续使用DB,但是保持整个过程在一个交易中被隔离):

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();
}

问题是,mc.SaveChanges();会抛出异常。内部异常是TransactionException:“该操作对事务状态无效。”似乎在某些时候,交易被中止。我认为它是在第一个线程调用SaveChanges()之后,但我不确定。知道交易中止的原因吗?

2 个答案:

答案 0 :(得分:1)

我发现这里发生了什么问题。基于this article,我发现在一个事务中同时使用两个MSSQL服务器连接是不可能的。我还发现我在以前的代码中没有正确处理依赖事务。我的工作插图代码如下:

    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();            
    }
}

编写多线程业务层可能不是很好的方法,但这种共享事务方法在我的案例中非常有用。进行此项工作的唯一修改是覆盖Db上下文并在测试运行中同步save方法。

答案 1 :(得分:0)

SqlConnection不是线程安全的(并且EF ObjectContext / DbContect也不是线程安全的),因此这仅在您同步对上下文和连接的访问​​时才有效。您想出了一个模型,您可以并行处理CPU密集型内容,并在所有线程完成后在一个线程中写入所有更改。