2

マルチスレッド トランザクションとエンティティ フレームワークに問題があります。トランザクションで動作するスレッドがあり、同じトランザクション内でさらにいくつかのワーカー スレッドを動作させたいと考えています。次のコードは状況を示しています (EF コンテキストに 1 つのダミー エンティティがあり、コードは基本的に 5 つのスレッドを生成します。各スレッド内にいくつかのエンティティを挿入し、最後にメイン スレッドで、DB で作業を続けたいと考えていますが、プロセス全体を 1 つのトランザクションに分離するため):

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() を呼び出した後だと思いますが、よくわかりません。トランザクションが中止される理由は何ですか?

4

2 に答える 2

1

ここで何が問題なのかを発見しました。この記事に基づいて、1 つのトランザクション内で 2 つの 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 メソッドを同期することです。

于 2012-10-21T14:34:12.303 に答える
0

SqlConnection はスレッド セーフではありません (また、EF ObjectContext/DbContect もスレッド セーフではありません)。したがって、これは、コンテキストと接続へのアクセスを同期する場合にのみ機能します。CPU を集中的に使用するものを並行して処理し、すべてのスレッドが終了した後にすべての変更を 1 つのスレッドに書き込むモデルを考え出します。

于 2012-10-16T15:52:12.677 に答える